@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.
- package/README.md +7 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +58 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +101 -27
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +14 -10
- package/src/cli/commands/search.ts +84 -25
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +6 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/glossary.ts +2 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +189 -11
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +51 -5
- package/src/cli/listing-arg.ts +63 -10
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +49 -6
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +51 -6
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +10 -5
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +8 -2
- package/src/components/layout/pane/footer/model.ts +73 -6
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +4 -2
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +60 -26
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +16 -3
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +165 -90
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +324 -0
- package/src/data/sqlite/checkpointer.ts +285 -0
- package/src/data/sqlite/database.ts +80 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +5 -4
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +6 -4
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +19 -3
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +16 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +127 -29
- package/src/plugins/builtin/thirteenf/data.ts +165 -35
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
- package/src/plugins/builtin/thirteenf/types.ts +14 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +64 -42
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +5 -2
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +6 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/resolve.ts +2 -1
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +12 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +25 -9
- package/src/telemetry/stall-reports.ts +218 -13
- package/src/test-support/outage.ts +8 -0
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name-listing.ts +59 -29
- package/src/tickers/company-name.ts +84 -1
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +104 -8
- package/src/tickers/search/index.ts +5 -2
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/market.ts +5 -3
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +75 -8
- package/src/time-series/resolve.ts +32 -4
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/time-series/types.ts +7 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +40 -3
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +17 -1
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +122 -5
- package/src/utils/perf-marks.ts +58 -12
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- 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
|
-
|
|
237
|
-
if (input?.
|
|
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
|
-
|
|
304
|
-
if (input?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]);
|