gloomberb 0.18.2 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/data.ts +15 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/cli/commands/market.ts +73 -33
- package/src/cli/commands/ticker.ts +82 -25
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/listing-arg.ts +15 -2
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +16 -0
- package/src/cli/pane-functions/dom.ts +2 -1
- package/src/cli/pane-functions/headless.ts +25 -6
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/resolver.ts +19 -5
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/symbol-list.ts +47 -11
- package/src/components/chart/composite/composite-chart.tsx +14 -5
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +11 -0
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/i18n/es.ts +22 -0
- package/src/i18n/ja.ts +22 -0
- package/src/i18n/ko.ts +22 -0
- package/src/i18n/zh-cn.ts +22 -0
- package/src/i18n/zh-tw.ts +22 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
- package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
- package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +10 -0
- package/src/plugins/builtin/equity-screener/model.ts +28 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
- package/src/plugins/builtin/exposure/pane.tsx +10 -9
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +19 -6
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +41 -13
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/research-trading.tsx +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +39 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/sources/gloomberb-cloud/index.ts +7 -0
- package/src/sources/provider-router/index.ts +16 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +30 -0
- package/src/types/data-provider.ts +6 -0
- package/src/types/headless.ts +7 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/sec.ts +68 -0
package/package.json
CHANGED
|
@@ -74,7 +74,7 @@ index cdaa2bca0fabe577cb667056aa6f8f84211189c2..3839e5232742c2467180c344f51a30f1
|
|
|
74
74
|
|
|
75
75
|
// Encrypt function
|
|
76
76
|
diff --git a/dist/api/bun/core/Updater.ts b/dist/api/bun/core/Updater.ts
|
|
77
|
-
index 39daf6d341780af8e30c2c50327162863f5977ec..
|
|
77
|
+
index 39daf6d341780af8e30c2c50327162863f5977ec..0174d2ce1a9a4fa8ae0fd97367293fc5a794286a 100644
|
|
78
78
|
--- a/dist/api/bun/core/Updater.ts
|
|
79
79
|
+++ b/dist/api/bun/core/Updater.ts
|
|
80
80
|
@@ -132,6 +132,19 @@ let updateInfo: {
|
|
@@ -97,7 +97,62 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
|
|
|
97
97
|
function cleanupExtractionFolder(
|
|
98
98
|
extractionFolder: string,
|
|
99
99
|
keepTarHash: string,
|
|
100
|
-
@@ -
|
|
100
|
+
@@ -158,6 +171,54 @@ function cleanupExtractionFolder(
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
+/**
|
|
105
|
+
+ * Where the running app goes while the new one moves in: `<app>.previous`, or,
|
|
106
|
+
+ * when an older one is still there and cannot be removed (a bundle another
|
|
107
|
+
+ * user owns), a name of its own, so a copy left behind once never blocks
|
|
108
|
+
+ * every later update. The app removes the copies it left at its next start.
|
|
109
|
+
+ */
|
|
110
|
+
+function updateAsidePath(appPath: string): string {
|
|
111
|
+
+ const previous = `${appPath}.previous`;
|
|
112
|
+
+ if (!statSync(previous, { throwIfNoEntry: false })) return previous;
|
|
113
|
+
+ try {
|
|
114
|
+
+ rmSync(previous, { recursive: true, force: true });
|
|
115
|
+
+ } catch (e) {
|
|
116
|
+
+ // Left for the next start to remove; this update takes another name.
|
|
117
|
+
+ }
|
|
118
|
+
+ if (!statSync(previous, { throwIfNoEntry: false })) return previous;
|
|
119
|
+
+ for (let attempt = 0; ; attempt += 1) {
|
|
120
|
+
+ const unique = `${previous}-${Date.now().toString(36)}${attempt ? `-${attempt}` : ""}`;
|
|
121
|
+
+ if (!statSync(unique, { throwIfNoEntry: false })) return unique;
|
|
122
|
+
+ }
|
|
123
|
+
+}
|
|
124
|
+
+
|
|
125
|
+
+/**
|
|
126
|
+
+ * Puts the new app where the running one is. The running app is moved aside
|
|
127
|
+
+ * rather than deleted first, so a move that fails (another volume, a
|
|
128
|
+
+ * read-only one, no permission) puts it back instead of leaving no app at all.
|
|
129
|
+
+ * The copy moved aside is removed when it can be; one that cannot goes at the
|
|
130
|
+
+ * app's next start.
|
|
131
|
+
+ */
|
|
132
|
+
+function replaceAppBundle(runningAppPath: string, newAppPath: string) {
|
|
133
|
+
+ const hadRunningApp = !!statSync(runningAppPath, { throwIfNoEntry: false });
|
|
134
|
+
+ const asidePath = hadRunningApp ? updateAsidePath(runningAppPath) : null;
|
|
135
|
+
+ if (asidePath) renameSync(runningAppPath, asidePath);
|
|
136
|
+
+
|
|
137
|
+
+ emitStatus("replacing-app", "Installing new version...");
|
|
138
|
+
+ try {
|
|
139
|
+
+ renameSync(newAppPath, runningAppPath);
|
|
140
|
+
+ } catch (error) {
|
|
141
|
+
+ if (asidePath) renameSync(asidePath, runningAppPath);
|
|
142
|
+
+ throw error;
|
|
143
|
+
+ }
|
|
144
|
+
+ if (!asidePath) return;
|
|
145
|
+
+ try {
|
|
146
|
+
+ rmSync(asidePath, { recursive: true, force: true });
|
|
147
|
+
+ } catch (e) {
|
|
148
|
+
+ // The new app is in place; the app removes the old copy at its next start.
|
|
149
|
+
+ }
|
|
150
|
+
+}
|
|
151
|
+
+
|
|
152
|
+
const Updater = {
|
|
153
|
+
updateInfo: () => {
|
|
154
|
+
return updateInfo;
|
|
155
|
+
@@ -176,6 +237,12 @@ const Updater = {
|
|
101
156
|
onStatusChangeCallback = callback;
|
|
102
157
|
},
|
|
103
158
|
|
|
@@ -110,7 +165,7 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
|
|
|
110
165
|
// todo: allow switching channels, by default will check the current channel
|
|
111
166
|
checkForUpdate: async () => {
|
|
112
167
|
emitStatus("checking", "Checking for updates...");
|
|
113
|
-
@@ -196,11 +
|
|
168
|
+
@@ -196,11 +263,11 @@ const Updater = {
|
|
114
169
|
|
|
115
170
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
116
171
|
const platformPrefix = getPlatformPrefix(
|
|
@@ -124,7 +179,7 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
|
|
|
124
179
|
|
|
125
180
|
try {
|
|
126
181
|
const updateInfoResponse = await fetch(updateInfoUrl);
|
|
127
|
-
@@ -329,11 +
|
|
182
|
+
@@ -329,11 +396,11 @@ const Updater = {
|
|
128
183
|
|
|
129
184
|
// check if there's a patch file for it
|
|
130
185
|
const platformPrefix = getPlatformPrefix(
|
|
@@ -138,7 +193,7 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
|
|
|
138
193
|
emitStatus(
|
|
139
194
|
"fetching-patch",
|
|
140
195
|
`Checking for patch: ${currentHash.slice(0, 8)}`,
|
|
141
|
-
@@ -612,12 +
|
|
196
|
+
@@ -612,12 +679,12 @@ const Updater = {
|
|
142
197
|
|
|
143
198
|
const cacheBuster = Math.random().toString(36).substring(7);
|
|
144
199
|
const platformPrefix = getPlatformPrefix(
|
|
@@ -153,6 +208,52 @@ index 39daf6d341780af8e30c2c50327162863f5977ec..bf4a4b615f84904759d4bcd57be0d899
|
|
|
153
208
|
const prevVersionCompressedTarballPath = join(
|
|
154
209
|
appDataFolder,
|
|
155
210
|
"self-extraction",
|
|
211
|
+
@@ -760,7 +827,12 @@ const Updater = {
|
|
212
|
+
mkdirSync(extractionFolder, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
- let latestHash = (await Updater.checkForUpdate()).hash;
|
|
216
|
+
+ // Install the build that was downloaded. Checking again here fetched
|
|
217
|
+
+ // the newest manifest instead: once a newer release was out (or the
|
|
218
|
+
+ // check failed offline) its tar was not there and the install
|
|
219
|
+
+ // returned without a word, so a download waiting for a restart could
|
|
220
|
+
+ // never be installed.
|
|
221
|
+
+ let latestHash = updateInfo.hash;
|
|
222
|
+
const latestTarPath = join(extractionFolder, `${latestHash}.tar`);
|
|
223
|
+
|
|
224
|
+
let appBundleSubpath: string = "";
|
|
225
|
+
@@ -894,14 +966,7 @@ const Updater = {
|
|
226
|
+
emitStatus("replacing-app", "Removing old version...");
|
|
227
|
+
|
|
228
|
+
if (currentOS === "macos") {
|
|
229
|
+
- // Remove existing app before installing the new one
|
|
230
|
+
- if (statSync(runningAppBundlePath, { throwIfNoEntry: false })) {
|
|
231
|
+
- rmSync(runningAppBundlePath, { recursive: true });
|
|
232
|
+
- }
|
|
233
|
+
-
|
|
234
|
+
- emitStatus("replacing-app", "Installing new version...");
|
|
235
|
+
- // Move new app to running location
|
|
236
|
+
- renameSync(newAppBundlePath, runningAppBundlePath);
|
|
237
|
+
+ replaceAppBundle(runningAppBundlePath, newAppBundlePath);
|
|
238
|
+
|
|
239
|
+
// Remove quarantine extended attributes to prevent "damaged" error
|
|
240
|
+
// The inner bundle is already signed/notarized, but macOS applies
|
|
241
|
+
@@ -918,14 +983,7 @@ const Updater = {
|
|
242
|
+
// On Linux, we now have directory bundles instead of AppImage files
|
|
243
|
+
// The app is stored in {appDataFolder}/app/
|
|
244
|
+
const appBundleDir = join(appDataFolder, "app");
|
|
245
|
+
-
|
|
246
|
+
- // Remove existing app directory if it exists
|
|
247
|
+
- if (statSync(appBundleDir, { throwIfNoEntry: false })) {
|
|
248
|
+
- rmSync(appBundleDir, { recursive: true });
|
|
249
|
+
- }
|
|
250
|
+
-
|
|
251
|
+
- // Move new app bundle directory to app location
|
|
252
|
+
- renameSync(newAppBundlePath, appBundleDir);
|
|
253
|
+
+ replaceAppBundle(appBundleDir, newAppBundlePath);
|
|
254
|
+
|
|
255
|
+
// Ensure launcher binary is executable
|
|
256
|
+
const launcherPath = join(appBundleDir, "bin", "launcher");
|
|
156
257
|
diff --git a/dist/api/bun/preload/.generated/compiled.ts b/dist/api/bun/preload/.generated/compiled.ts
|
|
157
258
|
index 3ab1d7d8de8938ae60823650fd3a29b952a76cb6..e0e841e6a87ce41533c6bdda8d29400aaf478e31 100644
|
|
158
259
|
--- a/dist/api/bun/preload/.generated/compiled.ts
|
package/src/api-client/data.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import type { LettersParams } from "./letters";
|
|
28
28
|
import type { IpoCalendarParams, IpoCalendarPayload } from "./ipo";
|
|
29
29
|
import type { TickerChanges } from "../types/ticker-changes";
|
|
30
|
+
import type { IssuerListings } from "../types/issuer-listings";
|
|
30
31
|
import type { OrganizationTickersResponse } from "./organization-tickers";
|
|
31
32
|
import type { CryptoMarketsPayload, StablecoinSupplyHistoryPayload, StablecoinSupplyPayload } from "./crypto-markets";
|
|
32
33
|
import type { CentralBankRatesPayload } from "./central-bank-rates";
|
|
@@ -470,6 +471,20 @@ export class CloudDataApi {
|
|
|
470
471
|
return answers;
|
|
471
472
|
}
|
|
472
473
|
|
|
474
|
+
/** Every listing of the company a listing belongs to (RCI and RCI.B:TSX are Rogers); empty for one the service does not hold. */
|
|
475
|
+
async getCloudIssuerListings(
|
|
476
|
+
symbol: string,
|
|
477
|
+
exchange = "",
|
|
478
|
+
options: { signal?: AbortSignal } = {},
|
|
479
|
+
): Promise<CloudMarketResponse<IssuerListings>> {
|
|
480
|
+
const params = new URLSearchParams({ symbol: symbol.trim().toUpperCase() });
|
|
481
|
+
if (exchange.trim()) params.set("exchange", exchange.trim().toUpperCase());
|
|
482
|
+
return this.request<CloudMarketResponse<IssuerListings>>(
|
|
483
|
+
`/market/issuer-listings?${params}`,
|
|
484
|
+
options.signal ? { signal: options.signal } : undefined,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
473
488
|
/** The listed company each organization name resolves to, if any: a trial's lead sponsors, a page at a time. */
|
|
474
489
|
async getCloudOrganizationTickers(names: readonly string[], signal?: AbortSignal): Promise<OrganizationTickersResponse> {
|
|
475
490
|
return this.request<OrganizationTickersResponse>("/market/organization-tickers", {
|
package/src/api-client/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ export type {
|
|
|
54
54
|
CrashReportSurface,
|
|
55
55
|
CrashReportsPayload,
|
|
56
56
|
FunctionUsageCount,
|
|
57
|
+
UpdateFailureReport,
|
|
57
58
|
UsageCountsPayload,
|
|
58
59
|
UsageCountsSurface,
|
|
59
60
|
} from "./telemetry";
|
|
@@ -774,6 +775,7 @@ class GloomApiClient {
|
|
|
774
775
|
getCloudTickerChanges = this.data.getCloudTickerChanges.bind(this.data);
|
|
775
776
|
getCloudOrganizationTickers = this.data.getCloudOrganizationTickers.bind(this.data);
|
|
776
777
|
getCloudTickerChangesBatch = this.data.getCloudTickerChangesBatch.bind(this.data);
|
|
778
|
+
getCloudIssuerListings = this.data.getCloudIssuerListings.bind(this.data);
|
|
777
779
|
searchInstrumentsBatch = this.data.searchInstrumentsBatch.bind(this.data);
|
|
778
780
|
getCloudHistory = this.data.getCloudHistory.bind(this.data);
|
|
779
781
|
getCloudExchangeRate = this.data.getCloudExchangeRate.bind(this.data);
|
|
@@ -413,9 +413,10 @@ export class CloudApiRequestTransport {
|
|
|
413
413
|
// change lookup: a note beside a quote, never one of the refused market
|
|
414
414
|
// requests a lookup of many symbols waits out and retries. Nor the
|
|
415
415
|
// tickers beside a list of organization names, which say so in place,
|
|
416
|
-
// or a 13F filer's EDGAR filing list, whose report says it went unchecked
|
|
417
|
-
//
|
|
418
|
-
//
|
|
416
|
+
// or a 13F filer's EDGAR filing list, whose report says it went unchecked,
|
|
417
|
+
// or a company's other listings, beside a list check that still answers
|
|
418
|
+
// for the listing itself. Nor the batched searches and ticker changes a
|
|
419
|
+
// list of symbols starts with: a service without them answers 404, and whatever they leave
|
|
419
420
|
// unanswered is looked up one symbol at a time, which is tracked.
|
|
420
421
|
if (
|
|
421
422
|
path.startsWith("/telemetry/")
|
|
@@ -423,6 +424,7 @@ export class CloudApiRequestTransport {
|
|
|
423
424
|
|| path === "/cloud/geo/layers"
|
|
424
425
|
|| path.startsWith("/cloud/sec/13f/filer?")
|
|
425
426
|
|| path.startsWith("/market/ticker-changes?")
|
|
427
|
+
|| path.startsWith("/market/issuer-listings?")
|
|
426
428
|
|| path === "/market/ticker-changes/batch"
|
|
427
429
|
|| path === "/market/search/batch"
|
|
428
430
|
|| path === "/market/organization-tickers"
|
|
@@ -16,6 +16,23 @@ export interface CrashReportError {
|
|
|
16
16
|
stack?: string;
|
|
17
17
|
plugin?: string;
|
|
18
18
|
pane?: string;
|
|
19
|
+
/** A failed update step, field by field. Servers that do not know it drop it. */
|
|
20
|
+
update?: UpdateFailureReport;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A failed update step as error tracking gets it: fixed values and version
|
|
25
|
+
* numbers only, never a path, URL or the error's own text.
|
|
26
|
+
*/
|
|
27
|
+
export interface UpdateFailureReport {
|
|
28
|
+
phase: "check" | "download" | "verify" | "apply" | "relaunch";
|
|
29
|
+
code: string;
|
|
30
|
+
appVersion: string;
|
|
31
|
+
targetVersion?: string;
|
|
32
|
+
channel: "stable" | "nightly";
|
|
33
|
+
platform: "macos" | "windows" | "linux" | "other";
|
|
34
|
+
arch: "arm64" | "x64" | "other";
|
|
35
|
+
installKind: string;
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
/** Body of `POST /telemetry/errors`. The server forwards each error to error tracking. */
|
|
@@ -8,7 +8,7 @@ import type { AppAction, AppState } from "../state/app/context";
|
|
|
8
8
|
import type { TickerRecord } from "../types/ticker";
|
|
9
9
|
import { findPaneInstance } from "../types/config";
|
|
10
10
|
import type { ReleaseInfo } from "../updater";
|
|
11
|
-
import { canRestartToApply,
|
|
11
|
+
import { canRestartToApply, canRetryUpdate } from "../updater";
|
|
12
12
|
import { getVisiblePaneCycleOrder } from "../components/layout/pane/cycle-order";
|
|
13
13
|
import { requestFunctionHelp, useFunctionHelpHost } from "../plugins/builtin/help/function-card";
|
|
14
14
|
import { buildRegistryHelpIndex, helpFunctionForPane } from "../plugins/builtin/help/function-index";
|
|
@@ -37,21 +37,6 @@ import {
|
|
|
37
37
|
/** Long enough to read a few conflicts and choose Review. */
|
|
38
38
|
const KEYBINDING_NOTICE_DURATION_MS = 15_000;
|
|
39
39
|
|
|
40
|
-
/**
|
|
41
|
-
* Whether the install-update key has something to retry: a release this build
|
|
42
|
-
* can install itself whose last attempt failed. A fresh one downloads on its
|
|
43
|
-
* own, and a manual or managed one names its own command. (A downloaded
|
|
44
|
-
* desktop update has its own use of the key: restarting into it.)
|
|
45
|
-
*/
|
|
46
|
-
export function canRetryUpdate(
|
|
47
|
-
state: Pick<AppState, "updateAvailable" | "updateProgress" | "updateCheckInProgress">,
|
|
48
|
-
): boolean {
|
|
49
|
-
return !!state.updateAvailable
|
|
50
|
-
&& state.updateProgress?.phase === "error"
|
|
51
|
-
&& !state.updateCheckInProgress
|
|
52
|
-
&& canSelfUpdate(state.updateAvailable);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
40
|
/**
|
|
56
41
|
* Tells the user once per launch when `config.json` holds a binding that does
|
|
57
42
|
* not parse, names no action, or lands on a key another action already has.
|
|
@@ -6,6 +6,7 @@ import type { AppAction, AppState } from "../../state/app/context";
|
|
|
6
6
|
import {
|
|
7
7
|
applyUpdate,
|
|
8
8
|
canRestartToApply,
|
|
9
|
+
canRetryUpdate,
|
|
9
10
|
canSelfUpdate,
|
|
10
11
|
checkForUpdateDetailed,
|
|
11
12
|
discardStagedUpdate,
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
type UpdateProgress,
|
|
16
17
|
} from "../../updater";
|
|
17
18
|
import { resolveUpdateChannel, stagedUpdateFitsChannel } from "../../updater/channel";
|
|
19
|
+
import { updateFailureReason } from "../../updater/failure";
|
|
20
|
+
import { t, tf } from "../../i18n";
|
|
18
21
|
import type { UpdateChannel } from "../../types/config";
|
|
19
22
|
import { VERSION } from "../../version";
|
|
20
23
|
import { debugLog } from "../../utils/debug-log";
|
|
@@ -27,11 +30,14 @@ const UPDATE_CHECK_INTERVAL_MS = 60 * 60_000; // hourly
|
|
|
27
30
|
const updateLog = debugLog.createLogger("update");
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
|
-
* The header
|
|
31
|
-
*
|
|
33
|
+
* The header says why an update failed in a few words and the command bar
|
|
34
|
+
* shows the whole error: the log keeps what the updater actually said, with
|
|
35
|
+
* the step and the reason.
|
|
32
36
|
*/
|
|
33
37
|
function logUpdateFailure(progress: UpdateProgress): void {
|
|
34
|
-
if (progress.phase === "error")
|
|
38
|
+
if (progress.phase === "error") {
|
|
39
|
+
updateLog.error("update failed", { error: progress.error, phase: progress.failure?.phase, code: progress.failure?.code });
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/** An update is being downloaded, applied, or is downloaded and waiting for a restart. */
|
|
@@ -127,7 +133,9 @@ export function useAppUpdateRuntime({
|
|
|
127
133
|
return { kind: "error", error: message || "Update check failed" };
|
|
128
134
|
});
|
|
129
135
|
|
|
130
|
-
if (result.kind === "error")
|
|
136
|
+
if (result.kind === "error") {
|
|
137
|
+
updateLog.warn("update check failed", { error: result.error, code: result.failure?.code });
|
|
138
|
+
}
|
|
131
139
|
|
|
132
140
|
if (!manual) {
|
|
133
141
|
if (result.kind === "available" && result.release.tagName !== declinedTagRef.current) {
|
|
@@ -154,13 +162,25 @@ export function useAppUpdateRuntime({
|
|
|
154
162
|
return;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
// The reason in a few words, as the failed banner gives it; the log has the error.
|
|
166
|
+
const reason = result.failure ? t(updateFailureReason(result.failure)) : result.error;
|
|
167
|
+
dispatch({ type: "SET_UPDATE_NOTICE", notice: tf("Update check failed: {reason}", { reason }) });
|
|
158
168
|
}, [dispatch, stateRef]);
|
|
159
169
|
|
|
160
170
|
const runUpdateCommand = useCallback(() => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
171
|
+
const state = stateRef.current;
|
|
172
|
+
if (canRestartToApply(state.updateProgress)) {
|
|
173
|
+
restartToApplyUpdate();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// After a failed download or install, the command tries again, as the key does.
|
|
177
|
+
if (canRetryUpdate(state)) {
|
|
178
|
+
startUpdate(state.updateAvailable!);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (state.updateProgress?.phase === "error") dispatch({ type: "SET_UPDATE_PROGRESS", progress: null });
|
|
182
|
+
void runUpdateCheck(true);
|
|
183
|
+
}, [dispatch, restartToApplyUpdate, runUpdateCheck, startUpdate, stateRef]);
|
|
164
184
|
|
|
165
185
|
useEffect(() => {
|
|
166
186
|
if (!enabled || isDetachedWindow) return;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"src/plugins/builtin/account-management/pane.tsx": {
|
|
3
|
+
"hasSession || apiClient.isSignedIn()": "Signed out, every tab is the same sign-in wall."
|
|
4
|
+
},
|
|
5
|
+
"src/plugins/builtin/analytics/index.tsx": {
|
|
6
|
+
"portfolioTabs.length > 0": "The tabs are the user's portfolios; with none there is nothing to choose between."
|
|
7
|
+
},
|
|
8
|
+
"src/plugins/builtin/analytics/risk-pane.tsx": {
|
|
9
|
+
"portfolio": "No local portfolio yet: the pane says to add holdings instead of offering views of nothing."
|
|
10
|
+
},
|
|
11
|
+
"src/plugins/builtin/attention/pane.tsx": {
|
|
12
|
+
"!signInWall": "Signed out, every tab is the same sign-in wall."
|
|
13
|
+
},
|
|
14
|
+
"src/plugins/builtin/awards/pane.tsx": {
|
|
15
|
+
"!denied": "Access denied draws the sign-in or plan wall for every tab."
|
|
16
|
+
},
|
|
17
|
+
"src/plugins/builtin/backtest/pane.tsx": {
|
|
18
|
+
"symbol": "No ticker yet: the pane asks for one before any view applies."
|
|
19
|
+
},
|
|
20
|
+
"src/plugins/builtin/catalysts/pane.tsx": {
|
|
21
|
+
"!openId": "An open event replaces the list by design and registers its own Evidence and History strip."
|
|
22
|
+
},
|
|
23
|
+
"src/plugins/builtin/cloud/team/pane.tsx": {
|
|
24
|
+
"signedIn": "The tabs are the user's teams; signed out there are none and the pane is a sign-in wall."
|
|
25
|
+
},
|
|
26
|
+
"src/plugins/builtin/company-attention/pane.tsx": {
|
|
27
|
+
"!signInWall && !openCompany && !app": "Signed out it is a sign-in wall; an open company or app replaces the board by design and registers its own strip."
|
|
28
|
+
},
|
|
29
|
+
"src/plugins/builtin/company-kpis/pane.tsx": {
|
|
30
|
+
"symbol && !signInWall": "No ticker yet, or signed out: every tab would be the same prompt or sign-in wall."
|
|
31
|
+
},
|
|
32
|
+
"src/plugins/builtin/congress-trades/pane.tsx": {
|
|
33
|
+
"tickerFilter": "A ticker's own congressional trades are one list; the Trades, Members and Tickers tabs belong to the whole-market view."
|
|
34
|
+
},
|
|
35
|
+
"src/plugins/builtin/debt-maturities/pane.tsx": {
|
|
36
|
+
"symbol && !signInWall": "No ticker yet, or signed out: every tab would be the same prompt or sign-in wall."
|
|
37
|
+
},
|
|
38
|
+
"src/plugins/builtin/earnings-ripple/pane.tsx": {
|
|
39
|
+
"holdings.length": "No holdings to ripple from yet: the pane says how to add them instead of offering views of nothing."
|
|
40
|
+
},
|
|
41
|
+
"src/plugins/builtin/estimate-revisions/pane.tsx": {
|
|
42
|
+
"!symbol || signInWall": "No ticker yet, or signed out: every tab would be the same prompt or sign-in wall."
|
|
43
|
+
},
|
|
44
|
+
"src/plugins/builtin/gpu/pane.tsx": {
|
|
45
|
+
"!signIn": "Signed out or unverified, every tab is the same sign-in wall."
|
|
46
|
+
},
|
|
47
|
+
"src/plugins/builtin/portfolio-list/pane/index.tsx": {
|
|
48
|
+
"showCollectionTabs": "The tabs are the pane's collections; a single collection has nothing to switch to."
|
|
49
|
+
},
|
|
50
|
+
"src/plugins/builtin/power/pane.tsx": {
|
|
51
|
+
"!signInWall": "Signed out, every tab is the same sign-in wall."
|
|
52
|
+
},
|
|
53
|
+
"src/plugins/builtin/research-search/pane.tsx": {
|
|
54
|
+
"signInRequired || verificationRequired": "Signed out or unverified, every mode is the same sign-in wall."
|
|
55
|
+
},
|
|
56
|
+
"src/plugins/builtin/shared/filing-year-reader.tsx": {
|
|
57
|
+
"years.length > 1": "The tabs are the filings on record, known once their list answers; a single filing has no other year to pick."
|
|
58
|
+
},
|
|
59
|
+
"src/plugins/builtin/supply-chain/pane.tsx": {
|
|
60
|
+
"symbol": "No ticker yet: the pane asks for one before any view applies."
|
|
61
|
+
},
|
|
62
|
+
"src/plugins/builtin/ticker-detail/pane.tsx": {
|
|
63
|
+
"!paneSettings.hideTabs && ticker": "The hideTabs setting locks the pane to one view, and with no ticker selected there is nothing to research."
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -218,8 +218,9 @@ import { centralBankEventsMessage } from "../central-bank-events";
|
|
|
218
218
|
import { classSharesToday } from "../../plugins/builtin/holders/client";
|
|
219
219
|
import { filingFormMatches, SEC_FILING_FETCH_LIMIT } from "../../plugins/builtin/sec/forms";
|
|
220
220
|
import { currentReportItemsLabel } from "../../utils/sec-filings";
|
|
221
|
-
import { loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
|
|
222
|
-
import { insiderEmptyNotice } from "../../plugins/builtin/insider/issuer";
|
|
221
|
+
import { INSIDER_REPORT_FILINGS, loadParsedInsiderFilings } from "../../plugins/builtin/insider/client";
|
|
222
|
+
import { insiderEmptyNotice, loadHomeInsiderReportsNote } from "../../plugins/builtin/insider/issuer";
|
|
223
|
+
import { buildInsiderSummary } from "../../plugins/builtin/insider/model";
|
|
223
224
|
import {
|
|
224
225
|
BENEFICIAL_REPORT_COLUMNS,
|
|
225
226
|
beneficialCoverageNotices,
|
|
@@ -290,8 +291,8 @@ function uniqueEntries(entries: readonly string[]): string[] {
|
|
|
290
291
|
|
|
291
292
|
/**
|
|
292
293
|
* The symbols to resolve: each entry with a space read as a company's name
|
|
293
|
-
* when search finds one, else as its words
|
|
294
|
-
* with the repeats that leaves dropped. A name
|
|
294
|
+
* when search finds one, else as its words when they are symbols
|
|
295
|
+
* (`readNameEntries`), with the repeats that leaves dropped. A name stays whole.
|
|
295
296
|
*/
|
|
296
297
|
async function readSymbols(symbols: readonly string[], dataProvider: Parameters<typeof readNameEntries>[1]): Promise<string[]> {
|
|
297
298
|
return uniqueEntries(await readNameEntries(symbols, dataProvider));
|
|
@@ -1549,50 +1550,81 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
1549
1550
|
|
|
1550
1551
|
type Form4Insiders = "available" | "none" | "foreign-private-issuer" | "no-sec-reports";
|
|
1551
1552
|
|
|
1553
|
+
/** The company's own Form 4 filings as `fn INS` reads them: whether it has any, and its summary of them. */
|
|
1554
|
+
interface Form4Report {
|
|
1555
|
+
status: Form4Insiders;
|
|
1556
|
+
/** `fn INS`'s line of the last 90 days' open-market buys and sales in the filings read; null when it has none. */
|
|
1557
|
+
summary: string | null;
|
|
1558
|
+
/** The Form 4 lines in the filings read, and how many filings that was. */
|
|
1559
|
+
lines: number;
|
|
1560
|
+
filings: number;
|
|
1561
|
+
/** The newest filing's date (YYYY-MM-DD), which dates the report. */
|
|
1562
|
+
newestFiled: string | null;
|
|
1563
|
+
/** "Canadian insider reports (SEDI) not covered." beside the reason a company from abroad has none. */
|
|
1564
|
+
home: string | null;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1552
1567
|
/**
|
|
1553
|
-
*
|
|
1554
|
-
*
|
|
1555
|
-
* nothing for files no SEC
|
|
1568
|
+
* The company's Form 4 filings, read as `fn INS` reads its first page (its
|
|
1569
|
+
* latest 20 of its own) and summed up as it sums them; null when they could
|
|
1570
|
+
* not be read. A listing abroad the SEC lists nothing for files no SEC
|
|
1571
|
+
* reports at all, and a company from abroad with none names its home
|
|
1572
|
+
* market's insider reports as not covered either.
|
|
1556
1573
|
*/
|
|
1557
|
-
async function
|
|
1574
|
+
async function loadForm4Report(market: MarketContext, listing: CliListing): Promise<Form4Report | null> {
|
|
1575
|
+
const { symbol, exchange } = listing.request;
|
|
1558
1576
|
try {
|
|
1559
|
-
const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider,
|
|
1560
|
-
limit:
|
|
1561
|
-
exchange
|
|
1577
|
+
const { parsed, foreignPrivateIssuer, noFilings } = await loadParsedInsiderFilings(market.dataProvider, symbol, {
|
|
1578
|
+
limit: INSIDER_REPORT_FILINGS,
|
|
1579
|
+
exchange,
|
|
1562
1580
|
});
|
|
1563
|
-
if (parsed.length > 0)
|
|
1564
|
-
|
|
1565
|
-
|
|
1581
|
+
if (parsed.length > 0) {
|
|
1582
|
+
const filed = parsed.map(({ filing }) => new Date(filing.filingDate).getTime()).filter(Number.isFinite);
|
|
1583
|
+
return {
|
|
1584
|
+
status: "available",
|
|
1585
|
+
summary: buildInsiderSummary(parsed, Date.now(), parsed),
|
|
1586
|
+
lines: parsed.filter(({ transaction }) => transaction != null).length,
|
|
1587
|
+
filings: new Set(parsed.map(({ filing }) => filing.accessionNumber)).size,
|
|
1588
|
+
newestFiled: filed.length > 0 ? new Date(Math.max(...filed)).toISOString().slice(0, 10) : null,
|
|
1589
|
+
home: null,
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
const status: Form4Insiders = noFilings && nonUsSecListingVenue(symbol, exchange) ? "no-sec-reports"
|
|
1593
|
+
: foreignPrivateIssuer ? "foreign-private-issuer" : "none";
|
|
1594
|
+
const home = await loadHomeInsiderReportsNote(market.dataProvider, listing.key, exchange, foreignPrivateIssuer);
|
|
1595
|
+
return { status, summary: null, lines: 0, filings: 0, newestFiled: null, home };
|
|
1566
1596
|
} catch (error) {
|
|
1567
|
-
logFailure("cli.commands-market", "Read insider
|
|
1597
|
+
logFailure("cli.commands-market", "Read insider filings failed")(error);
|
|
1568
1598
|
return null;
|
|
1569
1599
|
}
|
|
1570
1600
|
}
|
|
1571
1601
|
|
|
1572
1602
|
/**
|
|
1573
|
-
*
|
|
1574
|
-
*
|
|
1575
|
-
* Form 4
|
|
1576
|
-
*
|
|
1603
|
+
* The insider lines under `insider`'s holders. A company's insiders file
|
|
1604
|
+
* Form 4, which 13F holder lists rarely name, so the report leads with
|
|
1605
|
+
* `fn INS`'s summary of the Form 4 filings and points to the transactions;
|
|
1606
|
+
* a company with none says why, in the words `fn INS` uses. The insider share
|
|
1607
|
+
* of the company follows when the holder data reports it.
|
|
1577
1608
|
*/
|
|
1578
1609
|
function insiderSummary(
|
|
1579
1610
|
data: HolderData,
|
|
1580
|
-
ownerTypes: Set<string>,
|
|
1581
1611
|
listing: Pick<CliListing, "key">,
|
|
1582
1612
|
identity: ListingIdentity,
|
|
1583
|
-
form4:
|
|
1613
|
+
form4: Form4Report | null,
|
|
1584
1614
|
): string {
|
|
1585
1615
|
const held = data.summary?.insidersPercentHeld;
|
|
1586
1616
|
const share = held == null || !Number.isFinite(held)
|
|
1587
1617
|
? ""
|
|
1588
1618
|
: `Insiders hold ${formatFractionPercentCell(held)} of shares outstanding.`;
|
|
1589
|
-
const listed = data.holders.some((holder) => ownerTypes.has(holder.ownerType));
|
|
1590
1619
|
// The listing as the command read it (CAR:ASX), not the source's spelling of it (CAR.AX), which fn does not take.
|
|
1591
|
-
const transactions = form4 === "available"
|
|
1592
|
-
?
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1620
|
+
const transactions = form4?.status === "available"
|
|
1621
|
+
? [
|
|
1622
|
+
...form4.summary ? [form4.summary] : [],
|
|
1623
|
+
`${form4.lines} Form 4 ${form4.lines === 1 ? "line" : "lines"} in the latest ${form4.filings} ${form4.filings === 1 ? "filing" : "filings"}${form4.newestFiled ? `, newest filed ${form4.newestFiled}` : ""}: gloomberb fn INS ${listing.key}`,
|
|
1624
|
+
].join("\n")
|
|
1625
|
+
: form4?.status === "no-sec-reports" ? [noSecReportsNotice(identity), form4.home].filter(Boolean).join(" ")
|
|
1626
|
+
: form4 ? insiderEmptyNotice(form4.status === "foreign-private-issuer", form4.home) : "";
|
|
1627
|
+
return [transactions, share].filter(Boolean).join("\n");
|
|
1596
1628
|
}
|
|
1597
1629
|
|
|
1598
1630
|
/** `holders --form 13d|13g|all`: the 13D/13G beneficial owners, joined by name to the 13F holders. */
|
|
@@ -1682,7 +1714,7 @@ async function runHolders(
|
|
|
1682
1714
|
{ command: commandName, noun: "holder data" },
|
|
1683
1715
|
),
|
|
1684
1716
|
loadListingQuote(market.dataProvider, listing),
|
|
1685
|
-
insiderCommand ?
|
|
1717
|
+
insiderCommand ? loadForm4Report(market, listing) : null,
|
|
1686
1718
|
]);
|
|
1687
1719
|
const identity = listingIdentity(listing, quote);
|
|
1688
1720
|
const choice = await listingChoiceNotes(listing, market, quote, venues);
|
|
@@ -1704,7 +1736,9 @@ async function runHolders(
|
|
|
1704
1736
|
const markedRows = rows.some((row) => holderShareBasisMarker(row));
|
|
1705
1737
|
// The heading names the listing; its unit, date and how much of the list follows on the same line.
|
|
1706
1738
|
// One "as of" only when every row shown is of that date; each row carries its own.
|
|
1707
|
-
|
|
1739
|
+
// An insider list the 13F filings name nobody in is not "none reported": the Form 4 summary under it says what insiders did.
|
|
1740
|
+
const form4Only = insiderCommand && rows.length === 0;
|
|
1741
|
+
const facts = form4Only ? [] : holderListFacts({
|
|
1708
1742
|
currency: valueCurrency,
|
|
1709
1743
|
listingCurrency: data.currency,
|
|
1710
1744
|
period: holderListPeriod(shownRows, data.asOf),
|
|
@@ -1715,8 +1749,10 @@ async function runHolders(
|
|
|
1715
1749
|
});
|
|
1716
1750
|
const unvaluedNote = unvaluedHoldersNote(shownRows);
|
|
1717
1751
|
const columns = holderReportColumns(shownRows, valueCurrency);
|
|
1718
|
-
// Dated as HDS dates it: by the list's as-of.
|
|
1719
|
-
const freshness =
|
|
1752
|
+
// Dated as HDS dates it: by the list's as-of; an insider report of Form 4 filings alone as INS dates it, by the newest.
|
|
1753
|
+
const freshness = form4Only
|
|
1754
|
+
? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILINGS, basis: "Form 4 filings", observedKey: "filingDate" })
|
|
1755
|
+
: rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
|
|
1720
1756
|
// Named as fn HDS takes it, so the closing notes point to the same listing.
|
|
1721
1757
|
const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
|
|
1722
1758
|
const { notices } = holderReportNotices({ data, requested, shown: shownRows, reported: rows.length, total });
|
|
@@ -1738,7 +1774,11 @@ async function runHolders(
|
|
|
1738
1774
|
truncated: shown < (total ?? rows.length),
|
|
1739
1775
|
valueBasis,
|
|
1740
1776
|
positionsBasis,
|
|
1741
|
-
...(insiderCommand ? {
|
|
1777
|
+
...(insiderCommand ? {
|
|
1778
|
+
form4Insiders: form4?.status ?? null,
|
|
1779
|
+
// INS's summary of the latest Form 4 filings, as `fn INS` gives it in its metadata.
|
|
1780
|
+
form4: form4 ? { summary: form4.summary, lines: form4.lines, filings: form4.filings, newestFiled: form4.newestFiled, notCovered: form4.home } : null,
|
|
1781
|
+
} : {}),
|
|
1742
1782
|
},
|
|
1743
1783
|
...(choice.length > 0 ? { warnings: choice } : {}),
|
|
1744
1784
|
}, {
|
|
@@ -1772,7 +1812,7 @@ async function runHolders(
|
|
|
1772
1812
|
exportIdentity: { symbolOrSeries: listing.key },
|
|
1773
1813
|
summary: () => [
|
|
1774
1814
|
[valueBasis ? `Value = ${valueBasis}.` : "", positionsBasis ?? "", shareBasisNote ?? "", unvaluedNote ?? ""].filter(Boolean).join(" "),
|
|
1775
|
-
insiderCommand ? insiderSummary(data,
|
|
1815
|
+
insiderCommand ? insiderSummary(data, listing, identity, form4) : "",
|
|
1776
1816
|
].filter(Boolean).join("\n"),
|
|
1777
1817
|
empty: `No holders reported for ${listingTitle(identity)}.`,
|
|
1778
1818
|
});
|