gloomberb 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/types.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/fred-series.ts +15 -5
- package/src/cli/commands/market.ts +290 -159
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/overview.ts +31 -6
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +105 -52
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +31 -7
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/symbol-list.ts +51 -16
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +22 -7
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +64 -16
- package/src/components/chart/composite/price-axis-labels.tsx +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/command-bar/workflow/collection-actions.ts +4 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/onboarding/wizard-positions.ts +8 -0
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +57 -22
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/analytics/risk-client.ts +44 -11
- package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
- package/src/plugins/builtin/analytics/risk-model.ts +79 -16
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
- package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/custom-view/loader.ts +3 -2
- package/src/plugins/builtin/custom-view/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +38 -2
- package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/executives/data.ts +24 -8
- package/src/plugins/builtin/executives/index.tsx +2 -18
- package/src/plugins/builtin/executives/pane.tsx +34 -10
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-movers/client.ts +2 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -3
- package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
- package/src/plugins/builtin/market-movers/session.ts +25 -10
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
- package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +33 -8
- package/src/plugins/builtin/notes/export.ts +49 -3
- package/src/plugins/builtin/notes/files.ts +14 -3
- package/src/plugins/builtin/notes/index.tsx +3 -3
- package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
- package/src/plugins/builtin/notes/store.ts +13 -3
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
- package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/thirteenf/api.ts +4 -1
- package/src/plugins/builtin/thirteenf/data.ts +5 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +61 -0
- package/src/portfolio/mutations.ts +16 -2
- package/src/portfolio/position-metrics.ts +45 -4
- package/src/portfolio/position-quantity.ts +43 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +63 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/team/collections.ts +2 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/ticker.ts +9 -0
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +41 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PluginConfigState } from "../../../../types/plugin";
|
|
2
|
+
import { logFailure } from "../../../../utils/log-failure";
|
|
2
3
|
import { DEFAULT_FEEDS } from "./default-feeds";
|
|
3
4
|
import { hashString } from "./hash";
|
|
4
5
|
import type { RssFeedConfig } from "./rss/parser";
|
|
@@ -8,11 +9,13 @@ const USER_FEEDS_KEY = "feeds";
|
|
|
8
9
|
const DISABLED_DEFAULT_FEED_IDS_KEY = "disabledDefaultFeedIds";
|
|
9
10
|
|
|
10
11
|
const DEFAULT_FEED_IDS = new Set(DEFAULT_FEEDS.map((feed) => feed.id));
|
|
12
|
+
const UNREADABLE_SETTINGS = "News feed settings could not be read. Saved settings were kept.";
|
|
11
13
|
|
|
12
14
|
export interface NewsFeedSettings {
|
|
13
15
|
userFeeds: RssFeedConfig[];
|
|
14
16
|
disabledDefaultFeedIds: string[];
|
|
15
17
|
needsMigration: boolean;
|
|
18
|
+
error?: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
export interface UserNewsFeedInput {
|
|
@@ -24,14 +27,15 @@ export interface UserNewsFeedInput {
|
|
|
24
27
|
enabled?: boolean;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
function parseJsonArray(value: unknown): unknown[] {
|
|
30
|
+
function parseJsonArray(value: unknown): unknown[] | null {
|
|
28
31
|
if (Array.isArray(value)) return value;
|
|
29
|
-
if (typeof value !== "string"
|
|
32
|
+
if (typeof value !== "string") return [];
|
|
30
33
|
try {
|
|
31
34
|
const parsed = JSON.parse(value);
|
|
32
35
|
return Array.isArray(parsed) ? parsed : [];
|
|
33
|
-
} catch {
|
|
34
|
-
|
|
36
|
+
} catch (error) {
|
|
37
|
+
logFailure("news.feed-config", "Read stored news feed settings failed")(error);
|
|
38
|
+
return null;
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -93,6 +97,7 @@ function normalizeUserFeed(value: unknown): RssFeedConfig | null {
|
|
|
93
97
|
enabled: record.enabled !== false,
|
|
94
98
|
});
|
|
95
99
|
} catch {
|
|
100
|
+
// Intentional ignore: invalid stored feed entries are omitted during normalization.
|
|
96
101
|
return null;
|
|
97
102
|
}
|
|
98
103
|
}
|
|
@@ -131,14 +136,20 @@ function sameIdList(raw: unknown, normalized: string[]): boolean {
|
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSettings {
|
|
134
|
-
|
|
139
|
+
return parseNewsFeedSettings(configState.get(USER_FEEDS_KEY), configState.get(DISABLED_DEFAULT_FEED_IDS_KEY));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function parseNewsFeedSettings(rawUserFeeds: unknown, rawDisabledIds: unknown): NewsFeedSettings {
|
|
135
143
|
const userFeedValues = parseJsonArray(rawUserFeeds);
|
|
136
|
-
const userFeeds = userFeedValues
|
|
144
|
+
const userFeeds = (userFeedValues ?? [])
|
|
137
145
|
.map(normalizeUserFeed)
|
|
138
146
|
.filter((feed): feed is RssFeedConfig => !!feed);
|
|
139
147
|
|
|
140
|
-
const
|
|
141
|
-
const disabledDefaultFeedIds = normalizeDisabledDefaultFeedIds(
|
|
148
|
+
const disabledIdValues = parseJsonArray(rawDisabledIds);
|
|
149
|
+
const disabledDefaultFeedIds = normalizeDisabledDefaultFeedIds(disabledIdValues ?? []);
|
|
150
|
+
if (userFeedValues === null || disabledIdValues === null) {
|
|
151
|
+
return { userFeeds, disabledDefaultFeedIds, needsMigration: false, error: UNREADABLE_SETTINGS };
|
|
152
|
+
}
|
|
142
153
|
|
|
143
154
|
const needsMigration = (rawUserFeeds !== null && !sameUserFeeds(rawUserFeeds, userFeeds))
|
|
144
155
|
|| (rawDisabledIds !== null && !sameIdList(rawDisabledIds, disabledDefaultFeedIds));
|
|
@@ -146,15 +157,24 @@ export function loadNewsFeedSettings(configState: PluginConfigState): NewsFeedSe
|
|
|
146
157
|
return { userFeeds, disabledDefaultFeedIds, needsMigration };
|
|
147
158
|
}
|
|
148
159
|
|
|
160
|
+
function requireReadableSettings(configState: PluginConfigState): NewsFeedSettings {
|
|
161
|
+
const settings = loadNewsFeedSettings(configState);
|
|
162
|
+
if (settings.error) throw new Error(settings.error);
|
|
163
|
+
return settings;
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
export async function saveNewsFeedSettings(
|
|
150
167
|
configState: PluginConfigState,
|
|
151
168
|
settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds">,
|
|
152
169
|
): Promise<void> {
|
|
170
|
+
requireReadableSettings(configState);
|
|
153
171
|
await configState.set(USER_FEEDS_KEY, settings.userFeeds);
|
|
172
|
+
requireReadableSettings(configState);
|
|
154
173
|
await configState.set(DISABLED_DEFAULT_FEED_IDS_KEY, settings.disabledDefaultFeedIds);
|
|
155
174
|
}
|
|
156
175
|
|
|
157
|
-
export function getEnabledNewsFeeds(settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds">): RssFeedConfig[] {
|
|
176
|
+
export function getEnabledNewsFeeds(settings: Pick<NewsFeedSettings, "userFeeds" | "disabledDefaultFeedIds" | "error">): RssFeedConfig[] {
|
|
177
|
+
if (settings.error) return [];
|
|
158
178
|
const disabled = new Set(settings.disabledDefaultFeedIds);
|
|
159
179
|
return [
|
|
160
180
|
...DEFAULT_FEEDS.filter((feed) => feed.enabled && !disabled.has(feed.id)),
|
|
@@ -166,7 +186,7 @@ export async function addUserNewsFeed(
|
|
|
166
186
|
configState: PluginConfigState,
|
|
167
187
|
input: UserNewsFeedInput,
|
|
168
188
|
): Promise<RssFeedConfig> {
|
|
169
|
-
const settings =
|
|
189
|
+
const settings = requireReadableSettings(configState);
|
|
170
190
|
const feed = createUserFeed(input);
|
|
171
191
|
const nextUserFeeds = [
|
|
172
192
|
...settings.userFeeds.filter((entry) => entry.id !== feed.id),
|
|
@@ -184,7 +204,7 @@ export async function updateUserNewsFeed(
|
|
|
184
204
|
feedId: string,
|
|
185
205
|
patch: Partial<UserNewsFeedInput>,
|
|
186
206
|
): Promise<RssFeedConfig | null> {
|
|
187
|
-
const settings =
|
|
207
|
+
const settings = requireReadableSettings(configState);
|
|
188
208
|
const existing = settings.userFeeds.find((feed) => feed.id === feedId);
|
|
189
209
|
if (!existing) return null;
|
|
190
210
|
const nextFeed = createUserFeed({
|
|
@@ -203,7 +223,7 @@ export async function removeUserNewsFeed(
|
|
|
203
223
|
configState: PluginConfigState,
|
|
204
224
|
feedId: string,
|
|
205
225
|
): Promise<boolean> {
|
|
206
|
-
const settings =
|
|
226
|
+
const settings = requireReadableSettings(configState);
|
|
207
227
|
const nextUserFeeds = settings.userFeeds.filter((feed) => feed.id !== feedId);
|
|
208
228
|
if (nextUserFeeds.length === settings.userFeeds.length) return false;
|
|
209
229
|
await saveNewsFeedSettings(configState, {
|
|
@@ -219,7 +239,7 @@ export async function setDefaultNewsFeedEnabled(
|
|
|
219
239
|
enabled: boolean,
|
|
220
240
|
): Promise<boolean> {
|
|
221
241
|
if (!DEFAULT_FEED_IDS.has(feedId)) return false;
|
|
222
|
-
const settings =
|
|
242
|
+
const settings = requireReadableSettings(configState);
|
|
223
243
|
const disabled = new Set(settings.disabledDefaultFeedIds);
|
|
224
244
|
if (enabled) disabled.delete(feedId);
|
|
225
245
|
else disabled.add(feedId);
|
|
@@ -26,6 +26,7 @@ import { createRssNewsCapability } from "./rss/source";
|
|
|
26
26
|
import { rssCliCommand } from "./rss/cli";
|
|
27
27
|
import { newsFeedHeadless } from "../headless";
|
|
28
28
|
import type { PaneDef } from "../../../../types/plugin";
|
|
29
|
+
import { logFailure } from "../../../../utils/log-failure";
|
|
29
30
|
|
|
30
31
|
const TopPane = createNewsPresetPane({
|
|
31
32
|
paneKey: "top:curated",
|
|
@@ -195,11 +196,15 @@ export const newsWireModule: PluginModule = {
|
|
|
195
196
|
setup(ctx) {
|
|
196
197
|
const initialSettings = loadNewsFeedSettings(ctx.configState);
|
|
197
198
|
if (initialSettings.needsMigration) {
|
|
198
|
-
void saveNewsFeedSettings(ctx.configState, initialSettings)
|
|
199
|
+
void saveNewsFeedSettings(ctx.configState, initialSettings)
|
|
200
|
+
.catch(logFailure("news.feed-config", "Migrate news feed settings failed"));
|
|
199
201
|
}
|
|
200
202
|
|
|
201
203
|
const source = createRssNewsCapability(
|
|
202
|
-
() =>
|
|
204
|
+
() => {
|
|
205
|
+
const settings = loadNewsFeedSettings(ctx.configState);
|
|
206
|
+
return { feeds: getEnabledNewsFeeds(settings), error: settings.error };
|
|
207
|
+
},
|
|
203
208
|
{ persistence: ctx.persistence },
|
|
204
209
|
);
|
|
205
210
|
ctx.registerCapability(source);
|
|
@@ -31,6 +31,7 @@ const isWebUrl = (value: unknown): value is string => {
|
|
|
31
31
|
const url = new URL(value);
|
|
32
32
|
return url.protocol === "https:" || url.protocol === "http:";
|
|
33
33
|
} catch {
|
|
34
|
+
// Intentional ignore: malformed links fail letter validation like other invalid fields.
|
|
34
35
|
return false;
|
|
35
36
|
}
|
|
36
37
|
};
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
GloomPluginContext,
|
|
9
9
|
} from "../../../../../types/plugin";
|
|
10
10
|
import { formatFeedTime } from "../../../../../utils/datetime-format";
|
|
11
|
+
import { logFailure } from "../../../../../utils/log-failure";
|
|
11
12
|
import { openNewsStoryPane } from "./pop-out";
|
|
12
13
|
import { NEWS_QUERY_PRESETS } from "./query-presets";
|
|
13
14
|
|
|
@@ -75,9 +76,11 @@ let warming: Promise<unknown> | null = null;
|
|
|
75
76
|
*/
|
|
76
77
|
async function warmLatestStories(service: NewsService): Promise<void> {
|
|
77
78
|
if (service.getQueryState(NEWS_QUERY_PRESETS.feed).articles.length > 0) return;
|
|
78
|
-
warming ??= service.load(NEWS_QUERY_PRESETS.feed)
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
warming ??= service.load(NEWS_QUERY_PRESETS.feed)
|
|
80
|
+
.catch(logFailure("news.command-bar-search", "Load news for command bar search failed", null))
|
|
81
|
+
.finally(() => {
|
|
82
|
+
warming = null;
|
|
83
|
+
});
|
|
81
84
|
await warming;
|
|
82
85
|
}
|
|
83
86
|
|
|
@@ -14,6 +14,7 @@ import { useInlineTickers } from "../../../../../state/hooks/inline-tickers";
|
|
|
14
14
|
import { truncateToDisplayWidth } from "../../../../../utils/format";
|
|
15
15
|
import { wrapTextLines } from "../../../../../utils/text-wrap";
|
|
16
16
|
import { formatDetailDate } from "../../../../../utils/datetime-format";
|
|
17
|
+
import { logFailure } from "../../../../../utils/log-failure";
|
|
17
18
|
import { mergeNewsArticle } from "../../../../../news/news-model";
|
|
18
19
|
import { usePluginPaneState } from "../../../../runtime";
|
|
19
20
|
import { formatNewsCategory } from "../categories";
|
|
@@ -277,7 +278,7 @@ export function NewsDetailView({ item, focused, width, showTitle = true }: {
|
|
|
277
278
|
}).then((choice) => {
|
|
278
279
|
const url = choice ? coverage[Number(choice)]?.url : undefined;
|
|
279
280
|
if (url) void rendererHost.openExternal(url);
|
|
280
|
-
}).catch((
|
|
281
|
+
}).catch(logFailure("news.detail-view", "Choose news coverage failed"));
|
|
281
282
|
}, [coverage, dialog, rendererHost]);
|
|
282
283
|
usePaneFooter("news-detail:story", () => ({
|
|
283
284
|
order: -1,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { Box } from "../../../../../ui";
|
|
3
3
|
import type { NewsArticle, NewsQuery } from "../../../../../news/types";
|
|
4
4
|
import { useLoadNewsStory, useNewsArticles, useNewsTableLoadMore } from "../../../../../news/hooks";
|
|
5
5
|
import type { PaneProps } from "../../../../../types/plugin";
|
|
6
|
-
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../../../runtime";
|
|
6
|
+
import { useDebouncedPluginPaneState, usePluginConfigState, usePluginPaneState } from "../../../../runtime";
|
|
7
7
|
import { PaneStatusBody, QueryBar, type QueryBarSearch } from "../../../../../components";
|
|
8
8
|
import { NewsDetailView, useNewsArticleDetail } from "./detail-view";
|
|
9
9
|
import {
|
|
@@ -18,6 +18,7 @@ import { newsListEmptyCopy } from "../filter-articles";
|
|
|
18
18
|
import { NEWS_LIST_SEARCH_PLACEHOLDER, useNewsListSearch, useNewsListSearchHint } from "./list-search";
|
|
19
19
|
import { newsMutesApplyToFeed, useNewsMuteFilter } from "../mutes";
|
|
20
20
|
import { usePopOutNewsArticle } from "./pop-out";
|
|
21
|
+
import { parseNewsFeedSettings } from "../feed-config";
|
|
21
22
|
|
|
22
23
|
interface NewsArticleStackOptions {
|
|
23
24
|
/** Prefix of every key the pane persists, and of its footer registration. */
|
|
@@ -146,6 +147,13 @@ export function NewsPresetPane({
|
|
|
146
147
|
emptyStateTitle,
|
|
147
148
|
emptyStateHint,
|
|
148
149
|
}: PaneProps & NewsPresetPaneConfig) {
|
|
150
|
+
const [storedFeeds] = usePluginConfigState<unknown>("feeds", null);
|
|
151
|
+
const [storedDisabledIds] = usePluginConfigState<unknown>("disabledDefaultFeedIds", null);
|
|
152
|
+
// The first query can finish before RSS registers, and desktop queries may
|
|
153
|
+
// use Cloud alone. A settings failure must remain visible in either case.
|
|
154
|
+
const settingsError = useMemo(() => query.feed === "latest"
|
|
155
|
+
? parseNewsFeedSettings(storedFeeds, storedDisabledIds).error
|
|
156
|
+
: undefined, [query.feed, storedFeeds, storedDisabledIds]);
|
|
149
157
|
const newsState = useNewsArticles(query);
|
|
150
158
|
const persisted = usePersistedNewsArticles(`${paneKey}:articles`, newsState.articles);
|
|
151
159
|
const articles = useNewsMuteFilter(persisted, newsMutesApplyToFeed(query.feed));
|
|
@@ -154,7 +162,7 @@ export function NewsPresetPane({
|
|
|
154
162
|
// body rather than a definitive empty wire.
|
|
155
163
|
const loading = newsState.phase === "loading"
|
|
156
164
|
|| (newsState.phase === "refreshing" && persisted.length === 0);
|
|
157
|
-
const error = newsState.error;
|
|
165
|
+
const error = settingsError ?? newsState.error;
|
|
158
166
|
const { search, searchQuery, ...stack } = useNewsArticleStack({
|
|
159
167
|
paneKey,
|
|
160
168
|
articles,
|
|
@@ -15,6 +15,7 @@ import { colors } from "../../../../../theme/colors";
|
|
|
15
15
|
import { collectNewsDisplayTickers } from "../../../../../news/ticker-symbols";
|
|
16
16
|
import { useLoadNewsStory } from "../../../../../news/hooks";
|
|
17
17
|
import { formatFeedTime } from "../../../../../utils/datetime-format";
|
|
18
|
+
import { logFailure } from "../../../../../utils/log-failure";
|
|
18
19
|
import { nextHeaderSort } from "../../../../../utils/sort-values";
|
|
19
20
|
import { formatNewsCategory } from "../categories";
|
|
20
21
|
import { useOpenTickerChoice } from "../../../shared/ticker-choice";
|
|
@@ -270,7 +271,7 @@ export function NewsArticleStackView({
|
|
|
270
271
|
const loadNewsStory = useLoadNewsStory();
|
|
271
272
|
const prefetchStory = useCallback((article: MarketNewsItem) => {
|
|
272
273
|
if ((article.items?.length ?? 0) > 0) return;
|
|
273
|
-
void loadNewsStory(article.id).catch((
|
|
274
|
+
void loadNewsStory(article.id).catch(logFailure("news.table", "Prefetch news story failed"));
|
|
274
275
|
}, [loadNewsStory]);
|
|
275
276
|
|
|
276
277
|
// A crosshair pinned in the tab picks the newest story out by then, and
|
|
@@ -26,8 +26,9 @@ export const rssCliCommand: CliCommandDef = {
|
|
|
26
26
|
authority: 50,
|
|
27
27
|
enabled: true,
|
|
28
28
|
}]);
|
|
29
|
-
const
|
|
30
|
-
ctx.
|
|
29
|
+
const page = await capability.provider.fetchNewsPage!({ feed: "latest", limit: ctx.cliOptions.limit ?? 20 });
|
|
30
|
+
if (page.error) ctx.fail(page.error);
|
|
31
|
+
ctx.printResult({ data: page.articles.map((article) => ({
|
|
31
32
|
title: article.title,
|
|
32
33
|
source: article.source,
|
|
33
34
|
publishedAt: article.publishedAt.toISOString(),
|
|
@@ -14,7 +14,12 @@ export interface FeedElement {
|
|
|
14
14
|
|
|
15
15
|
export function resolveFeedUrl(value: string, base: string): string {
|
|
16
16
|
if (!value.trim()) return "";
|
|
17
|
-
try {
|
|
17
|
+
try {
|
|
18
|
+
return new URL(value.trim(), base).href;
|
|
19
|
+
} catch {
|
|
20
|
+
// Intentional ignore: malformed optional feed links are treated as absent.
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
// Read the structure of a syndication document without executing declarations or
|
|
@@ -14,6 +14,7 @@ export function isManagedNewsFeedUrl(value: string): boolean {
|
|
|
14
14
|
const url = new URL(value);
|
|
15
15
|
return url.origin === "https://api.gloom.sh" && url.pathname.replace(/\/$/, "") === "/news/rss";
|
|
16
16
|
} catch {
|
|
17
|
+
// Intentional ignore: malformed URLs cannot identify the managed news feed.
|
|
17
18
|
return false;
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -129,11 +129,14 @@ function writeFeedCache(
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
export function createRssNewsCapability(
|
|
132
|
-
feedsOrGetter: RssFeedConfig[] | (() => RssFeedConfig[]),
|
|
132
|
+
feedsOrGetter: RssFeedConfig[] | (() => RssFeedConfig[] | { feeds: RssFeedConfig[]; error?: string }),
|
|
133
133
|
options: RssNewsCapabilityOptions = {},
|
|
134
134
|
): NewsCapability {
|
|
135
135
|
const fetchText = options.fetchText ?? ((url: string) => rssClient.fetch(url));
|
|
136
|
-
const getFeeds = () =>
|
|
136
|
+
const getFeeds = () => {
|
|
137
|
+
const selection = Array.isArray(feedsOrGetter) ? feedsOrGetter : feedsOrGetter();
|
|
138
|
+
return Array.isArray(selection) ? { feeds: selection } : selection;
|
|
139
|
+
};
|
|
137
140
|
|
|
138
141
|
async function fetchFeed(feed: RssFeedConfig): Promise<{ articles: MarketNewsItem[]; failed: boolean }> {
|
|
139
142
|
const freshCache = readFeedCache(options.persistence, feed);
|
|
@@ -153,7 +156,9 @@ export function createRssNewsCapability(
|
|
|
153
156
|
|
|
154
157
|
async function fetchPage(query: NewsQuery) {
|
|
155
158
|
if (!supportsQuery(query)) return { articles: [] };
|
|
156
|
-
const
|
|
159
|
+
const selection = getFeeds();
|
|
160
|
+
if (selection.error) return { articles: [], error: selection.error };
|
|
161
|
+
const feeds = selection.feeds.filter((feed) => feed.enabled);
|
|
157
162
|
const results = await Promise.all(feeds.map(fetchFeed));
|
|
158
163
|
const failed = results.filter((result) => result.failed).length;
|
|
159
164
|
return {
|
|
@@ -170,11 +175,15 @@ export function createRssNewsCapability(
|
|
|
170
175
|
supports: supportsQuery,
|
|
171
176
|
getCachedNews(query: NewsQuery): MarketNewsItem[] {
|
|
172
177
|
if (!supportsQuery(query)) return [];
|
|
173
|
-
const
|
|
178
|
+
const selection = getFeeds();
|
|
179
|
+
if (selection.error) return [];
|
|
180
|
+
const enabledFeeds = selection.feeds.filter((feed) => feed.enabled);
|
|
174
181
|
return enabledFeeds.flatMap((feed) => readFeedCache(options.persistence, feed, { allowExpired: true }) ?? []);
|
|
175
182
|
},
|
|
176
183
|
fetchNewsPage: fetchPage,
|
|
177
184
|
async fetchNews(query: NewsQuery): Promise<MarketNewsItem[]> {
|
|
185
|
+
const error = supportsQuery(query) ? getFeeds().error : undefined;
|
|
186
|
+
if (error) throw new Error(error);
|
|
178
187
|
return (await fetchPage(query)).articles;
|
|
179
188
|
},
|
|
180
189
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
2
|
import { withCliServices, withConfigData } from "../../../cli/scoped-context";
|
|
3
|
+
import type { CliServicesContext } from "../../../cli/types";
|
|
3
4
|
import { CLI_COMMAND_GROUPS } from "../../../cli/help";
|
|
4
5
|
import { dryRunNote } from "../../../cli/helpers";
|
|
5
6
|
import { requireArg, takeOption } from "../../../cli/commands/command-utils";
|
|
@@ -7,9 +8,10 @@ import { EXCHANGE_OPTION, listingReadNotes, requireListingArg } from "../../../c
|
|
|
7
8
|
import { createPluginPersistence } from "../../plugin-persistence";
|
|
8
9
|
import type { CliCommandDef } from "../../../types/plugin";
|
|
9
10
|
import { cliStyles } from "../../../utils/cli-output";
|
|
11
|
+
import { errorMessage } from "../../../utils/errors";
|
|
10
12
|
import { logFailure } from "../../../utils/log-failure";
|
|
11
13
|
import { readChatSessionState } from "../chat/controller/persistence";
|
|
12
|
-
import { exportNotesToDirectory, type NotesExportSource } from "./export";
|
|
14
|
+
import { describeExportSkip, exportNotesToDirectory, type NotesExportSource } from "./export";
|
|
13
15
|
import { NotesFiles } from "./files";
|
|
14
16
|
import { CloudNotesStore } from "./store";
|
|
15
17
|
|
|
@@ -42,29 +44,52 @@ export const notesCliCommand: CliCommandDef = {
|
|
|
42
44
|
// (personal and every team), one folder each.
|
|
43
45
|
const dir = args[1] ?? `${config.dataDir}/notes-export-${new Date().toISOString().slice(0, 10)}`;
|
|
44
46
|
const sources: NotesExportSource[] = [{ label: "local", store: notes }];
|
|
45
|
-
|
|
47
|
+
// A Cloud source left out or unreadable is a warning, as `quote` reports
|
|
48
|
+
// a symbol it found nothing for: the local files are always written.
|
|
49
|
+
const warnings: string[] = [];
|
|
50
|
+
const leftOut = (what: string, failure: string) => (error: unknown) => {
|
|
51
|
+
logFailure("notes.cli", failure)(error);
|
|
52
|
+
warnings.push(`${what} (${errorMessage(error)}).`);
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
const addCloudSources = async (services: CliServicesContext) => {
|
|
46
56
|
const cloudPersistence = createPluginPersistence(services.persistence.pluginState, services.persistence.resources, "plugin:gloomberb-cloud", "gloomberb-cloud");
|
|
47
57
|
const session = readChatSessionState(cloudPersistence, null);
|
|
48
58
|
if (!session?.sessionToken) return;
|
|
49
59
|
apiClient.setSessionToken(session.sessionToken);
|
|
50
60
|
const user = await apiClient.ensureVerifiedSession()
|
|
51
|
-
.catch(
|
|
61
|
+
.catch(leftOut("Skipped Gloom Cloud notes: the saved session could not be checked", "Verify the saved session for the notes export failed"));
|
|
52
62
|
if (!user) return;
|
|
53
63
|
const notesPersistence = createPluginPersistence(services.persistence.pluginState, services.persistence.resources, "plugin:notes", "notes");
|
|
54
64
|
sources.push({ label: "mine", store: new CloudNotesStore({ kind: "user" }, notesPersistence) });
|
|
55
|
-
const teams = await apiClient.listTeams()
|
|
65
|
+
const teams = await apiClient.listTeams()
|
|
66
|
+
.catch(leftOut("Skipped team notes: the team list could not be read", "List teams for the notes export failed")) ?? [];
|
|
56
67
|
for (const team of teams) {
|
|
57
68
|
sources.push({ label: team.name, store: new CloudNotesStore({ kind: "team", teamId: team.id }, notesPersistence) });
|
|
58
69
|
}
|
|
70
|
+
};
|
|
71
|
+
// The Cloud stores read and cache through these services, so the
|
|
72
|
+
// export runs before they close.
|
|
73
|
+
const result = await withCliServices(ctx, async (services) => {
|
|
74
|
+
await addCloudSources(services);
|
|
75
|
+
return ctx.cliOptions.dryRun ? null : await exportNotesToDirectory(dir, sources);
|
|
59
76
|
});
|
|
60
|
-
if (
|
|
61
|
-
ctx.printResult({ data: { dryRun: true, dir, sources: sources.map((source) => source.label) } }, {
|
|
77
|
+
if (!result) {
|
|
78
|
+
ctx.printResult({ data: { dryRun: true, dir, sources: sources.map((source) => source.label) }, ...(warnings.length > 0 ? { warnings } : {}) }, {
|
|
62
79
|
text: (data) => `Would export ${data.sources.join(", ")} notes to ${data.dir}.${dryRunNote(true)}`,
|
|
63
80
|
});
|
|
64
81
|
return;
|
|
65
82
|
}
|
|
66
|
-
|
|
67
|
-
ctx.printResult({
|
|
83
|
+
warnings.push(...result.skipped.map(describeExportSkip));
|
|
84
|
+
ctx.printResult({
|
|
85
|
+
data: {
|
|
86
|
+
dir: result.dir,
|
|
87
|
+
files: result.files,
|
|
88
|
+
sources: result.exported,
|
|
89
|
+
...(result.skipped.length > 0 ? { skipped: result.skipped } : {}),
|
|
90
|
+
},
|
|
91
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
92
|
+
}, {
|
|
68
93
|
text: (data) => `Exported ${data.files} notes from ${data.sources.join(", ")} to ${data.dir}.`,
|
|
69
94
|
});
|
|
70
95
|
return;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from "../../../utils/errors";
|
|
1
2
|
import type { NoteFileEntry, QuickNoteEntry } from "./files";
|
|
2
3
|
import type { NotesStore } from "./store";
|
|
3
4
|
import { noteOwnerKey, splitNoteKey } from "./store";
|
|
@@ -7,9 +8,23 @@ export interface NotesExportSource {
|
|
|
7
8
|
store: NotesStore;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* What an export left out, and why: a whole source whose notes could not be
|
|
13
|
+
* listed, or with `note`, one note of that source that could not be read.
|
|
14
|
+
*/
|
|
15
|
+
export interface NotesExportSkip {
|
|
16
|
+
label: string;
|
|
17
|
+
/** The note's symbol, or a quick note's title. */
|
|
18
|
+
note?: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
export interface NotesExportResult {
|
|
11
23
|
dir: string;
|
|
12
24
|
files: number;
|
|
25
|
+
/** The sources whose notes were written, in the order given. */
|
|
26
|
+
exported: string[];
|
|
27
|
+
skipped: NotesExportSkip[];
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
function safeFileName(value: string): string {
|
|
@@ -29,16 +44,27 @@ export async function exportNotesToDirectory(
|
|
|
29
44
|
const fsModulePath = "fs/promises";
|
|
30
45
|
const { mkdir, writeFile } = await import(fsModulePath) as typeof import("fs/promises");
|
|
31
46
|
let files = 0;
|
|
47
|
+
const exported: string[] = [];
|
|
48
|
+
const skipped: NotesExportResult["skipped"] = [];
|
|
32
49
|
for (const source of sources) {
|
|
33
50
|
const folder = sources.length > 1 ? `${dir}/${safeFileName(source.label)}` : dir;
|
|
34
51
|
let entries: NoteFileEntry[];
|
|
35
52
|
let quick: QuickNoteEntry[];
|
|
53
|
+
const unreadable: Array<{ key: string; error: unknown }> = [];
|
|
36
54
|
try {
|
|
37
|
-
[entries, quick] = await Promise.all([
|
|
38
|
-
|
|
55
|
+
[entries, quick] = await Promise.all([
|
|
56
|
+
source.store.list({ strict: true, skipped: (key, error) => unreadable.push({ key, error }) }),
|
|
57
|
+
source.store.loadQuickNotesIndex(),
|
|
58
|
+
]);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
skipped.push({ label: source.label, reason: errorMessage(error) });
|
|
39
61
|
continue;
|
|
40
62
|
}
|
|
41
63
|
const titles = new Map(quick.map((entry) => [entry.id, entry.title]));
|
|
64
|
+
for (const { key: noteKey, error } of unreadable) {
|
|
65
|
+
const { kind, key } = splitNoteKey(noteKey);
|
|
66
|
+
skipped.push({ label: source.label, note: kind === "quick" ? (titles.get(key) ?? key) : key, reason: errorMessage(error) });
|
|
67
|
+
}
|
|
42
68
|
for (const entry of entries) {
|
|
43
69
|
const { kind, key } = splitNoteKey(entry.key);
|
|
44
70
|
const path = kind === "quick"
|
|
@@ -48,8 +74,28 @@ export async function exportNotesToDirectory(
|
|
|
48
74
|
await writeFile(path, entry.text, "utf-8");
|
|
49
75
|
files += 1;
|
|
50
76
|
}
|
|
77
|
+
exported.push(source.label);
|
|
51
78
|
}
|
|
52
|
-
return { dir, files };
|
|
79
|
+
return { dir, files, exported, skipped };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One line per skip, as `notes export` warns and the Export Notes command reports. */
|
|
83
|
+
export function describeExportSkip(skip: NotesExportSkip): string {
|
|
84
|
+
return skip.note === undefined
|
|
85
|
+
? `Skipped ${skip.label}: its notes could not be read (${skip.reason}).`
|
|
86
|
+
: `Skipped the ${skip.label} note ${skip.note}: it could not be read (${skip.reason}).`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The Export Notes command's notice: the count, then what was skipped. */
|
|
90
|
+
export function exportNotice(result: NotesExportResult): { body: string; type: "success" | "error" } {
|
|
91
|
+
const exported = `Exported ${result.files} ${result.files === 1 ? "note" : "notes"} to ${result.dir}`;
|
|
92
|
+
if (result.skipped.length === 0) return { body: exported, type: "success" };
|
|
93
|
+
const shown = result.skipped.slice(0, 3).map(describeExportSkip);
|
|
94
|
+
const more = result.skipped.length - shown.length;
|
|
95
|
+
return {
|
|
96
|
+
body: [`${exported}.`, ...shown, ...(more > 0 ? [`${more} more could not be read.`] : [])].join(" "),
|
|
97
|
+
type: "error",
|
|
98
|
+
};
|
|
53
99
|
}
|
|
54
100
|
|
|
55
101
|
export function exportSourceLabel(store: NotesStore, teamName?: string): string {
|
|
@@ -35,6 +35,13 @@ export interface NotesFilesIO {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const QUICK_NOTES_INDEX_FILE = "__quick-notes-index__.json";
|
|
38
|
+
|
|
39
|
+
export interface NotesListOptions {
|
|
40
|
+
/** A notes folder that cannot be listed rejects instead of listing nothing. */
|
|
41
|
+
strict?: boolean;
|
|
42
|
+
/** Hears each note left out because it could not be read, for a caller that says so. */
|
|
43
|
+
skipped?: (key: string, error: unknown) => void;
|
|
44
|
+
}
|
|
38
45
|
const LOCAL_KEY_PREFIX = "gloomberb:notes:";
|
|
39
46
|
const LOCAL_TIMESTAMP_KEY = "gloomberb:notes:__updated-at__";
|
|
40
47
|
|
|
@@ -237,8 +244,8 @@ export class NotesFiles implements NotesStore {
|
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
/** Every stored note, ticker notes and quick notes alike, newest first. */
|
|
240
|
-
async list(): Promise<NoteFileEntry[]> {
|
|
241
|
-
const { entries } = await this.readEach(await this.io.listKeys());
|
|
247
|
+
async list(options: NotesListOptions = {}): Promise<NoteFileEntry[]> {
|
|
248
|
+
const { entries } = await this.readEach(await this.io.listKeys({ strict: options.strict }), options.skipped);
|
|
242
249
|
return entries.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
243
250
|
}
|
|
244
251
|
|
|
@@ -277,7 +284,10 @@ export class NotesFiles implements NotesStore {
|
|
|
277
284
|
}
|
|
278
285
|
|
|
279
286
|
/** An unreadable note is left out and counted rather than synced as empty. */
|
|
280
|
-
private async readEach(
|
|
287
|
+
private async readEach(
|
|
288
|
+
keys: Array<[string, number]>,
|
|
289
|
+
skipped?: NotesListOptions["skipped"],
|
|
290
|
+
): Promise<{ entries: NoteFileEntry[]; unreadable: number }> {
|
|
281
291
|
const entries: NoteFileEntry[] = [];
|
|
282
292
|
let unreadable = 0;
|
|
283
293
|
for (const [key, updatedAt] of keys) {
|
|
@@ -286,6 +296,7 @@ export class NotesFiles implements NotesStore {
|
|
|
286
296
|
} catch (error) {
|
|
287
297
|
unreadable += 1;
|
|
288
298
|
logFailure("notes.files", "Read a note while listing notes failed")(error);
|
|
299
|
+
skipped?.(key, error);
|
|
289
300
|
}
|
|
290
301
|
}
|
|
291
302
|
return { entries, unreadable };
|
|
@@ -2,7 +2,7 @@ import { apiClient } from "../../../api-client";
|
|
|
2
2
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
3
3
|
import { teamStore } from "../../../team/store";
|
|
4
4
|
import { notesCliCommand } from "./cli";
|
|
5
|
-
import { exportNotesToDirectory, exportSourceLabel } from "./export";
|
|
5
|
+
import { exportNotesToDirectory, exportNotice, exportSourceLabel } from "./export";
|
|
6
6
|
import { NotesFiles } from "./files";
|
|
7
7
|
import { migrateLocalNotes, notesMigratedAt } from "./migration";
|
|
8
8
|
import { createQuickNotesPane } from "./quick-notes-pane";
|
|
@@ -114,8 +114,8 @@ export const notesPlugin: GloomPlugin = {
|
|
|
114
114
|
...teams.map((team) => ({ label: exportSourceLabel(registry.forOwner({ kind: "team", teamId: team.id }), team.name), store: registry.forOwner({ kind: "team", teamId: team.id }) })),
|
|
115
115
|
];
|
|
116
116
|
try {
|
|
117
|
-
|
|
118
|
-
ctx.notify(
|
|
117
|
+
// What was skipped is said with the count, as `notes export` warns.
|
|
118
|
+
ctx.notify(exportNotice(await exportNotesToDirectory(dir, sources)));
|
|
119
119
|
} catch (error) {
|
|
120
120
|
ctx.notify({ body: error instanceof Error ? error.message : "Could not export notes.", type: "error" });
|
|
121
121
|
}
|