@agent-native/core 0.114.13 → 0.114.16
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/bin/agent-native.js +35 -14
- package/bin/launcher.js +34 -0
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +21 -0
- package/corpus/core/bin/agent-native.js +35 -14
- package/corpus/core/bin/launcher.js +34 -0
- package/corpus/core/docs/content/deployment.mdx +1 -1
- package/corpus/core/docs/content/frames.mdx +1 -1
- package/corpus/core/docs/content/integrations.mdx +27 -27
- package/corpus/core/docs/content/pr-visual-recap.mdx +17 -13
- package/corpus/core/docs/content/workspace-management.mdx +1 -1
- package/corpus/core/package.json +2 -1
- package/corpus/core/src/agent/engine/builder-engine.ts +4 -0
- package/corpus/core/src/client/AgentPanel.tsx +10 -4
- package/corpus/core/src/client/ConnectBuilderCard.tsx +5 -1
- package/corpus/core/src/client/chat/markdown-renderer.tsx +3 -5
- package/corpus/core/src/client/error-format.ts +2 -1
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/corpus/core/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/corpus/core/src/client/settings/SettingsPanel.tsx +4 -4
- package/corpus/core/src/client/settings/useBuilderStatus.ts +3 -0
- package/corpus/core/src/localization/default-messages.ts +1 -7
- package/corpus/core/src/server/builder-browser.ts +4 -0
- package/corpus/core/src/server/builder-design-systems.ts +6 -1
- package/corpus/core/src/server/fusion-app.ts +5 -1
- package/corpus/core/src/shared/builder-link-tracking.ts +41 -0
- package/corpus/core/src/shared/index.ts +1 -0
- package/corpus/templates/analytics/AGENTS.md +10 -1
- package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +10 -2
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +5 -4
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +20 -32
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-panel-window.ts +62 -0
- package/corpus/templates/analytics/changelog/2026-07-21-chart-series-controls-stay-open-while-moving-from-the-legend.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-21-dashboard-email-captures-complete-large-dashboards-in-chunks.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +4 -4
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +1 -8
- package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +39 -7
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +341 -251
- package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +114 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +84 -6
- package/corpus/templates/analytics/server/plugins/db.ts +13 -0
- package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +25 -8
- package/corpus/templates/clips/app/components/library/library-grid.tsx +118 -9
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -1
- package/corpus/templates/clips/app/components/library/recording-card.tsx +10 -8
- package/corpus/templates/clips/app/components/player/video-player.tsx +10 -4
- package/corpus/templates/clips/app/i18n/en-US.ts +6 -0
- package/corpus/templates/clips/app/routes/_app.trash.tsx +16 -1
- package/corpus/templates/clips/changelog/2026-07-21-choose-which-whisper-model-to-use-in-settings.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-fixed-organization-recording-visibility-default.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-21-meeting-notes-stop-when-calls-end.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +90 -78
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +2 -0
- package/corpus/templates/clips/desktop/src/hooks/useWhisperSettings.ts +172 -0
- package/corpus/templates/clips/desktop/src/lib/meeting-call-app.ts +41 -0
- package/corpus/templates/clips/desktop/src/lib/silence-events.ts +1 -0
- package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +80 -7
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +32 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +268 -20
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_model.rs +285 -164
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +18 -7
- package/corpus/templates/clips/learnings.defaults.md +1 -1
- package/corpus/templates/clips/server/plugins/db.ts +11 -0
- package/corpus/templates/clips/shared/builder-credits.ts +1 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +20 -38
- package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +40 -1
- package/corpus/templates/design/app/components/design/FusionAppBanner.tsx +9 -1
- package/corpus/templates/design/app/components/design/edit-panel/component-section.tsx +5 -1
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +9 -1
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +9 -1
- package/dist/agent/engine/builder-engine.d.ts.map +1 -1
- package/dist/agent/engine/builder-engine.js +4 -0
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +10 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -1
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/markdown-renderer.d.ts.map +1 -1
- package/dist/client/chat/markdown-renderer.js +3 -2
- package/dist/client/chat/markdown-renderer.js.map +1 -1
- package/dist/client/error-format.d.ts +1 -1
- package/dist/client/error-format.d.ts.map +1 -1
- package/dist/client/error-format.js +1 -1
- package/dist/client/error-format.js.map +1 -1
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
- package/dist/client/integrations/IntegrationsPanel.js +3 -4
- package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +21 -7
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +2 -2
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.d.ts.map +1 -1
- package/dist/client/settings/BackgroundAgentSection.js +5 -1
- package/dist/client/settings/BackgroundAgentSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +2 -0
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +0 -6
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +1 -7
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +4 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts.map +1 -1
- package/dist/server/builder-design-systems.js +6 -1
- package/dist/server/builder-design-systems.js.map +1 -1
- package/dist/server/fusion-app.d.ts.map +1 -1
- package/dist/server/fusion-app.js +2 -1
- package/dist/server/fusion-app.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/builder-link-tracking.d.ts +14 -0
- package/dist/shared/builder-link-tracking.d.ts.map +1 -0
- package/dist/shared/builder-link-tracking.js +26 -0
- package/dist/shared/builder-link-tracking.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/docs/content/deployment.mdx +1 -1
- package/docs/content/frames.mdx +1 -1
- package/docs/content/integrations.mdx +27 -27
- package/docs/content/pr-visual-recap.mdx +17 -13
- package/docs/content/workspace-management.mdx +1 -1
- package/package.json +3 -2
- package/src/agent/engine/builder-engine.ts +4 -0
- package/src/client/AgentPanel.tsx +10 -4
- package/src/client/ConnectBuilderCard.tsx +5 -1
- package/src/client/chat/markdown-renderer.tsx +3 -5
- package/src/client/error-format.ts +2 -1
- package/src/client/index.ts +1 -0
- package/src/client/integrations/IntegrationsPanel.tsx +2 -18
- package/src/client/resources/McpIntegrationDialog.tsx +43 -32
- package/src/client/resources/mcp-integration-catalog.ts +4 -2
- package/src/client/settings/BackgroundAgentSection.tsx +5 -1
- package/src/client/settings/SettingsPanel.tsx +4 -4
- package/src/client/settings/useBuilderStatus.ts +3 -0
- package/src/localization/default-messages.ts +1 -7
- package/src/server/builder-browser.ts +4 -0
- package/src/server/builder-design-systems.ts +6 -1
- package/src/server/fusion-app.ts +5 -1
- package/src/shared/builder-link-tracking.ts +41 -0
- package/src/shared/index.ts +1 -0
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
EMBED_TOKEN_QUERY_PARAM,
|
|
16
16
|
} from "@agent-native/core/shared";
|
|
17
17
|
|
|
18
|
+
import {
|
|
19
|
+
listReportablePanelIds,
|
|
20
|
+
REPORT_PANEL_CHUNK_SIZE,
|
|
21
|
+
} from "../../app/pages/adhoc/sql-dashboard/report-panel-window";
|
|
18
22
|
import type {
|
|
19
23
|
DashboardFilter,
|
|
20
24
|
FilterType,
|
|
@@ -22,6 +26,7 @@ import type {
|
|
|
22
26
|
} from "../../app/pages/adhoc/sql-dashboard/types";
|
|
23
27
|
import {
|
|
24
28
|
getReportDashboard,
|
|
29
|
+
normalizeDashboardReportRecipients,
|
|
25
30
|
type AccessCtx,
|
|
26
31
|
type DashboardReportSubscription,
|
|
27
32
|
} from "./dashboard-report-subscriptions";
|
|
@@ -34,6 +39,7 @@ type ReportSnapshot = {
|
|
|
34
39
|
dashboardUrl: string;
|
|
35
40
|
reportSettingsUrl: string;
|
|
36
41
|
generatedAt: string;
|
|
42
|
+
panelIds: string[];
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
|
|
@@ -49,23 +55,21 @@ const DASHBOARD_REPORT_CID = "dashboard-report-snapshot";
|
|
|
49
55
|
const LOCAL_SCREENSHOT_TIMEOUT_MS = 90_000;
|
|
50
56
|
const SERVERLESS_SCREENSHOT_TIMEOUT_MS = 90_000;
|
|
51
57
|
const SERVERLESS_SECOND_READY_TIMEOUT_MS = 45_000;
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
// up to 60s, leaving a small delivery buffer.
|
|
56
|
-
const SERVERLESS_FULL_ATTEMPT_TIMEOUT_MS = 110_000;
|
|
57
|
-
const SERVERLESS_LIGHTWEIGHT_ATTEMPT_TIMEOUT_MS = 70_000;
|
|
58
|
+
// Reserve 90s under Netlify's 300s background-function limit for bounded
|
|
59
|
+
// browser cleanup plus delivery to the capped recipient list.
|
|
60
|
+
const SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS = 210_000;
|
|
58
61
|
const BROWSER_CLEANUP_TIMEOUT_MS = 10_000;
|
|
59
62
|
const SCREENSHOT_VIEWPORT_PADDING = 64;
|
|
63
|
+
const MAX_DASHBOARD_REPORT_CHUNKS = 10;
|
|
64
|
+
const MAX_DASHBOARD_REPORT_ATTACHMENT_BYTES = 14 * 1024 * 1024;
|
|
60
65
|
|
|
61
66
|
type DashboardScreenshotAttempt = {
|
|
62
|
-
label: "
|
|
67
|
+
label: "chunked";
|
|
63
68
|
viewport: { width: number; height: number };
|
|
64
69
|
captureScale?: number;
|
|
65
70
|
readyTimeout?: number;
|
|
66
71
|
secondReadyTimeout?: number;
|
|
67
72
|
totalTimeout?: number;
|
|
68
|
-
reportPanelLimit?: number;
|
|
69
73
|
};
|
|
70
74
|
|
|
71
75
|
type LaunchedScreenshotBrowser = {
|
|
@@ -209,6 +213,7 @@ async function collectReportSnapshot(
|
|
|
209
213
|
reportSettings: true,
|
|
210
214
|
}),
|
|
211
215
|
generatedAt: new Date().toISOString(),
|
|
216
|
+
panelIds: listReportablePanelIds(config.panels),
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|
|
@@ -351,6 +356,37 @@ function secondReadyTimeoutMs(): number {
|
|
|
351
356
|
: screenshotTimeoutMs();
|
|
352
357
|
}
|
|
353
358
|
|
|
359
|
+
function boundedStageTimeout(capMs: number, deadlineAt?: number): number {
|
|
360
|
+
if (!deadlineAt) return capMs;
|
|
361
|
+
const remainingMs = deadlineAt - Date.now();
|
|
362
|
+
if (remainingMs <= 0) throw new Error("capture deadline exhausted");
|
|
363
|
+
return Math.max(1, Math.min(capMs, remainingMs));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function runWithinCaptureDeadline<T>(
|
|
367
|
+
operation: () => Promise<T>,
|
|
368
|
+
deadlineAt?: number,
|
|
369
|
+
): Promise<T> {
|
|
370
|
+
if (!deadlineAt) return operation();
|
|
371
|
+
const timeoutMs = deadlineAt - Date.now();
|
|
372
|
+
if (timeoutMs <= 0) throw new Error("capture deadline exhausted");
|
|
373
|
+
|
|
374
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
375
|
+
try {
|
|
376
|
+
return await Promise.race([
|
|
377
|
+
operation(),
|
|
378
|
+
new Promise<never>((_, reject) => {
|
|
379
|
+
timeout = setTimeout(
|
|
380
|
+
() => reject(new Error("capture deadline exhausted")),
|
|
381
|
+
timeoutMs,
|
|
382
|
+
);
|
|
383
|
+
}),
|
|
384
|
+
]);
|
|
385
|
+
} finally {
|
|
386
|
+
if (timeout) clearTimeout(timeout);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
354
390
|
function positiveIntEnv(name: string): number | null {
|
|
355
391
|
const raw = process.env[name]?.trim();
|
|
356
392
|
if (!raw) return null;
|
|
@@ -392,12 +428,6 @@ async function waitForDashboardReportReady(
|
|
|
392
428
|
};
|
|
393
429
|
})()`)
|
|
394
430
|
.catch(() => null);
|
|
395
|
-
if (detail?.ready === "true") {
|
|
396
|
-
console.warn(
|
|
397
|
-
"[dashboard-report] Dashboard surface stayed partially loaded; capturing the available panels.",
|
|
398
|
-
);
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
431
|
const message = detail
|
|
402
432
|
? `${err?.message ?? String(err)}; dashboard state: ${JSON.stringify(detail)}`
|
|
403
433
|
: `${err?.message ?? String(err)}; dashboard page was not inspectable`;
|
|
@@ -405,6 +435,34 @@ async function waitForDashboardReportReady(
|
|
|
405
435
|
}
|
|
406
436
|
}
|
|
407
437
|
|
|
438
|
+
async function assertDashboardReportPanelWindow(
|
|
439
|
+
page: any,
|
|
440
|
+
expectedPanelIds: string[],
|
|
441
|
+
): Promise<void> {
|
|
442
|
+
const rawPanelIds = await page.evaluate(`(() =>
|
|
443
|
+
document
|
|
444
|
+
.querySelector("[data-dashboard-report-capture]")
|
|
445
|
+
?.getAttribute("data-dashboard-report-panel-ids") ?? null
|
|
446
|
+
)()`);
|
|
447
|
+
let actualPanelIds: unknown;
|
|
448
|
+
try {
|
|
449
|
+
actualPanelIds =
|
|
450
|
+
typeof rawPanelIds === "string" ? JSON.parse(rawPanelIds) : null;
|
|
451
|
+
} catch {
|
|
452
|
+
actualPanelIds = null;
|
|
453
|
+
}
|
|
454
|
+
if (
|
|
455
|
+
!Array.isArray(actualPanelIds) ||
|
|
456
|
+
actualPanelIds.some((panelId) => typeof panelId !== "string") ||
|
|
457
|
+
actualPanelIds.length !== expectedPanelIds.length ||
|
|
458
|
+
actualPanelIds.some((panelId, index) => panelId !== expectedPanelIds[index])
|
|
459
|
+
) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
`report chunk panel mismatch; expected=${JSON.stringify(expectedPanelIds)} actual=${JSON.stringify(actualPanelIds)}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
408
466
|
async function scrollDashboardForLazyRendering(page: any): Promise<void> {
|
|
409
467
|
await page.evaluate(`(async () => {
|
|
410
468
|
const wait = (ms) =>
|
|
@@ -483,6 +541,15 @@ const DIAGNOSTICS_PROBE_TIMEOUT_MS = 2_000;
|
|
|
483
541
|
const DIAGNOSTICS_MAX_LENGTH = 700;
|
|
484
542
|
const DIAGNOSTICS_COLLECTOR_LIMIT = 5;
|
|
485
543
|
|
|
544
|
+
// netlify.toml's memory = "2gb" is plan-gated and can be silently ignored, so
|
|
545
|
+
// capture the actual lambda memory ceiling alongside current RSS.
|
|
546
|
+
function memoryDiagnostics(): string {
|
|
547
|
+
const lambdaMemoryMb =
|
|
548
|
+
process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE ?? "unknown";
|
|
549
|
+
const rssMb = Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
550
|
+
return `lambdaMemoryMb=${lambdaMemoryMb} rssMb=${rssMb}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
486
553
|
// Best-effort page inspection used when the report surface never becomes
|
|
487
554
|
// visible, so failures carry enough state to tell wrong-page/wedged-renderer/
|
|
488
555
|
// auth-bounce apart. Must never throw and must stay bounded even if the page
|
|
@@ -492,6 +559,7 @@ async function collectPageDiagnostics(
|
|
|
492
559
|
consoleErrors: string[],
|
|
493
560
|
failedRequests: string[],
|
|
494
561
|
): Promise<string> {
|
|
562
|
+
const memory = memoryDiagnostics();
|
|
495
563
|
try {
|
|
496
564
|
let responsive = true;
|
|
497
565
|
let probeTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -512,7 +580,7 @@ async function collectPageDiagnostics(
|
|
|
512
580
|
}
|
|
513
581
|
|
|
514
582
|
if (!responsive) {
|
|
515
|
-
return
|
|
583
|
+
return `${memory} page unresponsive (renderer hung or crashed); consoleErrors=${JSON.stringify(
|
|
516
584
|
consoleErrors,
|
|
517
585
|
)} failedRequests=${JSON.stringify(failedRequests)}`.slice(
|
|
518
586
|
0,
|
|
@@ -557,7 +625,7 @@ async function collectPageDiagnostics(
|
|
|
557
625
|
if (detailsTimeout) clearTimeout(detailsTimeout);
|
|
558
626
|
}
|
|
559
627
|
|
|
560
|
-
return
|
|
628
|
+
return `${memory} page state: ${JSON.stringify({
|
|
561
629
|
url,
|
|
562
630
|
title,
|
|
563
631
|
bodyText,
|
|
@@ -565,88 +633,85 @@ async function collectPageDiagnostics(
|
|
|
565
633
|
failedRequests,
|
|
566
634
|
})}`.slice(0, DIAGNOSTICS_MAX_LENGTH);
|
|
567
635
|
} catch {
|
|
568
|
-
return
|
|
636
|
+
return `${memory} diagnostics unavailable`;
|
|
569
637
|
}
|
|
570
638
|
}
|
|
571
639
|
|
|
572
|
-
|
|
640
|
+
function reportChunkOffsets(panelCount: number): number[] {
|
|
641
|
+
if (panelCount === 0) return [0];
|
|
642
|
+
return Array.from(
|
|
643
|
+
{ length: Math.ceil(panelCount / REPORT_PANEL_CHUNK_SIZE) },
|
|
644
|
+
(_, index) => index * REPORT_PANEL_CHUNK_SIZE,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function screenshotUrlForChunk(
|
|
573
649
|
sub: DashboardReportSubscription,
|
|
574
650
|
snapshot: ReportSnapshot,
|
|
575
|
-
|
|
576
|
-
):
|
|
651
|
+
offset: number,
|
|
652
|
+
): { token: string; url: string } {
|
|
577
653
|
const targetPath = buildDashboardPath(
|
|
578
654
|
snapshot.dashboardId,
|
|
579
655
|
snapshot.filters,
|
|
580
|
-
{
|
|
581
|
-
|
|
582
|
-
|
|
656
|
+
{ reportScreenshot: true },
|
|
657
|
+
);
|
|
658
|
+
const targetUrl = new URL(targetPath, "https://agent-native.invalid/");
|
|
659
|
+
targetUrl.searchParams.set("reportPanelOffset", String(offset));
|
|
660
|
+
targetUrl.searchParams.set(
|
|
661
|
+
"reportPanelLimit",
|
|
662
|
+
String(REPORT_PANEL_CHUNK_SIZE),
|
|
583
663
|
);
|
|
664
|
+
const signedTargetPath = `${targetUrl.pathname}${targetUrl.search}`;
|
|
584
665
|
const token = signEmbedSessionToken({
|
|
585
666
|
ownerEmail: sub.ownerEmail,
|
|
586
667
|
orgId: sub.orgId,
|
|
587
|
-
targetPath,
|
|
668
|
+
targetPath: signedTargetPath,
|
|
588
669
|
scope: `dashboard-report-screenshot:${sub.id}`,
|
|
589
670
|
ttlSeconds: 5 * 60,
|
|
590
671
|
});
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
"reportPanelLimit",
|
|
597
|
-
String(attempt.reportPanelLimit),
|
|
598
|
-
);
|
|
599
|
-
}
|
|
672
|
+
const url = new URL(signedTargetPath, `${dashboardBaseUrl()}/`);
|
|
673
|
+
url.searchParams.set(EMBED_MODE_QUERY_PARAM, "1");
|
|
674
|
+
url.searchParams.set(EMBED_TOKEN_QUERY_PARAM, token);
|
|
675
|
+
return { token, url: url.toString() };
|
|
676
|
+
}
|
|
600
677
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
let
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
678
|
+
async function captureDashboardChunk(
|
|
679
|
+
page: any,
|
|
680
|
+
attempt: DashboardScreenshotAttempt,
|
|
681
|
+
token: string,
|
|
682
|
+
screenshotUrl: string,
|
|
683
|
+
expectedPanelIds: string[],
|
|
684
|
+
deadlineAt?: number,
|
|
685
|
+
): Promise<Buffer> {
|
|
686
|
+
const timeout = screenshotTimeoutMs();
|
|
687
|
+
let captureStage = "pre-seeding the embed session";
|
|
688
|
+
const consoleErrors: string[] = [];
|
|
689
|
+
const failedRequests: string[] = [];
|
|
690
|
+
page.on("console", (msg: any) => {
|
|
691
|
+
if (
|
|
692
|
+
msg.type() !== "error" ||
|
|
693
|
+
consoleErrors.length >= DIAGNOSTICS_COLLECTOR_LIMIT
|
|
694
|
+
)
|
|
695
|
+
return;
|
|
696
|
+
consoleErrors.push(msg.text().slice(0, 160));
|
|
697
|
+
});
|
|
698
|
+
page.on("requestfailed", (req: any) => {
|
|
699
|
+
if (failedRequests.length >= DIAGNOSTICS_COLLECTOR_LIMIT) return;
|
|
700
|
+
failedRequests.push(
|
|
701
|
+
`${req.method()} ${req.url().slice(0, 120)}: ${req.failure()?.errorText ?? "failed"}`,
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
page.on("response", (res: any) => {
|
|
705
|
+
if (
|
|
706
|
+
res.status() < 400 ||
|
|
707
|
+
failedRequests.length >= DIAGNOSTICS_COLLECTOR_LIMIT
|
|
708
|
+
)
|
|
709
|
+
return;
|
|
710
|
+
failedRequests.push(
|
|
711
|
+
`${res.request().method()} ${res.url().slice(0, 120)}: HTTP ${res.status()}`,
|
|
712
|
+
);
|
|
713
|
+
});
|
|
617
714
|
try {
|
|
618
|
-
launchPromise = launchScreenshotBrowser(attempt.viewport);
|
|
619
|
-
const launched = attempt.totalTimeout
|
|
620
|
-
? await Promise.race([
|
|
621
|
-
launchPromise,
|
|
622
|
-
new Promise<never>((_, reject) => {
|
|
623
|
-
launchTimeout = setTimeout(() => {
|
|
624
|
-
attemptTimedOut = true;
|
|
625
|
-
reject(
|
|
626
|
-
new Error(
|
|
627
|
-
`${attempt.label} browser launch exceeded ${attempt.totalTimeout}ms`,
|
|
628
|
-
),
|
|
629
|
-
);
|
|
630
|
-
}, attempt.totalTimeout);
|
|
631
|
-
}),
|
|
632
|
-
])
|
|
633
|
-
: await launchPromise;
|
|
634
|
-
browser = launched.browser;
|
|
635
|
-
cleanup = launched.cleanup;
|
|
636
|
-
newPage = launched.newPage;
|
|
637
|
-
if (attemptTimedOut) {
|
|
638
|
-
throw new Error("Screenshot browser launch exceeded attempt timeout");
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const timeout = screenshotTimeoutMs();
|
|
642
|
-
const page = await newPage();
|
|
643
|
-
|
|
644
|
-
// Belt-and-braces auth: the query token authenticates only after the
|
|
645
|
-
// client-side bootstrap harvests it from the URL, which gives the
|
|
646
|
-
// stripped-down serverless browser several ways to end up session-less.
|
|
647
|
-
// Seeding the same signed token as a cookie authenticates every request
|
|
648
|
-
// server-side with no client cooperation. Never abort the capture over
|
|
649
|
-
// this — the query token path still exists.
|
|
650
715
|
try {
|
|
651
716
|
await page.context().addCookies([
|
|
652
717
|
{
|
|
@@ -661,81 +726,170 @@ async function captureDashboardPng(
|
|
|
661
726
|
errorMessage(err),
|
|
662
727
|
);
|
|
663
728
|
}
|
|
664
|
-
|
|
665
|
-
// Bounded diagnostics collectors so a failed wait carries evidence of
|
|
666
|
-
// wrong-page/wedged-renderer/auth-bounce instead of a bare timeout.
|
|
667
|
-
const consoleErrors: string[] = [];
|
|
668
|
-
page.on("console", (msg: any) => {
|
|
669
|
-
if (msg.type() !== "error") return;
|
|
670
|
-
if (consoleErrors.length >= DIAGNOSTICS_COLLECTOR_LIMIT) return;
|
|
671
|
-
consoleErrors.push(msg.text().slice(0, 160));
|
|
672
|
-
});
|
|
673
|
-
const failedRequests: string[] = [];
|
|
674
|
-
page.on("requestfailed", (req: any) => {
|
|
675
|
-
if (failedRequests.length >= DIAGNOSTICS_COLLECTOR_LIMIT) return;
|
|
676
|
-
failedRequests.push(
|
|
677
|
-
`${req.method()} ${req.url().slice(0, 120)}: ${req.failure()?.errorText ?? "failed"}`,
|
|
678
|
-
);
|
|
679
|
-
});
|
|
680
|
-
page.on("response", (res: any) => {
|
|
681
|
-
if (res.status() < 400) return;
|
|
682
|
-
if (failedRequests.length >= DIAGNOSTICS_COLLECTOR_LIMIT) return;
|
|
683
|
-
failedRequests.push(
|
|
684
|
-
`${res.request().method()} ${res.url().slice(0, 120)}: HTTP ${res.status()}`,
|
|
685
|
-
);
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
page.setDefaultTimeout(timeout);
|
|
729
|
+
page.setDefaultTimeout(boundedStageTimeout(timeout, deadlineAt));
|
|
689
730
|
await page.emulateMedia({ media: "screen", colorScheme: "light" });
|
|
690
|
-
await page.addInitScript(() =>
|
|
691
|
-
window.localStorage.setItem("theme", "light")
|
|
692
|
-
|
|
693
|
-
|
|
731
|
+
await page.addInitScript(() =>
|
|
732
|
+
window.localStorage.setItem("theme", "light"),
|
|
733
|
+
);
|
|
734
|
+
captureStage = "navigating to the report chunk";
|
|
735
|
+
await page.goto(screenshotUrl, {
|
|
694
736
|
waitUntil: "domcontentloaded",
|
|
695
|
-
timeout,
|
|
737
|
+
timeout: boundedStageTimeout(timeout, deadlineAt),
|
|
696
738
|
});
|
|
697
|
-
|
|
698
|
-
captureStage = "waiting for the report surface";
|
|
699
739
|
const capture = page.locator("[data-dashboard-report-capture]");
|
|
740
|
+
captureStage = "waiting for the report chunk surface";
|
|
700
741
|
try {
|
|
701
|
-
await capture.waitFor({
|
|
742
|
+
await capture.waitFor({
|
|
743
|
+
state: "visible",
|
|
744
|
+
timeout: boundedStageTimeout(timeout, deadlineAt),
|
|
745
|
+
});
|
|
702
746
|
} catch (err) {
|
|
703
|
-
|
|
747
|
+
const diagnostics = errorMessage(
|
|
704
748
|
await collectPageDiagnostics(page, consoleErrors, failedRequests),
|
|
705
749
|
);
|
|
706
|
-
throw new Error(`${errorMessage(err)}; ${
|
|
750
|
+
throw new Error(`${errorMessage(err)}; ${diagnostics}`);
|
|
707
751
|
}
|
|
708
|
-
captureStage = "waiting for
|
|
709
|
-
await waitForDashboardReportReady(
|
|
710
|
-
|
|
752
|
+
captureStage = "waiting for report chunk queries";
|
|
753
|
+
await waitForDashboardReportReady(
|
|
754
|
+
page,
|
|
755
|
+
boundedStageTimeout(attempt.readyTimeout ?? timeout, deadlineAt),
|
|
756
|
+
);
|
|
757
|
+
captureStage = "validating the report chunk panels";
|
|
758
|
+
await assertDashboardReportPanelWindow(page, expectedPanelIds);
|
|
759
|
+
captureStage = "rendering lazy report chunk panels";
|
|
711
760
|
await scrollDashboardForLazyRendering(page);
|
|
712
|
-
captureStage = "waiting for lazy
|
|
761
|
+
captureStage = "waiting for lazy report chunk panels";
|
|
713
762
|
await waitForDashboardReportReady(
|
|
714
763
|
page,
|
|
715
|
-
|
|
764
|
+
boundedStageTimeout(
|
|
765
|
+
attempt.secondReadyTimeout ?? secondReadyTimeoutMs(),
|
|
766
|
+
deadlineAt,
|
|
767
|
+
),
|
|
716
768
|
);
|
|
717
|
-
|
|
718
|
-
|
|
769
|
+
captureStage = "revalidating the report chunk panels";
|
|
770
|
+
await assertDashboardReportPanelWindow(page, expectedPanelIds);
|
|
771
|
+
captureStage = "sizing the report chunk";
|
|
719
772
|
await fitViewportWidthToDashboardCapture(page, capture, attempt.viewport);
|
|
720
773
|
await scaleDashboardCapture(page, attempt.captureScale);
|
|
721
774
|
await capture.scrollIntoViewIfNeeded();
|
|
722
|
-
captureStage = "rasterizing the
|
|
775
|
+
captureStage = "rasterizing the report chunk PNG";
|
|
776
|
+
page.setDefaultTimeout(boundedStageTimeout(timeout, deadlineAt));
|
|
723
777
|
const image = await capture.screenshot({
|
|
724
778
|
type: "png",
|
|
725
779
|
animations: "disabled",
|
|
726
780
|
});
|
|
727
|
-
if (!image?.length)
|
|
728
|
-
throw new Error("Dashboard screenshot was empty");
|
|
729
|
-
}
|
|
781
|
+
if (!image?.length) throw new Error("Dashboard screenshot was empty");
|
|
730
782
|
return Buffer.from(image);
|
|
783
|
+
} catch (err) {
|
|
784
|
+
throw new Error(`${captureStage}: ${errorMessage(err)}`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async function captureDashboardPngChunks(
|
|
789
|
+
sub: DashboardReportSubscription,
|
|
790
|
+
snapshot: ReportSnapshot,
|
|
791
|
+
): Promise<Buffer[]> {
|
|
792
|
+
const offsets = reportChunkOffsets(snapshot.panelIds.length);
|
|
793
|
+
if (offsets.length > MAX_DASHBOARD_REPORT_CHUNKS) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`complete dashboard requires ${offsets.length} image chunks; email delivery supports at most ${MAX_DASHBOARD_REPORT_CHUNKS}`,
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
const serverless = isServerlessBrowserRuntime();
|
|
799
|
+
const attempt: DashboardScreenshotAttempt = {
|
|
800
|
+
label: "chunked",
|
|
801
|
+
viewport: { width: 1200, height: 1400 },
|
|
802
|
+
captureScale: 0.7,
|
|
803
|
+
...(serverless
|
|
804
|
+
? {
|
|
805
|
+
readyTimeout: 35_000,
|
|
806
|
+
secondReadyTimeout: 15_000,
|
|
807
|
+
totalTimeout: SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
|
|
808
|
+
}
|
|
809
|
+
: {}),
|
|
810
|
+
};
|
|
811
|
+
let browser: any;
|
|
812
|
+
let cleanup = async () => {};
|
|
813
|
+
let launchPromise: Promise<LaunchedScreenshotBrowser> | undefined;
|
|
814
|
+
let launchTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
815
|
+
let attemptTimedOut = false;
|
|
816
|
+
const deadlineAt = attempt.totalTimeout
|
|
817
|
+
? Date.now() + attempt.totalTimeout
|
|
818
|
+
: undefined;
|
|
819
|
+
const attemptTimeout = attempt.totalTimeout
|
|
820
|
+
? setTimeout(() => {
|
|
821
|
+
attemptTimedOut = true;
|
|
822
|
+
if (browser) void browser.close().catch(() => {});
|
|
823
|
+
}, attempt.totalTimeout)
|
|
824
|
+
: null;
|
|
825
|
+
try {
|
|
826
|
+
launchPromise = launchScreenshotBrowser(attempt.viewport);
|
|
827
|
+
const launched = attempt.totalTimeout
|
|
828
|
+
? await Promise.race([
|
|
829
|
+
launchPromise,
|
|
830
|
+
new Promise<never>((_, reject) => {
|
|
831
|
+
launchTimeout = setTimeout(
|
|
832
|
+
() =>
|
|
833
|
+
reject(
|
|
834
|
+
new Error(
|
|
835
|
+
`${attempt.label} browser launch exceeded ${attempt.totalTimeout}ms`,
|
|
836
|
+
),
|
|
837
|
+
),
|
|
838
|
+
attempt.totalTimeout,
|
|
839
|
+
);
|
|
840
|
+
}),
|
|
841
|
+
])
|
|
842
|
+
: await launchPromise;
|
|
843
|
+
browser = launched.browser;
|
|
844
|
+
cleanup = launched.cleanup;
|
|
845
|
+
const images: Buffer[] = [];
|
|
846
|
+
let attachmentBytes = 0;
|
|
847
|
+
for (const offset of offsets) {
|
|
848
|
+
if (attemptTimedOut)
|
|
849
|
+
throw new Error(`capture exceeded ${attempt.totalTimeout}ms`);
|
|
850
|
+
const page = await runWithinCaptureDeadline(
|
|
851
|
+
() => launched.newPage(),
|
|
852
|
+
deadlineAt,
|
|
853
|
+
);
|
|
854
|
+
try {
|
|
855
|
+
const { token, url } = screenshotUrlForChunk(sub, snapshot, offset);
|
|
856
|
+
const expectedPanelIds = snapshot.panelIds.slice(
|
|
857
|
+
offset,
|
|
858
|
+
offset + REPORT_PANEL_CHUNK_SIZE,
|
|
859
|
+
);
|
|
860
|
+
const image = await runWithinCaptureDeadline(
|
|
861
|
+
() =>
|
|
862
|
+
captureDashboardChunk(
|
|
863
|
+
page,
|
|
864
|
+
attempt,
|
|
865
|
+
token,
|
|
866
|
+
url,
|
|
867
|
+
expectedPanelIds,
|
|
868
|
+
deadlineAt,
|
|
869
|
+
),
|
|
870
|
+
deadlineAt,
|
|
871
|
+
);
|
|
872
|
+
attachmentBytes += image.length;
|
|
873
|
+
if (attachmentBytes > MAX_DASHBOARD_REPORT_ATTACHMENT_BYTES) {
|
|
874
|
+
throw new Error(
|
|
875
|
+
`complete dashboard images total ${attachmentBytes} bytes; email delivery supports at most ${MAX_DASHBOARD_REPORT_ATTACHMENT_BYTES} bytes`,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
images.push(image);
|
|
879
|
+
} finally {
|
|
880
|
+
await runBoundedBrowserCleanup("Failed to close screenshot page", () =>
|
|
881
|
+
page.close(),
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return images;
|
|
731
886
|
} catch (err) {
|
|
732
887
|
if (attemptTimedOut) {
|
|
733
888
|
throw new Error(
|
|
734
|
-
`${attempt.label} capture exceeded ${attempt.totalTimeout}ms
|
|
735
|
-
(lastDiagnostics ? `; ${lastDiagnostics}` : ""),
|
|
889
|
+
`${memoryDiagnostics()} ${attempt.label} capture exceeded ${attempt.totalTimeout}ms: ${errorMessage(err)}`,
|
|
736
890
|
);
|
|
737
891
|
}
|
|
738
|
-
throw new Error(`${
|
|
892
|
+
throw new Error(`${attempt.label}: ${errorMessage(err)}`);
|
|
739
893
|
} finally {
|
|
740
894
|
if (attemptTimeout) clearTimeout(attemptTimeout);
|
|
741
895
|
if (launchTimeout) clearTimeout(launchTimeout);
|
|
@@ -751,9 +905,7 @@ async function captureDashboardPng(
|
|
|
751
905
|
lateBrowser.cleanup,
|
|
752
906
|
);
|
|
753
907
|
},
|
|
754
|
-
() => {
|
|
755
|
-
// The launch rejection was already handled by the race above.
|
|
756
|
-
},
|
|
908
|
+
() => {},
|
|
757
909
|
);
|
|
758
910
|
}
|
|
759
911
|
if (browser) {
|
|
@@ -773,91 +925,36 @@ function errorMessage(err: unknown): string {
|
|
|
773
925
|
);
|
|
774
926
|
}
|
|
775
927
|
|
|
776
|
-
function storedAttemptError(message: string): string {
|
|
777
|
-
const normalized = message.replace(/\s+/g, " ").trim();
|
|
778
|
-
return normalized.length > 400 ? `${normalized.slice(0, 399)}…` : normalized;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
928
|
async function captureDashboardPngWithFallback(
|
|
782
929
|
sub: DashboardReportSubscription,
|
|
783
930
|
snapshot: ReportSnapshot,
|
|
784
|
-
options?: { includeLimitedFallback?: boolean },
|
|
785
931
|
): Promise<{
|
|
786
|
-
|
|
787
|
-
mode: "full" | "
|
|
932
|
+
pngs: Buffer[] | null;
|
|
933
|
+
mode: "full" | "none";
|
|
788
934
|
error?: string;
|
|
789
935
|
}> {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
{
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
captureScale: 0.85,
|
|
796
|
-
...(serverless
|
|
797
|
-
? {
|
|
798
|
-
readyTimeout: 45_000,
|
|
799
|
-
secondReadyTimeout: 25_000,
|
|
800
|
-
totalTimeout: SERVERLESS_FULL_ATTEMPT_TIMEOUT_MS,
|
|
801
|
-
}
|
|
802
|
-
: {}),
|
|
803
|
-
},
|
|
804
|
-
{
|
|
805
|
-
label: "full-lightweight",
|
|
806
|
-
viewport: { width: 1200, height: 1400 },
|
|
807
|
-
captureScale: 0.7,
|
|
808
|
-
...(serverless
|
|
809
|
-
? {
|
|
810
|
-
readyTimeout: 35_000,
|
|
811
|
-
secondReadyTimeout: 15_000,
|
|
812
|
-
totalTimeout: SERVERLESS_LIGHTWEIGHT_ATTEMPT_TIMEOUT_MS,
|
|
813
|
-
}
|
|
814
|
-
: {}),
|
|
815
|
-
},
|
|
816
|
-
];
|
|
817
|
-
if (options?.includeLimitedFallback) {
|
|
818
|
-
// Only added on the final sweep after the 1-hour retry window, when the
|
|
819
|
-
// alternative is a no-image fallback email. All three serverless attempt
|
|
820
|
-
// totalTimeouts sum to 225s, leaving room for bounded cleanup and email
|
|
821
|
-
// delivery under the 300s Netlify background-function timeout.
|
|
822
|
-
attempts.push({
|
|
823
|
-
label: "limited",
|
|
824
|
-
viewport: { width: 1200, height: 1400 },
|
|
825
|
-
captureScale: 0.7,
|
|
826
|
-
reportPanelLimit: 8,
|
|
827
|
-
...(serverless
|
|
828
|
-
? {
|
|
829
|
-
readyTimeout: 25_000,
|
|
830
|
-
secondReadyTimeout: 10_000,
|
|
831
|
-
totalTimeout: 45_000,
|
|
832
|
-
}
|
|
833
|
-
: {}),
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
const errors: string[] = [];
|
|
837
|
-
|
|
838
|
-
for (const attempt of attempts) {
|
|
839
|
-
try {
|
|
840
|
-
const png = await captureDashboardPng(sub, snapshot, attempt);
|
|
841
|
-
return {
|
|
842
|
-
png,
|
|
843
|
-
mode: attempt.label,
|
|
844
|
-
...(errors.length ? { error: errors.join(" | ") } : {}),
|
|
845
|
-
};
|
|
846
|
-
} catch (err) {
|
|
847
|
-
const attemptError = errorMessage(err);
|
|
848
|
-
errors.push(`${attempt.label}: ${storedAttemptError(attemptError)}`);
|
|
849
|
-
console.error(
|
|
850
|
-
`[dashboard-report] ${attempt.label} screenshot failed for subscription ${sub.id}:`,
|
|
851
|
-
attemptError,
|
|
936
|
+
try {
|
|
937
|
+
const pngs = await captureDashboardPngChunks(sub, snapshot);
|
|
938
|
+
if (isServerlessBrowserRuntime()) {
|
|
939
|
+
console.info(
|
|
940
|
+
`[dashboard-report] complete chunked capture succeeded: chunks=${pngs.length} ${memoryDiagnostics()}`,
|
|
852
941
|
);
|
|
853
942
|
}
|
|
943
|
+
return {
|
|
944
|
+
pngs,
|
|
945
|
+
mode: "full",
|
|
946
|
+
};
|
|
947
|
+
} catch (err) {
|
|
948
|
+
const rawError = errorMessage(err).replace(/\s+/g, " ").trim();
|
|
949
|
+
const error = rawError.includes("lambdaMemoryMb=")
|
|
950
|
+
? rawError
|
|
951
|
+
: `${rawError}; ${memoryDiagnostics()}`;
|
|
952
|
+
console.error(
|
|
953
|
+
`[dashboard-report] complete chunked screenshot failed for subscription ${sub.id}:`,
|
|
954
|
+
error,
|
|
955
|
+
);
|
|
956
|
+
return { pngs: null, mode: "none", error };
|
|
854
957
|
}
|
|
855
|
-
|
|
856
|
-
return {
|
|
857
|
-
png: null,
|
|
858
|
-
mode: "none",
|
|
859
|
-
...(errors.length ? { error: errors.join(" | ") } : {}),
|
|
860
|
-
};
|
|
861
958
|
}
|
|
862
959
|
|
|
863
960
|
function reportDate(snapshot: ReportSnapshot): string {
|
|
@@ -872,7 +969,7 @@ function renderReportEmailHtml(
|
|
|
872
969
|
snapshot: ReportSnapshot,
|
|
873
970
|
options: {
|
|
874
971
|
screenshotAttached: boolean;
|
|
875
|
-
|
|
972
|
+
screenshotCount: number;
|
|
876
973
|
},
|
|
877
974
|
): string {
|
|
878
975
|
const title = escapeHtml(snapshot.title);
|
|
@@ -880,18 +977,18 @@ function renderReportEmailHtml(
|
|
|
880
977
|
const reportSettingsUrl = escapeHtml(snapshot.reportSettingsUrl);
|
|
881
978
|
const date = escapeHtml(reportDate(snapshot));
|
|
882
979
|
const screenshotBlock = options.screenshotAttached
|
|
883
|
-
?
|
|
884
|
-
|
|
885
|
-
|
|
980
|
+
? Array.from(
|
|
981
|
+
{ length: options.screenshotCount },
|
|
982
|
+
(
|
|
983
|
+
_,
|
|
984
|
+
index,
|
|
985
|
+
) => `<a href="${dashboardUrl}" style="display:block;text-decoration:none;">
|
|
986
|
+
<img src="cid:${DASHBOARD_REPORT_CID}-${index + 1}" alt="${title}${options.screenshotCount > 1 ? ` (part ${index + 1})` : ""}" width="100%" style="display:block;width:100%;max-width:1280px;height:auto;border:0;outline:0;border-radius:0;" />
|
|
987
|
+
</a>`,
|
|
988
|
+
).join("\n")
|
|
886
989
|
: `<div style="margin:18px 0;padding:14px 16px;border:1px solid #e5e7eb;border-radius:8px;background:#f9fafb;color:#374151;font-size:14px;line-height:1.5;">
|
|
887
990
|
The dashboard image was unavailable for this run. Open the live dashboard to view the latest report.
|
|
888
991
|
</div>`;
|
|
889
|
-
const limitedScreenshotNotice =
|
|
890
|
-
options.screenshotMode === "limited"
|
|
891
|
-
? `<div style="margin:12px 0 0;padding:12px 14px;border:1px solid #f3c46b;border-radius:8px;background:#fff8e6;color:#6b4f14;font-size:13px;line-height:1.45;">
|
|
892
|
-
This is a limited fallback image and may omit some dashboard panels. <a href="${dashboardUrl}" style="color:#2563eb;text-decoration:none;">Open the full dashboard</a> to see every panel.
|
|
893
|
-
</div>`
|
|
894
|
-
: "";
|
|
895
992
|
|
|
896
993
|
return `<!doctype html>
|
|
897
994
|
<html>
|
|
@@ -900,7 +997,6 @@ function renderReportEmailHtml(
|
|
|
900
997
|
Here's your report of <a href="${dashboardUrl}" style="color:#2563eb;text-decoration:none;">${title}</a> for ${date}
|
|
901
998
|
</p>
|
|
902
999
|
${screenshotBlock}
|
|
903
|
-
${limitedScreenshotNotice}
|
|
904
1000
|
<p style="margin:18px 0 0;color:#525866;font-size:13px;line-height:1.45;">
|
|
905
1001
|
<a href="${dashboardUrl}" style="color:#2563eb;text-decoration:none;">Open dashboard</a>
|
|
906
1002
|
<span style="color:#9ca3af;"> · </span>
|
|
@@ -914,7 +1010,6 @@ function renderReportText(
|
|
|
914
1010
|
snapshot: ReportSnapshot,
|
|
915
1011
|
options: {
|
|
916
1012
|
screenshotAttached: boolean;
|
|
917
|
-
screenshotMode: "full" | "full-lightweight" | "limited" | "none";
|
|
918
1013
|
},
|
|
919
1014
|
): string {
|
|
920
1015
|
const lines = [
|
|
@@ -926,21 +1021,17 @@ function renderReportText(
|
|
|
926
1021
|
if (!options.screenshotAttached) {
|
|
927
1022
|
lines.push("Dashboard image unavailable for this run.");
|
|
928
1023
|
}
|
|
929
|
-
if (options.screenshotMode === "limited") {
|
|
930
|
-
lines.push(
|
|
931
|
-
`This is a limited fallback image and may omit some dashboard panels. Open the full dashboard: ${snapshot.dashboardUrl}`,
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
1024
|
return lines.join("\n");
|
|
935
1025
|
}
|
|
936
1026
|
|
|
937
|
-
function reportFilename(title: string): string {
|
|
1027
|
+
function reportFilename(title: string, part: number, total: number): string {
|
|
938
1028
|
const slug = title
|
|
939
1029
|
.toLowerCase()
|
|
940
1030
|
.replace(/[^a-z0-9]+/g, "-")
|
|
941
1031
|
.replace(/^-|-$/g, "")
|
|
942
1032
|
.slice(0, 64);
|
|
943
|
-
|
|
1033
|
+
const suffix = total > 1 ? `-part-${part}` : "";
|
|
1034
|
+
return `${slug || "dashboard"}-report${suffix}.png`;
|
|
944
1035
|
}
|
|
945
1036
|
|
|
946
1037
|
export async function sendDashboardReportSubscription(
|
|
@@ -948,71 +1039,70 @@ export async function sendDashboardReportSubscription(
|
|
|
948
1039
|
options: {
|
|
949
1040
|
requireScreenshot?: boolean;
|
|
950
1041
|
skipEmailWithoutScreenshot?: boolean;
|
|
951
|
-
allowLimitedFallback?: boolean;
|
|
952
1042
|
} = {},
|
|
953
1043
|
): Promise<{
|
|
954
1044
|
dashboardUrl: string;
|
|
955
1045
|
recipientCount: number;
|
|
956
1046
|
screenshotAttached: boolean;
|
|
957
|
-
screenshotMode: "full" | "
|
|
1047
|
+
screenshotMode: "full" | "none";
|
|
958
1048
|
screenshotError?: string;
|
|
959
1049
|
emailsSent: boolean;
|
|
960
1050
|
}> {
|
|
1051
|
+
const recipients = normalizeDashboardReportRecipients(sub.recipients);
|
|
961
1052
|
const snapshot = await collectReportSnapshot(sub);
|
|
962
|
-
const capture = await captureDashboardPngWithFallback(sub, snapshot
|
|
963
|
-
|
|
964
|
-
});
|
|
965
|
-
if (!capture.png && options.requireScreenshot) {
|
|
1053
|
+
const capture = await captureDashboardPngWithFallback(sub, snapshot);
|
|
1054
|
+
if (!capture.pngs && options.requireScreenshot) {
|
|
966
1055
|
throw new Error(
|
|
967
1056
|
capture.error
|
|
968
1057
|
? `Dashboard screenshot unavailable: ${capture.error}`
|
|
969
1058
|
: "Dashboard screenshot unavailable",
|
|
970
1059
|
);
|
|
971
1060
|
}
|
|
972
|
-
if (!capture.
|
|
1061
|
+
if (!capture.pngs && options.skipEmailWithoutScreenshot) {
|
|
973
1062
|
return {
|
|
974
1063
|
dashboardUrl: snapshot.dashboardUrl,
|
|
975
|
-
recipientCount:
|
|
1064
|
+
recipientCount: recipients.length,
|
|
976
1065
|
screenshotAttached: false,
|
|
977
1066
|
screenshotMode: capture.mode,
|
|
978
1067
|
emailsSent: false,
|
|
979
1068
|
...(capture.error ? { screenshotError: capture.error } : {}),
|
|
980
1069
|
};
|
|
981
1070
|
}
|
|
982
|
-
const screenshotAttached = Boolean(capture.
|
|
1071
|
+
const screenshotAttached = Boolean(capture.pngs?.length);
|
|
983
1072
|
const html = renderReportEmailHtml(snapshot, {
|
|
984
1073
|
screenshotAttached,
|
|
985
|
-
|
|
1074
|
+
screenshotCount: capture.pngs?.length ?? 0,
|
|
986
1075
|
});
|
|
987
1076
|
const text = renderReportText(snapshot, {
|
|
988
1077
|
screenshotAttached,
|
|
989
|
-
screenshotMode: capture.mode,
|
|
990
1078
|
});
|
|
991
1079
|
const subject = `Daily dashboard: ${snapshot.title}`;
|
|
992
1080
|
|
|
993
|
-
for (const to of
|
|
1081
|
+
for (const to of recipients) {
|
|
994
1082
|
await sendEmail({
|
|
995
1083
|
to,
|
|
996
1084
|
subject,
|
|
997
1085
|
html,
|
|
998
1086
|
text,
|
|
999
|
-
attachments: capture.
|
|
1000
|
-
?
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1087
|
+
attachments: capture.pngs
|
|
1088
|
+
? capture.pngs.map((png, index) => ({
|
|
1089
|
+
filename: reportFilename(
|
|
1090
|
+
snapshot.title,
|
|
1091
|
+
index + 1,
|
|
1092
|
+
capture.pngs!.length,
|
|
1093
|
+
),
|
|
1094
|
+
content: png,
|
|
1095
|
+
contentType: "image/png",
|
|
1096
|
+
contentId: `${DASHBOARD_REPORT_CID}-${index + 1}`,
|
|
1097
|
+
disposition: "inline",
|
|
1098
|
+
}))
|
|
1009
1099
|
: undefined,
|
|
1010
1100
|
});
|
|
1011
1101
|
}
|
|
1012
1102
|
|
|
1013
1103
|
return {
|
|
1014
1104
|
dashboardUrl: snapshot.dashboardUrl,
|
|
1015
|
-
recipientCount:
|
|
1105
|
+
recipientCount: recipients.length,
|
|
1016
1106
|
screenshotAttached,
|
|
1017
1107
|
screenshotMode: capture.mode,
|
|
1018
1108
|
emailsSent: true,
|