gloomberb 0.9.7 → 0.9.9
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/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { scheduleConfigSave } from "../state/config-save-scheduler";
|
|
2
|
+
import type { AppConfig, BrokerInstanceConfig } from "../types/config";
|
|
3
|
+
import type { PricePoint, TickerFinancials } from "../types/financials";
|
|
4
|
+
import type { Portfolio, TickerMetadata, TickerPosition, TickerRecord, Watchlist } from "../types/ticker";
|
|
5
|
+
import { hydrateTickerMetadata } from "../tickers/metadata";
|
|
6
|
+
import type { SyncContributor } from "./types";
|
|
7
|
+
import { convertCurrency } from "../utils/format";
|
|
8
|
+
import {
|
|
9
|
+
computeDatedBeta,
|
|
10
|
+
computeDatedReturns,
|
|
11
|
+
computeWeightedPortfolioReturns,
|
|
12
|
+
type WeightedReturnSeries,
|
|
13
|
+
} from "../plugins/builtin/analytics/metrics";
|
|
14
|
+
import { getSyncedProfileAnalytics } from "./profile-analytics";
|
|
15
|
+
|
|
16
|
+
const SENSITIVE_KEY_PATTERN = /(token|secret|password|credential|private|api[_-]?key|access[_-]?key|refresh[_-]?key|session|cookie|dataDir|path|directory|localPath)/i;
|
|
17
|
+
|
|
18
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sanitizeUnknown(value: unknown): unknown {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return value.map(sanitizeUnknown).filter((entry) => entry !== undefined);
|
|
25
|
+
}
|
|
26
|
+
if (!isPlainObject(value)) return value;
|
|
27
|
+
|
|
28
|
+
const output: Record<string, unknown> = {};
|
|
29
|
+
for (const [key, child] of Object.entries(value)) {
|
|
30
|
+
if (SENSITIVE_KEY_PATTERN.test(key)) continue;
|
|
31
|
+
const sanitized = sanitizeUnknown(child);
|
|
32
|
+
if (sanitized !== undefined) output[key] = sanitized;
|
|
33
|
+
}
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sanitizePortfolio(portfolio: Portfolio): Portfolio {
|
|
38
|
+
return {
|
|
39
|
+
id: portfolio.id,
|
|
40
|
+
name: portfolio.name,
|
|
41
|
+
description: portfolio.description,
|
|
42
|
+
currency: portfolio.currency,
|
|
43
|
+
brokerId: portfolio.brokerId,
|
|
44
|
+
lastSyncedAt: portfolio.lastSyncedAt,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function sanitizeWatchlist(watchlist: Watchlist): Watchlist {
|
|
49
|
+
return {
|
|
50
|
+
id: watchlist.id,
|
|
51
|
+
name: watchlist.name,
|
|
52
|
+
description: watchlist.description,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sanitizeBrokerInstance(instance: BrokerInstanceConfig): Omit<BrokerInstanceConfig, "config"> {
|
|
57
|
+
return {
|
|
58
|
+
id: instance.id,
|
|
59
|
+
brokerType: instance.brokerType,
|
|
60
|
+
label: instance.label,
|
|
61
|
+
connectionMode: instance.connectionMode,
|
|
62
|
+
enabled: instance.enabled,
|
|
63
|
+
lastSyncedAt: instance.lastSyncedAt,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sanitizePosition(position: TickerPosition): TickerPosition {
|
|
68
|
+
return {
|
|
69
|
+
portfolio: position.portfolio,
|
|
70
|
+
shares: position.shares,
|
|
71
|
+
avgCost: position.avgCost,
|
|
72
|
+
currency: position.currency,
|
|
73
|
+
dateAcquired: position.dateAcquired,
|
|
74
|
+
broker: position.broker,
|
|
75
|
+
side: position.side,
|
|
76
|
+
marketValue: position.marketValue,
|
|
77
|
+
unrealizedPnl: position.unrealizedPnl,
|
|
78
|
+
multiplier: position.multiplier,
|
|
79
|
+
markPrice: position.markPrice,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pricePointTime(point: { date: Date | string | number }): number {
|
|
84
|
+
const value = point.date;
|
|
85
|
+
if (value instanceof Date) return value.getTime();
|
|
86
|
+
return new Date(value).getTime();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function weeklyQuoteMove(financials: TickerFinancials | null | undefined) {
|
|
90
|
+
const quote = financials?.quote;
|
|
91
|
+
if (!quote || !Number.isFinite(quote.price) || quote.price <= 0) return undefined;
|
|
92
|
+
const history = (financials?.priceHistory ?? [])
|
|
93
|
+
.filter((point) => Number.isFinite(point.close) && point.close > 0 && Number.isFinite(pricePointTime(point)))
|
|
94
|
+
.sort((left, right) => pricePointTime(left) - pricePointTime(right));
|
|
95
|
+
if (history.length === 0) return undefined;
|
|
96
|
+
const latestTime = Number.isFinite(quote.lastUpdated)
|
|
97
|
+
? quote.lastUpdated
|
|
98
|
+
: pricePointTime(history[history.length - 1]!);
|
|
99
|
+
const cutoff = latestTime - 7 * 24 * 60 * 60 * 1000;
|
|
100
|
+
const reference = history.find((point) => pricePointTime(point) >= cutoff) ?? history[Math.max(0, history.length - 7)];
|
|
101
|
+
if (!reference || !Number.isFinite(reference.close) || reference.close <= 0) return undefined;
|
|
102
|
+
const weekChange = quote.price - reference.close;
|
|
103
|
+
return {
|
|
104
|
+
weekReferencePrice: reference.close,
|
|
105
|
+
weekChange,
|
|
106
|
+
weekChangePercent: (weekChange / reference.close) * 100,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sanitizeQuote(financials: TickerFinancials | null | undefined) {
|
|
111
|
+
const quote = financials?.quote;
|
|
112
|
+
if (!quote) return undefined;
|
|
113
|
+
const week = weeklyQuoteMove(financials);
|
|
114
|
+
return {
|
|
115
|
+
price: Number.isFinite(quote.price) ? quote.price : undefined,
|
|
116
|
+
currency: quote.currency,
|
|
117
|
+
changePercent: Number.isFinite(quote.changePercent) ? quote.changePercent : undefined,
|
|
118
|
+
previousClose: Number.isFinite(quote.previousClose) ? quote.previousClose : undefined,
|
|
119
|
+
weekReferencePrice: week?.weekReferencePrice,
|
|
120
|
+
weekChange: week?.weekChange,
|
|
121
|
+
weekChangePercent: week?.weekChangePercent,
|
|
122
|
+
lastUpdated: quote.lastUpdated,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sanitizeTickerMetadata(metadata: TickerMetadata, financials?: TickerFinancials | null): Omit<TickerMetadata, "broker_contracts"> & { quote?: ReturnType<typeof sanitizeQuote> } {
|
|
127
|
+
return {
|
|
128
|
+
ticker: metadata.ticker,
|
|
129
|
+
exchange: metadata.exchange,
|
|
130
|
+
currency: metadata.currency,
|
|
131
|
+
name: metadata.name,
|
|
132
|
+
sector: metadata.sector,
|
|
133
|
+
industry: metadata.industry,
|
|
134
|
+
assetCategory: metadata.assetCategory,
|
|
135
|
+
isin: metadata.isin,
|
|
136
|
+
cusip: metadata.cusip,
|
|
137
|
+
portfolios: [...metadata.portfolios],
|
|
138
|
+
watchlists: [...metadata.watchlists],
|
|
139
|
+
positions: metadata.positions.map(sanitizePosition),
|
|
140
|
+
custom: (sanitizeUnknown(metadata.custom) as Record<string, unknown>) ?? {},
|
|
141
|
+
tags: [...metadata.tags],
|
|
142
|
+
quote: sanitizeQuote(financials),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function collectCoreConfigPayload(config: AppConfig) {
|
|
147
|
+
return sanitizeUnknown({
|
|
148
|
+
configVersion: config.configVersion,
|
|
149
|
+
baseCurrency: config.baseCurrency,
|
|
150
|
+
refreshIntervalMinutes: config.refreshIntervalMinutes,
|
|
151
|
+
portfolios: config.portfolios.map(sanitizePortfolio),
|
|
152
|
+
watchlists: config.watchlists.map(sanitizeWatchlist),
|
|
153
|
+
layout: config.layout,
|
|
154
|
+
layouts: config.layouts,
|
|
155
|
+
activeLayoutIndex: config.activeLayoutIndex,
|
|
156
|
+
brokerInstances: config.brokerInstances.map(sanitizeBrokerInstance),
|
|
157
|
+
disabledPlugins: config.disabledPlugins,
|
|
158
|
+
disabledSources: config.disabledSources,
|
|
159
|
+
pluginConfig: config.pluginConfig,
|
|
160
|
+
theme: config.theme,
|
|
161
|
+
chartPreferences: config.chartPreferences,
|
|
162
|
+
valueFlashingEnabled: config.valueFlashingEnabled,
|
|
163
|
+
recentTickers: config.recentTickers,
|
|
164
|
+
onboardingComplete: config.onboardingComplete,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function positionValueBase(
|
|
169
|
+
position: TickerPosition,
|
|
170
|
+
financials: TickerFinancials | null | undefined,
|
|
171
|
+
baseCurrency: string,
|
|
172
|
+
exchangeRates: Map<string, number>,
|
|
173
|
+
): number | null {
|
|
174
|
+
const quoteCurrency = financials?.quote?.currency || position.currency || baseCurrency;
|
|
175
|
+
const positionCurrency = position.currency || quoteCurrency;
|
|
176
|
+
if (typeof position.marketValue === "number" && Number.isFinite(position.marketValue)) {
|
|
177
|
+
return convertCurrency(Math.abs(position.marketValue), positionCurrency, baseCurrency, exchangeRates);
|
|
178
|
+
}
|
|
179
|
+
const hasMarkPrice = typeof position.markPrice === "number" && Number.isFinite(position.markPrice);
|
|
180
|
+
const hasQuotePrice = typeof financials?.quote?.price === "number" && Number.isFinite(financials.quote.price);
|
|
181
|
+
const price = hasMarkPrice ? position.markPrice : hasQuotePrice ? financials!.quote!.price : position.avgCost;
|
|
182
|
+
if (!Number.isFinite(position.shares) || typeof price !== "number" || !Number.isFinite(price)) return null;
|
|
183
|
+
return convertCurrency(
|
|
184
|
+
Math.abs(position.shares * price * (position.multiplier ?? 1)),
|
|
185
|
+
hasQuotePrice && !hasMarkPrice ? quoteCurrency : positionCurrency,
|
|
186
|
+
baseCurrency,
|
|
187
|
+
exchangeRates,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pricePointTimeOrNull(point: PricePoint): number | null {
|
|
192
|
+
const value = point.date;
|
|
193
|
+
if (value == null) return null;
|
|
194
|
+
const time = value instanceof Date ? value.getTime() : new Date(value).getTime();
|
|
195
|
+
return Number.isFinite(time) ? time : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function recentPriceHistory(history: PricePoint[], days: number): PricePoint[] {
|
|
199
|
+
let latestTime = 0;
|
|
200
|
+
for (const point of history) {
|
|
201
|
+
const time = pricePointTimeOrNull(point);
|
|
202
|
+
if (time != null && time > latestTime) latestTime = time;
|
|
203
|
+
}
|
|
204
|
+
if (latestTime <= 0) return [];
|
|
205
|
+
const cutoff = latestTime - days * 24 * 60 * 60 * 1000;
|
|
206
|
+
return history.filter((point) => {
|
|
207
|
+
const time = pricePointTimeOrNull(point);
|
|
208
|
+
return time != null && time >= cutoff;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function oneYearReturnFromSeries(series: WeightedReturnSeries[]): number | null {
|
|
213
|
+
const returns = computeWeightedPortfolioReturns(series);
|
|
214
|
+
if (returns.length === 0) return null;
|
|
215
|
+
const cumulative = returns.reduce((acc, point) => (
|
|
216
|
+
Number.isFinite(point.value) ? acc * (1 + point.value) : acc
|
|
217
|
+
), 1) - 1;
|
|
218
|
+
return Number.isFinite(cumulative) ? cumulative : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function collectAnalyticsByPortfolio(
|
|
222
|
+
config: AppConfig,
|
|
223
|
+
tickers: Map<string, TickerRecord>,
|
|
224
|
+
financials: Map<string, TickerFinancials>,
|
|
225
|
+
exchangeRates: Map<string, number>,
|
|
226
|
+
) {
|
|
227
|
+
const output: Record<string, Record<string, unknown>> = {};
|
|
228
|
+
const spyReturns = computeDatedReturns(recentPriceHistory(financials.get("SPY")?.priceHistory ?? [], 366));
|
|
229
|
+
for (const portfolio of config.portfolios) {
|
|
230
|
+
let returnWeight = 0;
|
|
231
|
+
let weightedReturn = 0;
|
|
232
|
+
const datedReturnSeries: WeightedReturnSeries[] = [];
|
|
233
|
+
for (const ticker of tickers.values()) {
|
|
234
|
+
const tickerFinancials = financials.get(ticker.metadata.ticker);
|
|
235
|
+
const portfolioPositions = ticker.metadata.positions.filter((position) => position.portfolio === portfolio.id);
|
|
236
|
+
if (portfolioPositions.length === 0 && !ticker.metadata.portfolios.includes(portfolio.id)) continue;
|
|
237
|
+
for (const position of portfolioPositions) {
|
|
238
|
+
const value = positionValueBase(position, tickerFinancials, config.baseCurrency, exchangeRates);
|
|
239
|
+
if (value != null) {
|
|
240
|
+
const return1Y = tickerFinancials?.fundamentals?.return1Y;
|
|
241
|
+
if (typeof return1Y === "number" && Number.isFinite(return1Y)) {
|
|
242
|
+
returnWeight += value;
|
|
243
|
+
weightedReturn += return1Y * value;
|
|
244
|
+
}
|
|
245
|
+
const returns = computeDatedReturns(recentPriceHistory(tickerFinancials?.priceHistory ?? [], 366));
|
|
246
|
+
if (returns.length >= 10) datedReturnSeries.push({ weight: value, returns });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const portfolioReturns = computeWeightedPortfolioReturns(datedReturnSeries);
|
|
251
|
+
const previewAnalytics = getSyncedProfileAnalytics(portfolio.id);
|
|
252
|
+
output[portfolio.id] = {
|
|
253
|
+
oneYearReturn: previewAnalytics?.oneYearReturn
|
|
254
|
+
?? (returnWeight > 0 ? weightedReturn / returnWeight : oneYearReturnFromSeries(datedReturnSeries)),
|
|
255
|
+
spyBeta: previewAnalytics?.spyBeta
|
|
256
|
+
?? (
|
|
257
|
+
portfolioReturns.length > 0 && spyReturns.length > 0
|
|
258
|
+
? computeDatedBeta(portfolioReturns, spyReturns)
|
|
259
|
+
: null
|
|
260
|
+
),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return output;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function collectCoreCollectionsPayload(
|
|
267
|
+
config: AppConfig,
|
|
268
|
+
tickers: Map<string, TickerRecord>,
|
|
269
|
+
financials: Map<string, TickerFinancials>,
|
|
270
|
+
exchangeRates: Map<string, number>,
|
|
271
|
+
) {
|
|
272
|
+
const records = [...tickers.values()]
|
|
273
|
+
.map((ticker) => sanitizeTickerMetadata(ticker.metadata, financials.get(ticker.metadata.ticker)))
|
|
274
|
+
.filter((metadata) => (
|
|
275
|
+
metadata.portfolios.length > 0 ||
|
|
276
|
+
metadata.watchlists.length > 0 ||
|
|
277
|
+
metadata.positions.length > 0
|
|
278
|
+
));
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
baseCurrency: config.baseCurrency,
|
|
282
|
+
exchangeRates: Object.fromEntries(
|
|
283
|
+
[...exchangeRates.entries()]
|
|
284
|
+
.filter(([currency, rate]) => (
|
|
285
|
+
typeof currency === "string" &&
|
|
286
|
+
Number.isFinite(rate) &&
|
|
287
|
+
rate > 0
|
|
288
|
+
))
|
|
289
|
+
.map(([currency, rate]) => [currency.trim().toUpperCase(), rate]),
|
|
290
|
+
),
|
|
291
|
+
portfolios: config.portfolios.map(sanitizePortfolio),
|
|
292
|
+
watchlists: config.watchlists.map(sanitizeWatchlist),
|
|
293
|
+
analyticsByPortfolio: collectAnalyticsByPortfolio(config, tickers, financials, exchangeRates),
|
|
294
|
+
tickers: records,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function mergeConfigPayload(config: AppConfig, payload: unknown): AppConfig | null {
|
|
299
|
+
if (!isPlainObject(payload)) return null;
|
|
300
|
+
const next: AppConfig = { ...config };
|
|
301
|
+
const assign = <K extends keyof AppConfig>(key: K) => {
|
|
302
|
+
if (key in payload) {
|
|
303
|
+
next[key] = payload[key as string] as AppConfig[K];
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
assign("baseCurrency");
|
|
308
|
+
assign("refreshIntervalMinutes");
|
|
309
|
+
assign("layout");
|
|
310
|
+
assign("layouts");
|
|
311
|
+
assign("activeLayoutIndex");
|
|
312
|
+
assign("disabledPlugins");
|
|
313
|
+
assign("disabledSources");
|
|
314
|
+
assign("pluginConfig");
|
|
315
|
+
assign("theme");
|
|
316
|
+
assign("chartPreferences");
|
|
317
|
+
assign("valueFlashingEnabled");
|
|
318
|
+
assign("recentTickers");
|
|
319
|
+
assign("onboardingComplete");
|
|
320
|
+
|
|
321
|
+
if (Array.isArray(payload.portfolios)) next.portfolios = payload.portfolios as Portfolio[];
|
|
322
|
+
if (Array.isArray(payload.watchlists)) next.watchlists = payload.watchlists as Watchlist[];
|
|
323
|
+
if (Array.isArray(payload.brokerInstances)) {
|
|
324
|
+
const incoming = payload.brokerInstances as Array<Partial<BrokerInstanceConfig>>;
|
|
325
|
+
const existingById = new Map(config.brokerInstances.map((instance) => [instance.id, instance]));
|
|
326
|
+
next.brokerInstances = incoming.map((instance) => {
|
|
327
|
+
const current = instance.id ? existingById.get(instance.id) : undefined;
|
|
328
|
+
return {
|
|
329
|
+
id: instance.id ?? current?.id ?? crypto.randomUUID(),
|
|
330
|
+
brokerType: instance.brokerType ?? current?.brokerType ?? "",
|
|
331
|
+
label: instance.label ?? current?.label ?? "",
|
|
332
|
+
connectionMode: instance.connectionMode ?? current?.connectionMode,
|
|
333
|
+
enabled: instance.enabled ?? current?.enabled,
|
|
334
|
+
lastSyncedAt: instance.lastSyncedAt ?? current?.lastSyncedAt,
|
|
335
|
+
config: current?.config ?? {},
|
|
336
|
+
};
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return next;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export const coreConfigSyncContributor: SyncContributor = {
|
|
343
|
+
id: "core.config",
|
|
344
|
+
schemaVersion: 1,
|
|
345
|
+
collect: ({ state }) => collectCoreConfigPayload(state.config),
|
|
346
|
+
apply: (payload, { state, dispatch }) => {
|
|
347
|
+
const nextConfig = mergeConfigPayload(state.config, payload);
|
|
348
|
+
if (!nextConfig) return;
|
|
349
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
350
|
+
scheduleConfigSave(nextConfig);
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
export const coreCollectionsSyncContributor: SyncContributor = {
|
|
355
|
+
id: "core.collections",
|
|
356
|
+
schemaVersion: 1,
|
|
357
|
+
collect: ({ state }) => collectCoreCollectionsPayload(
|
|
358
|
+
state.config,
|
|
359
|
+
state.tickers,
|
|
360
|
+
state.financials,
|
|
361
|
+
state.exchangeRates,
|
|
362
|
+
),
|
|
363
|
+
apply: async (payload, { state, dispatch, tickerRepository }) => {
|
|
364
|
+
if (!isPlainObject(payload)) return;
|
|
365
|
+
const nextConfig = { ...state.config };
|
|
366
|
+
if (Array.isArray(payload.portfolios)) nextConfig.portfolios = payload.portfolios as Portfolio[];
|
|
367
|
+
if (Array.isArray(payload.watchlists)) nextConfig.watchlists = payload.watchlists as Watchlist[];
|
|
368
|
+
|
|
369
|
+
const nextTickers = new Map(state.tickers);
|
|
370
|
+
const rawTickers = Array.isArray(payload.tickers) ? payload.tickers : [];
|
|
371
|
+
for (const rawTicker of rawTickers) {
|
|
372
|
+
if (!isPlainObject(rawTicker)) continue;
|
|
373
|
+
const current = typeof rawTicker.ticker === "string"
|
|
374
|
+
? nextTickers.get(rawTicker.ticker)
|
|
375
|
+
: null;
|
|
376
|
+
const metadata = hydrateTickerMetadata({
|
|
377
|
+
...current?.metadata,
|
|
378
|
+
...rawTicker,
|
|
379
|
+
broker_contracts: current?.metadata.broker_contracts ?? [],
|
|
380
|
+
});
|
|
381
|
+
const record: TickerRecord = { metadata };
|
|
382
|
+
await tickerRepository.saveTicker(record);
|
|
383
|
+
nextTickers.set(metadata.ticker, record);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
387
|
+
dispatch({ type: "SET_TICKERS", tickers: nextTickers });
|
|
388
|
+
scheduleConfigSave(nextConfig);
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
export function createCoreSyncContributors(): SyncContributor[] {
|
|
393
|
+
return [coreConfigSyncContributor, coreCollectionsSyncContributor];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export const __syncContributorInternalsForTests = {
|
|
397
|
+
sanitizeUnknown,
|
|
398
|
+
collectCoreConfigPayload,
|
|
399
|
+
collectCoreCollectionsPayload,
|
|
400
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PublicPortfolioAnalytics } from "../api-client";
|
|
2
|
+
|
|
3
|
+
const analyticsByPortfolio = new Map<string, PublicPortfolioAnalytics>();
|
|
4
|
+
|
|
5
|
+
function finiteMetric(value: number | null | undefined): number | null {
|
|
6
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeAnalytics(analytics: PublicPortfolioAnalytics | null | undefined): PublicPortfolioAnalytics | null {
|
|
10
|
+
if (!analytics) return null;
|
|
11
|
+
const normalized = {
|
|
12
|
+
oneYearReturn: finiteMetric(analytics.oneYearReturn),
|
|
13
|
+
spyBeta: finiteMetric(analytics.spyBeta),
|
|
14
|
+
};
|
|
15
|
+
return normalized.oneYearReturn != null || normalized.spyBeta != null ? normalized : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sameAnalytics(left: PublicPortfolioAnalytics | null, right: PublicPortfolioAnalytics | null): boolean {
|
|
19
|
+
return left?.oneYearReturn === right?.oneYearReturn && left?.spyBeta === right?.spyBeta;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setSyncedProfileAnalytics(
|
|
23
|
+
portfolioId: string,
|
|
24
|
+
analytics: PublicPortfolioAnalytics | null | undefined,
|
|
25
|
+
): boolean {
|
|
26
|
+
const normalized = normalizeAnalytics(analytics);
|
|
27
|
+
const current = analyticsByPortfolio.get(portfolioId) ?? null;
|
|
28
|
+
if (sameAnalytics(current, normalized)) return false;
|
|
29
|
+
if (normalized) {
|
|
30
|
+
analyticsByPortfolio.set(portfolioId, normalized);
|
|
31
|
+
} else {
|
|
32
|
+
analyticsByPortfolio.delete(portfolioId);
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getSyncedProfileAnalytics(portfolioId: string): PublicPortfolioAnalytics | null {
|
|
38
|
+
return analyticsByPortfolio.get(portfolioId) ?? null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore, type Dispatch } from "react";
|
|
2
|
+
import type { AppAction, AppState } from "../core/state/app/state";
|
|
3
|
+
import type { TickerRepository } from "../data/ticker-repository";
|
|
4
|
+
import type { PluginRegistry } from "../plugins/registry";
|
|
5
|
+
import { cloudSyncController } from "./controller";
|
|
6
|
+
|
|
7
|
+
interface CloudSyncRuntimeOptions {
|
|
8
|
+
state: AppState;
|
|
9
|
+
dispatch: Dispatch<AppAction>;
|
|
10
|
+
tickerRepository: TickerRepository;
|
|
11
|
+
pluginRegistry: PluginRegistry;
|
|
12
|
+
initialized: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useCloudSyncRuntime({
|
|
16
|
+
state,
|
|
17
|
+
dispatch,
|
|
18
|
+
tickerRepository,
|
|
19
|
+
pluginRegistry,
|
|
20
|
+
initialized,
|
|
21
|
+
}: CloudSyncRuntimeOptions): void {
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
cloudSyncController.setRuntime({
|
|
24
|
+
state,
|
|
25
|
+
dispatch,
|
|
26
|
+
tickerRepository,
|
|
27
|
+
getContributors: () => pluginRegistry.getEnabledSyncContributors(),
|
|
28
|
+
getTransport: () => pluginRegistry.getActiveSyncTransport(),
|
|
29
|
+
});
|
|
30
|
+
}, [dispatch, pluginRegistry, state, tickerRepository]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => () => {
|
|
33
|
+
cloudSyncController.clearRuntime();
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!initialized) return;
|
|
38
|
+
void cloudSyncController.pullLatest();
|
|
39
|
+
}, [initialized, pluginRegistry]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!initialized) return;
|
|
43
|
+
cloudSyncController.schedulePush("state-change");
|
|
44
|
+
}, [initialized, state.config, state.tickers]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useCloudSyncStatus() {
|
|
48
|
+
return useSyncExternalStore(
|
|
49
|
+
(listener) => cloudSyncController.subscribe(listener),
|
|
50
|
+
() => cloudSyncController.getStatus(),
|
|
51
|
+
() => cloudSyncController.getStatus(),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Dispatch } from "react";
|
|
2
|
+
import type { AppAction, AppState } from "../core/state/app/state";
|
|
3
|
+
import type { TickerRepository } from "../data/ticker-repository";
|
|
4
|
+
|
|
5
|
+
export const SYNC_SNAPSHOT_SCHEMA_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export interface SyncContributorPayload {
|
|
8
|
+
schemaVersion: number;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
payload: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SyncSnapshot {
|
|
14
|
+
schemaVersion: typeof SYNC_SNAPSHOT_SCHEMA_VERSION;
|
|
15
|
+
appId: "gloomberb";
|
|
16
|
+
clientId: string;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
appVersion?: string;
|
|
19
|
+
contributors: Record<string, SyncContributorPayload>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SyncCollectContext {
|
|
23
|
+
state: AppState;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SyncApplyContext {
|
|
27
|
+
snapshot: SyncSnapshot;
|
|
28
|
+
state: AppState;
|
|
29
|
+
dispatch: Dispatch<AppAction>;
|
|
30
|
+
tickerRepository: TickerRepository;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SyncContributor {
|
|
34
|
+
id: string;
|
|
35
|
+
schemaVersion: number;
|
|
36
|
+
collect(context: SyncCollectContext): unknown | Promise<unknown>;
|
|
37
|
+
apply?(payload: unknown, context: SyncApplyContext): void | Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SyncSnapshotResponse {
|
|
41
|
+
snapshot: SyncSnapshot | null;
|
|
42
|
+
revision: number | null;
|
|
43
|
+
updatedAt: string | null;
|
|
44
|
+
settings?: SyncSettings;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SyncPushResult {
|
|
48
|
+
revision: number;
|
|
49
|
+
updatedAt: string;
|
|
50
|
+
settings?: SyncSettings;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SyncTransport {
|
|
54
|
+
id: string;
|
|
55
|
+
isAvailable(): boolean;
|
|
56
|
+
pullSnapshot(): Promise<SyncSnapshotResponse>;
|
|
57
|
+
pushSnapshot(snapshot: SyncSnapshot, options?: { baseRevision?: number | null }): Promise<SyncPushResult>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SyncSettings {
|
|
61
|
+
syncEnabled: boolean;
|
|
62
|
+
weeklyRoundupEnabled: boolean;
|
|
63
|
+
positionAlertsEnabled: boolean;
|
|
64
|
+
selectedSharedPortfolioId?: string | null;
|
|
65
|
+
lastSyncAt?: string | null;
|
|
66
|
+
lastRoundupEmailAt?: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RegisteredSyncContributor {
|
|
70
|
+
pluginId: string;
|
|
71
|
+
contributor: SyncContributor;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface RegisteredSyncTransport {
|
|
75
|
+
pluginId: string;
|
|
76
|
+
transport: SyncTransport;
|
|
77
|
+
}
|
|
@@ -14,7 +14,6 @@ export function createTestDataProvider(overrides: Partial<DataProvider> = {}): D
|
|
|
14
14
|
getQuote: async () => unused<Quote>("getQuote"),
|
|
15
15
|
getExchangeRate: async () => 1,
|
|
16
16
|
search: async () => [] satisfies InstrumentSearchResult[],
|
|
17
|
-
getNews: async () => [],
|
|
18
17
|
getArticleSummary: async () => null,
|
|
19
18
|
getPriceHistory: async () => [],
|
|
20
19
|
...overrides,
|
package/src/types/plugin.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type { TickerFinancials } from "./financials";
|
|
|
20
20
|
import type { CachePolicy, PersistedResourceValue } from "./persistence";
|
|
21
21
|
import type { TickerRecord } from "./ticker";
|
|
22
22
|
import type { InstrumentSearchResult } from "./instrument";
|
|
23
|
+
import type { SyncContributor, SyncTransport } from "../sync/types";
|
|
23
24
|
|
|
24
25
|
export interface GloomSlots {
|
|
25
26
|
"ticker-research:tab": { ticker: TickerRecord; financials: TickerFinancials | null };
|
|
@@ -467,6 +468,8 @@ export interface GloomPluginContext {
|
|
|
467
468
|
registerShortcut(shortcut: KeyboardShortcut): void;
|
|
468
469
|
registerTickerAction(action: TickerAction): void;
|
|
469
470
|
registerContextMenuProvider(provider: ContextMenuProviderDef): void;
|
|
471
|
+
registerSyncContributor(contributor: SyncContributor): () => void;
|
|
472
|
+
registerSyncTransport(transport: SyncTransport): () => void;
|
|
470
473
|
watchNewsQuery?(
|
|
471
474
|
query: import("./news-source").NewsQuery,
|
|
472
475
|
listener: (state: import("./news-source").NewsQueryState) => void,
|
package/src/ui/host.tsx
CHANGED
|
@@ -85,7 +85,7 @@ export interface SyntaxStyleLike {
|
|
|
85
85
|
export interface BoxRenderable {
|
|
86
86
|
x?: number;
|
|
87
87
|
y?: number;
|
|
88
|
-
width?: number;
|
|
88
|
+
width?: number | string;
|
|
89
89
|
height?: number;
|
|
90
90
|
absoluteX?: number;
|
|
91
91
|
absoluteY?: number;
|
|
@@ -101,6 +101,7 @@ interface ScrollBarRenderable {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export interface ScrollBoxRenderable {
|
|
104
|
+
width?: number;
|
|
104
105
|
scrollTop: number;
|
|
105
106
|
scrollLeft?: number;
|
|
106
107
|
scrollHeight: number;
|
|
@@ -264,6 +265,18 @@ export interface HostTabsProps {
|
|
|
264
265
|
palette: HostTabsPalette;
|
|
265
266
|
}
|
|
266
267
|
|
|
268
|
+
export interface HostCheckboxProps {
|
|
269
|
+
label: string;
|
|
270
|
+
displayLabel?: string;
|
|
271
|
+
checked: boolean;
|
|
272
|
+
onChange?: (checked: boolean) => void;
|
|
273
|
+
disabled?: boolean;
|
|
274
|
+
active?: boolean;
|
|
275
|
+
description?: string;
|
|
276
|
+
width?: number | string;
|
|
277
|
+
variant?: "default" | "desktop";
|
|
278
|
+
}
|
|
279
|
+
|
|
267
280
|
export interface UiHost {
|
|
268
281
|
kind?: "opentui" | "desktop-web";
|
|
269
282
|
capabilities?: {
|
|
@@ -299,6 +312,7 @@ export interface UiHost {
|
|
|
299
312
|
DialogFrame?: ComponentType<any>;
|
|
300
313
|
PageStackView?: ComponentType<any>;
|
|
301
314
|
Tabs?: ComponentType<HostTabsProps>;
|
|
315
|
+
Checkbox?: ComponentType<HostCheckboxProps>;
|
|
302
316
|
DataTable?: ComponentType<any>;
|
|
303
317
|
createSyntaxStyle?(): SyntaxStyleLike;
|
|
304
318
|
colorFromHex?(hex: string): unknown;
|
package/src/ui/index.tsx
CHANGED
package/src/updater.test.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { tmpdir } from "os";
|
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { gzipSync } from "zlib";
|
|
6
6
|
import {
|
|
7
|
-
canSelfUpdate,
|
|
8
7
|
checkForUpdate,
|
|
9
8
|
checkForUpdateDetailed,
|
|
10
9
|
detectUpdateAction,
|
|
@@ -153,26 +152,6 @@ describe("resolveSelfUpdateTargetPath", () => {
|
|
|
153
152
|
});
|
|
154
153
|
});
|
|
155
154
|
|
|
156
|
-
describe("canSelfUpdate", () => {
|
|
157
|
-
test("returns false for manual update releases", () => {
|
|
158
|
-
expect(canSelfUpdate({
|
|
159
|
-
updateAction: { kind: "manual", command: "bun install -g gloomberb@latest" },
|
|
160
|
-
})).toBe(false);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("returns true for self-update releases", () => {
|
|
164
|
-
expect(canSelfUpdate({
|
|
165
|
-
updateAction: { kind: "self" },
|
|
166
|
-
})).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("returns true for desktop update releases", () => {
|
|
170
|
-
expect(canSelfUpdate({
|
|
171
|
-
updateAction: { kind: "desktop" },
|
|
172
|
-
})).toBe(true);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
155
|
describe("update host", () => {
|
|
177
156
|
test("delegates update checks to an installed host", async () => {
|
|
178
157
|
setUpdateHost({
|