@agent-native/core 0.119.1 → 0.119.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +15 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +4 -0
- package/corpus/core/src/agent/production-agent.ts +1 -0
- package/corpus/core/src/client/AssistantChat.tsx +11 -50
- package/corpus/core/src/client/ErrorBoundary.tsx +25 -19
- package/corpus/core/src/client/agent-page/AgentTabFrame.tsx +1 -1
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +23 -0
- package/corpus/core/src/client/components/ui/message-scroller.tsx +1 -9
- package/corpus/core/src/client/resources/ResourceTree.tsx +3 -3
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +64 -60
- package/corpus/core/src/client/settings/AccountSettingsCard.tsx +228 -0
- package/corpus/core/src/client/settings/SecretsSection.tsx +7 -5
- package/corpus/core/src/client/settings/SettingsPanel.tsx +67 -244
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +88 -21
- package/corpus/core/src/client/settings/agent-settings-search.ts +0 -2
- package/corpus/core/src/client/settings/index.ts +7 -0
- package/corpus/core/src/client/vite-dev-recovery-script.ts +18 -1
- package/corpus/core/src/db/client.ts +65 -15
- package/corpus/core/src/db/index.ts +2 -0
- package/corpus/core/src/localization/default-messages.ts +1 -0
- package/corpus/core/src/server/email.ts +33 -1
- package/corpus/core/src/styles/agent-native.css +23 -0
- package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/core/src/templates/default/app/routes/settings.tsx +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/core/src/vite/client.ts +76 -8
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +3 -1
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +9 -13
- package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +2 -2
- package/corpus/templates/analytics/app/global.css +0 -5
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +0 -13
- package/corpus/templates/analytics/app/lib/sql-query.ts +15 -2
- package/corpus/templates/analytics/app/pages/Settings.tsx +2 -24
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +6 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +4 -0
- package/corpus/templates/analytics/app/pages/settings/settings-search.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-07-23-ask-analytics-is-better-centered-with-a-quieter-chat-rail-an.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-23-daily-dashboard-emails-load-read-heavy-panels-reliably.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +2 -2
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +39 -5
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +186 -62
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +3 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +18 -2
- package/corpus/templates/analytics/shared/dashboard-report-timeouts.ts +3 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -4
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/assets/app/global.css +0 -5
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +0 -22
- package/corpus/templates/assets/app/routes/settings.tsx +2 -0
- package/corpus/templates/assets/changelog/2026-07-23-create-and-chat-surfaces-are-better-centered-with-quieter-ch.md +6 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/brain/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/brain/app/global.css +0 -9
- package/corpus/templates/brain/app/hooks/use-navigation-state.ts +0 -12
- package/corpus/templates/brain/app/i18n-data.ts +1 -0
- package/corpus/templates/brain/app/lib/settings-navigation.ts +1 -1
- package/corpus/templates/brain/app/routes/settings.tsx +21 -22
- package/corpus/templates/brain/changelog/2026-07-23-ask-brain-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/calendar/app/pages/Settings.tsx +2 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/chat/app/hooks/use-navigation-state.ts +0 -15
- package/corpus/templates/chat/app/routes/settings.tsx +2 -0
- package/corpus/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +2 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +2 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/design/app/routes/settings.tsx +2 -0
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/dispatch/app/routes/settings.tsx +2 -0
- package/corpus/templates/dispatch/changelog/2026-07-23-quieter-progressive-disclosure.md +6 -0
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/forms/app/global.css +0 -5
- package/corpus/templates/forms/app/hooks/use-navigation-state.ts +0 -7
- package/corpus/templates/forms/app/root.tsx +1 -7
- package/corpus/templates/forms/app/routes/_app.settings.tsx +2 -0
- package/corpus/templates/forms/changelog/2026-07-23-ask-forms-is-better-centered-with-quieter-chat-history-and-a.md +6 -0
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/macros/app/routes/settings.tsx +2 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +3 -1
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/plan/app/components/layout/Layout.tsx +2 -2
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +1 -13
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +0 -4
- package/corpus/templates/plan/app/routes/settings.tsx +2 -0
- package/corpus/templates/plan/changelog/2026-07-23-ask-plan-is-now-a-clean-full-page-chat-surface-with-better-c.md +6 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/templates/slides/app/routes/settings.tsx +2 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +28 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/chat-history.css +2 -2
- package/corpus/toolkit/src/composer/PromptComposer.tsx +0 -31
- package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +7 -2
- package/corpus/toolkit/src/styles.css +183 -36
- package/dist/a2a/client.d.ts +2 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +2 -0
- package/dist/a2a/client.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +1 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +11 -42
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/ErrorBoundary.d.ts.map +1 -1
- package/dist/client/ErrorBoundary.js +3 -2
- package/dist/client/ErrorBoundary.js.map +1 -1
- package/dist/client/agent-page/AgentTabFrame.js +1 -1
- package/dist/client/agent-page/AgentTabFrame.js.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.d.ts.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +13 -1
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/components/ui/message-scroller.d.ts.map +1 -1
- package/dist/client/components/ui/message-scroller.js +2 -2
- package/dist/client/components/ui/message-scroller.js.map +1 -1
- package/dist/client/resources/ResourceTree.js +2 -2
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +11 -11
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/client/settings/AccountSettingsCard.d.ts +9 -0
- package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -0
- package/dist/client/settings/AccountSettingsCard.js +78 -0
- package/dist/client/settings/AccountSettingsCard.js.map +1 -0
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +2 -2
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +3 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +25 -77
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts +5 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +35 -9
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
- package/dist/client/settings/agent-settings-search.js +0 -2
- package/dist/client/settings/agent-settings-search.js.map +1 -1
- package/dist/client/settings/index.d.ts +2 -1
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +2 -1
- package/dist/client/settings/index.js.map +1 -1
- package/dist/client/vite-dev-recovery-script.d.ts.map +1 -1
- package/dist/client/vite-dev-recovery-script.js +18 -1
- package/dist/client/vite-dev-recovery-script.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts +18 -8
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +34 -11
- package/dist/db/client.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +1 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/email.d.ts +1 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +29 -1
- package/dist/server/email.js.map +1 -1
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/styles/agent-native.css +23 -0
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
- package/dist/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +1 -1
- package/dist/templates/chat/app/hooks/use-navigation-state.ts +0 -15
- package/dist/templates/chat/app/routes/settings.tsx +2 -0
- package/dist/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
- package/dist/templates/default/app/routes/settings.tsx +2 -0
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +68 -6
- package/dist/vite/client.js.map +1 -1
- package/package.json +2 -2
- package/src/a2a/client.ts +4 -0
- package/src/agent/production-agent.ts +1 -0
- package/src/client/AssistantChat.tsx +11 -50
- package/src/client/ErrorBoundary.tsx +25 -19
- package/src/client/agent-page/AgentTabFrame.tsx +1 -1
- package/src/client/agent-page/AgentTabsPage.tsx +23 -0
- package/src/client/components/ui/message-scroller.tsx +1 -9
- package/src/client/resources/ResourceTree.tsx +3 -3
- package/src/client/resources/ResourcesPanel.tsx +64 -60
- package/src/client/settings/AccountSettingsCard.tsx +228 -0
- package/src/client/settings/SecretsSection.tsx +7 -5
- package/src/client/settings/SettingsPanel.tsx +67 -244
- package/src/client/settings/SettingsTabsPage.tsx +88 -21
- package/src/client/settings/agent-settings-search.ts +0 -2
- package/src/client/settings/index.ts +7 -0
- package/src/client/vite-dev-recovery-script.ts +18 -1
- package/src/db/client.ts +65 -15
- package/src/db/index.ts +2 -0
- package/src/localization/default-messages.ts +1 -0
- package/src/server/email.ts +33 -1
- package/src/styles/agent-native.css +23 -0
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +28 -0
- package/src/templates/chat/app/components/layout/Layout.tsx +1 -1
- package/src/templates/chat/app/components/layout/Sidebar.tsx +1 -1
- package/src/templates/chat/app/hooks/use-navigation-state.ts +0 -15
- package/src/templates/chat/app/routes/settings.tsx +2 -0
- package/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +6 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +28 -0
- package/src/templates/default/app/routes/settings.tsx +2 -0
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +28 -0
- package/src/vite/client.ts +76 -8
|
@@ -9,10 +9,26 @@ import {
|
|
|
9
9
|
|
|
10
10
|
let running = false;
|
|
11
11
|
const DEFAULT_MAX_REPORTS_PER_SWEEP = 5;
|
|
12
|
+
const SERVERLESS_REPORT_DELIVERY_BUDGET_MS = 220_000;
|
|
13
|
+
|
|
14
|
+
async function persistDashboardReportResult(
|
|
15
|
+
...args: Parameters<typeof markDashboardReportResult>
|
|
16
|
+
): Promise<boolean> {
|
|
17
|
+
try {
|
|
18
|
+
await markDashboardReportResult(...args);
|
|
19
|
+
return true;
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(
|
|
22
|
+
`[dashboard-report] Failed to persist subscription ${args[0].id} result:`,
|
|
23
|
+
err instanceof Error ? err.message : String(err),
|
|
24
|
+
);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
12
28
|
|
|
13
29
|
function maxReportsPerSweep(): number {
|
|
30
|
+
if (process.env.NETLIFY === "true") return 1;
|
|
14
31
|
const raw = process.env.DASHBOARD_REPORT_SWEEP_LIMIT?.trim();
|
|
15
|
-
if (!raw && process.env.NETLIFY === "true") return 1;
|
|
16
32
|
if (!raw) return DEFAULT_MAX_REPORTS_PER_SWEEP;
|
|
17
33
|
const parsed = Number.parseInt(raw, 10);
|
|
18
34
|
return Number.isFinite(parsed) && parsed > 0
|
|
@@ -31,6 +47,10 @@ export async function runDashboardReportsOnce(): Promise<{
|
|
|
31
47
|
}> {
|
|
32
48
|
if (running) return { processed: 0, failed: 0, remaining: 0 };
|
|
33
49
|
running = true;
|
|
50
|
+
const deliveryDeadlineAt =
|
|
51
|
+
process.env.NETLIFY === "true"
|
|
52
|
+
? Date.now() + SERVERLESS_REPORT_DELIVERY_BUDGET_MS
|
|
53
|
+
: undefined;
|
|
34
54
|
let processed = 0;
|
|
35
55
|
let failed = 0;
|
|
36
56
|
let remaining = 0;
|
|
@@ -51,8 +71,21 @@ export async function runDashboardReportsOnce(): Promise<{
|
|
|
51
71
|
() =>
|
|
52
72
|
sendDashboardReportSubscription(sub, {
|
|
53
73
|
skipEmailWithoutScreenshot: retryAt !== null,
|
|
74
|
+
...(deliveryDeadlineAt ? { deadlineAt: deliveryDeadlineAt } : {}),
|
|
54
75
|
}),
|
|
55
76
|
);
|
|
77
|
+
if (result.screenshotMode === "partial" && result.emailsSent) {
|
|
78
|
+
failed++;
|
|
79
|
+
const message = result.screenshotError
|
|
80
|
+
? `Dashboard screenshot partially available: ${result.screenshotError}`
|
|
81
|
+
: "Dashboard screenshot partially available";
|
|
82
|
+
console.error(
|
|
83
|
+
`[dashboard-report] Subscription ${sub.id} sent with a partial screenshot:`,
|
|
84
|
+
message,
|
|
85
|
+
);
|
|
86
|
+
await persistDashboardReportResult(sub, "error", message);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
56
89
|
if (!result.screenshotAttached) {
|
|
57
90
|
const message = result.screenshotError
|
|
58
91
|
? `Dashboard screenshot unavailable: ${result.screenshotError}`
|
|
@@ -62,12 +95,13 @@ export async function runDashboardReportsOnce(): Promise<{
|
|
|
62
95
|
`[dashboard-report] Subscription ${sub.id} skipped sending without a screenshot, will retry:`,
|
|
63
96
|
message,
|
|
64
97
|
);
|
|
65
|
-
await
|
|
98
|
+
const persisted = await persistDashboardReportResult(
|
|
66
99
|
sub,
|
|
67
100
|
"error",
|
|
68
101
|
`${message} (retry scheduled)`,
|
|
69
102
|
{ nextRunAt: retryAt },
|
|
70
103
|
);
|
|
104
|
+
if (!persisted) failed++;
|
|
71
105
|
continue;
|
|
72
106
|
}
|
|
73
107
|
failed++;
|
|
@@ -75,10 +109,10 @@ export async function runDashboardReportsOnce(): Promise<{
|
|
|
75
109
|
`[dashboard-report] Subscription ${sub.id} sent without a screenshot:`,
|
|
76
110
|
message,
|
|
77
111
|
);
|
|
78
|
-
await
|
|
112
|
+
await persistDashboardReportResult(sub, "error", message);
|
|
79
113
|
continue;
|
|
80
114
|
}
|
|
81
|
-
await
|
|
115
|
+
if (!(await persistDashboardReportResult(sub, "success"))) failed++;
|
|
82
116
|
} catch (err: any) {
|
|
83
117
|
failed++;
|
|
84
118
|
const message = err?.message ?? String(err);
|
|
@@ -86,7 +120,7 @@ export async function runDashboardReportsOnce(): Promise<{
|
|
|
86
120
|
`[dashboard-report] Subscription ${sub.id} failed:`,
|
|
87
121
|
message,
|
|
88
122
|
);
|
|
89
|
-
await
|
|
123
|
+
await persistDashboardReportResult(sub, "error", message);
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
} finally {
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
FilterType,
|
|
25
25
|
SqlDashboardConfig,
|
|
26
26
|
} from "../../app/pages/adhoc/sql-dashboard/types";
|
|
27
|
+
import { DASHBOARD_REPORT_READY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
|
|
27
28
|
import {
|
|
28
29
|
getReportDashboard,
|
|
29
30
|
normalizeDashboardReportRecipients,
|
|
@@ -55,10 +56,12 @@ const DASHBOARD_REPORT_CID = "dashboard-report-snapshot";
|
|
|
55
56
|
const LOCAL_SCREENSHOT_TIMEOUT_MS = 90_000;
|
|
56
57
|
const SERVERLESS_SCREENSHOT_TIMEOUT_MS = 90_000;
|
|
57
58
|
const SERVERLESS_SECOND_READY_TIMEOUT_MS = 45_000;
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
const SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS = 210_000;
|
|
59
|
+
// Cap browser work separately from the sweep-level delivery deadline.
|
|
60
|
+
const SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS = 150_000;
|
|
61
61
|
const BROWSER_CLEANUP_TIMEOUT_MS = 10_000;
|
|
62
|
+
const DASHBOARD_REPORT_EMAIL_TIMEOUT_MS = 10_000;
|
|
63
|
+
const DASHBOARD_REPORT_EMAIL_OVERHEAD_RESERVE_MS = 5_000;
|
|
64
|
+
const MAX_CAPTURE_CLEANUP_RESERVE_MS = BROWSER_CLEANUP_TIMEOUT_MS * 3;
|
|
62
65
|
const SCREENSHOT_VIEWPORT_PADDING = 64;
|
|
63
66
|
const MAX_DASHBOARD_REPORT_CHUNKS = 10;
|
|
64
67
|
const MAX_DASHBOARD_REPORT_ATTACHMENT_BYTES = 14 * 1024 * 1024;
|
|
@@ -78,6 +81,14 @@ type LaunchedScreenshotBrowser = {
|
|
|
78
81
|
newPage: () => Promise<any>;
|
|
79
82
|
};
|
|
80
83
|
|
|
84
|
+
class DashboardReportPanelMismatchError extends Error {}
|
|
85
|
+
|
|
86
|
+
type DashboardPngCapture = {
|
|
87
|
+
pngs: Array<Buffer | null> | null;
|
|
88
|
+
mode: "full" | "partial" | "none";
|
|
89
|
+
error?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
81
92
|
function daysAgo(n: number): string {
|
|
82
93
|
const d = new Date();
|
|
83
94
|
d.setDate(d.getDate() - n);
|
|
@@ -387,6 +398,33 @@ async function runWithinCaptureDeadline<T>(
|
|
|
387
398
|
}
|
|
388
399
|
}
|
|
389
400
|
|
|
401
|
+
async function runWithinReportDeadline<T>(
|
|
402
|
+
label: string,
|
|
403
|
+
operation: () => Promise<T>,
|
|
404
|
+
deadlineAt?: number,
|
|
405
|
+
): Promise<T> {
|
|
406
|
+
if (!deadlineAt) return operation();
|
|
407
|
+
const timeoutMs = deadlineAt - Date.now();
|
|
408
|
+
if (timeoutMs <= 0)
|
|
409
|
+
throw new Error(`${label} exceeded the report delivery deadline`);
|
|
410
|
+
|
|
411
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
412
|
+
try {
|
|
413
|
+
return await Promise.race([
|
|
414
|
+
operation(),
|
|
415
|
+
new Promise<never>((_, reject) => {
|
|
416
|
+
timeout = setTimeout(
|
|
417
|
+
() =>
|
|
418
|
+
reject(new Error(`${label} exceeded the report delivery deadline`)),
|
|
419
|
+
timeoutMs,
|
|
420
|
+
);
|
|
421
|
+
}),
|
|
422
|
+
]);
|
|
423
|
+
} finally {
|
|
424
|
+
if (timeout) clearTimeout(timeout);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
390
428
|
function positiveIntEnv(name: string): number | null {
|
|
391
429
|
const raw = process.env[name]?.trim();
|
|
392
430
|
if (!raw) return null;
|
|
@@ -397,6 +435,8 @@ function positiveIntEnv(name: string): number | null {
|
|
|
397
435
|
async function waitForDashboardReportReady(
|
|
398
436
|
page: any,
|
|
399
437
|
timeout: number,
|
|
438
|
+
consoleErrors: string[] = [],
|
|
439
|
+
failedRequests: string[] = [],
|
|
400
440
|
): Promise<boolean> {
|
|
401
441
|
try {
|
|
402
442
|
await page.waitForFunction(
|
|
@@ -440,9 +480,13 @@ async function waitForDashboardReportReady(
|
|
|
440
480
|
};
|
|
441
481
|
})()`)
|
|
442
482
|
.catch(() => null);
|
|
483
|
+
const captureDiagnostics = { consoleErrors, failedRequests };
|
|
443
484
|
const message = detail
|
|
444
|
-
? `${err?.message ?? String(err)}; dashboard state: ${JSON.stringify(
|
|
445
|
-
|
|
485
|
+
? `${err?.message ?? String(err)}; dashboard state: ${JSON.stringify({
|
|
486
|
+
...detail,
|
|
487
|
+
...captureDiagnostics,
|
|
488
|
+
})}`
|
|
489
|
+
: `${err?.message ?? String(err)}; dashboard page was not inspectable; capture diagnostics: ${JSON.stringify(captureDiagnostics)}`;
|
|
446
490
|
throw new Error(message);
|
|
447
491
|
}
|
|
448
492
|
}
|
|
@@ -469,7 +513,7 @@ async function assertDashboardReportPanelWindow(
|
|
|
469
513
|
actualPanelIds.length !== expectedPanelIds.length ||
|
|
470
514
|
actualPanelIds.some((panelId, index) => panelId !== expectedPanelIds[index])
|
|
471
515
|
) {
|
|
472
|
-
throw new
|
|
516
|
+
throw new DashboardReportPanelMismatchError(
|
|
473
517
|
`report chunk panel mismatch; expected=${JSON.stringify(expectedPanelIds)} actual=${JSON.stringify(actualPanelIds)}`,
|
|
474
518
|
);
|
|
475
519
|
}
|
|
@@ -765,6 +809,8 @@ async function captureDashboardChunk(
|
|
|
765
809
|
await waitForDashboardReportReady(
|
|
766
810
|
page,
|
|
767
811
|
boundedStageTimeout(attempt.readyTimeout ?? timeout, deadlineAt),
|
|
812
|
+
consoleErrors,
|
|
813
|
+
failedRequests,
|
|
768
814
|
);
|
|
769
815
|
captureStage = "validating the report chunk panels";
|
|
770
816
|
await assertDashboardReportPanelWindow(page, expectedPanelIds);
|
|
@@ -777,6 +823,8 @@ async function captureDashboardChunk(
|
|
|
777
823
|
attempt.secondReadyTimeout ?? secondReadyTimeoutMs(),
|
|
778
824
|
deadlineAt,
|
|
779
825
|
),
|
|
826
|
+
consoleErrors,
|
|
827
|
+
failedRequests,
|
|
780
828
|
);
|
|
781
829
|
captureStage = "revalidating the report chunk panels";
|
|
782
830
|
await assertDashboardReportPanelWindow(page, expectedPanelIds);
|
|
@@ -793,6 +841,7 @@ async function captureDashboardChunk(
|
|
|
793
841
|
if (!image?.length) throw new Error("Dashboard screenshot was empty");
|
|
794
842
|
return Buffer.from(image);
|
|
795
843
|
} catch (err) {
|
|
844
|
+
if (err instanceof DashboardReportPanelMismatchError) throw err;
|
|
796
845
|
throw new Error(`${captureStage}: ${errorMessage(err)}`);
|
|
797
846
|
}
|
|
798
847
|
}
|
|
@@ -800,7 +849,9 @@ async function captureDashboardChunk(
|
|
|
800
849
|
async function captureDashboardPngChunks(
|
|
801
850
|
sub: DashboardReportSubscription,
|
|
802
851
|
snapshot: ReportSnapshot,
|
|
803
|
-
|
|
852
|
+
allowPartialCapture: boolean,
|
|
853
|
+
totalTimeoutMs?: number,
|
|
854
|
+
): Promise<DashboardPngCapture> {
|
|
804
855
|
const offsets = reportChunkOffsets(snapshot.panelIds.length);
|
|
805
856
|
if (offsets.length > MAX_DASHBOARD_REPORT_CHUNKS) {
|
|
806
857
|
throw new Error(
|
|
@@ -814,9 +865,9 @@ async function captureDashboardPngChunks(
|
|
|
814
865
|
captureScale: 0.7,
|
|
815
866
|
...(serverless
|
|
816
867
|
? {
|
|
817
|
-
readyTimeout:
|
|
868
|
+
readyTimeout: DASHBOARD_REPORT_READY_TIMEOUT_MS,
|
|
818
869
|
secondReadyTimeout: 15_000,
|
|
819
|
-
totalTimeout: SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
|
|
870
|
+
totalTimeout: totalTimeoutMs ?? SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
|
|
820
871
|
}
|
|
821
872
|
: {}),
|
|
822
873
|
};
|
|
@@ -834,6 +885,7 @@ async function captureDashboardPngChunks(
|
|
|
834
885
|
if (browser) void browser.close().catch(() => {});
|
|
835
886
|
}, attempt.totalTimeout)
|
|
836
887
|
: null;
|
|
888
|
+
const images: Array<Buffer | null> = Array(offsets.length).fill(null);
|
|
837
889
|
try {
|
|
838
890
|
launchPromise = launchScreenshotBrowser(attempt.viewport);
|
|
839
891
|
const launched = attempt.totalTimeout
|
|
@@ -854,9 +906,8 @@ async function captureDashboardPngChunks(
|
|
|
854
906
|
: await launchPromise;
|
|
855
907
|
browser = launched.browser;
|
|
856
908
|
cleanup = launched.cleanup;
|
|
857
|
-
const images: Buffer[] = [];
|
|
858
909
|
let attachmentBytes = 0;
|
|
859
|
-
for (const offset of offsets) {
|
|
910
|
+
for (const [index, offset] of offsets.entries()) {
|
|
860
911
|
if (attemptTimedOut)
|
|
861
912
|
throw new Error(`capture exceeded ${attempt.totalTimeout}ms`);
|
|
862
913
|
const page = await runWithinCaptureDeadline(
|
|
@@ -887,21 +938,35 @@ async function captureDashboardPngChunks(
|
|
|
887
938
|
`complete dashboard images total ${attachmentBytes} bytes; email delivery supports at most ${MAX_DASHBOARD_REPORT_ATTACHMENT_BYTES} bytes`,
|
|
888
939
|
);
|
|
889
940
|
}
|
|
890
|
-
images
|
|
941
|
+
images[index] = image;
|
|
891
942
|
} finally {
|
|
892
943
|
await runBoundedBrowserCleanup("Failed to close screenshot page", () =>
|
|
893
944
|
page.close(),
|
|
894
945
|
);
|
|
895
946
|
}
|
|
896
947
|
}
|
|
897
|
-
return images;
|
|
948
|
+
return { pngs: images, mode: "full" };
|
|
898
949
|
} catch (err) {
|
|
950
|
+
const partialCapture =
|
|
951
|
+
allowPartialCapture &&
|
|
952
|
+
!(err instanceof DashboardReportPanelMismatchError) &&
|
|
953
|
+
images.some((image) => image !== null);
|
|
954
|
+
const suffix = attemptTimedOut
|
|
955
|
+
? `${memoryDiagnostics()} ${attempt.label} capture exceeded ${attempt.totalTimeout}ms: ${errorMessage(err)}`
|
|
956
|
+
: `${attempt.label}: ${errorMessage(err)}`;
|
|
957
|
+
if (partialCapture) {
|
|
958
|
+
return {
|
|
959
|
+
pngs: images,
|
|
960
|
+
mode: "partial",
|
|
961
|
+
error: suffix.includes("lambdaMemoryMb=")
|
|
962
|
+
? suffix
|
|
963
|
+
: `${suffix}; ${memoryDiagnostics()}`,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
899
966
|
if (attemptTimedOut) {
|
|
900
|
-
throw new Error(
|
|
901
|
-
`${memoryDiagnostics()} ${attempt.label} capture exceeded ${attempt.totalTimeout}ms: ${errorMessage(err)}`,
|
|
902
|
-
);
|
|
967
|
+
throw new Error(suffix);
|
|
903
968
|
}
|
|
904
|
-
throw new Error(
|
|
969
|
+
throw new Error(suffix);
|
|
905
970
|
} finally {
|
|
906
971
|
if (attemptTimeout) clearTimeout(attemptTimeout);
|
|
907
972
|
if (launchTimeout) clearTimeout(launchTimeout);
|
|
@@ -940,22 +1005,35 @@ function errorMessage(err: unknown): string {
|
|
|
940
1005
|
async function captureDashboardPngWithFallback(
|
|
941
1006
|
sub: DashboardReportSubscription,
|
|
942
1007
|
snapshot: ReportSnapshot,
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1008
|
+
allowPartialCapture: boolean,
|
|
1009
|
+
totalTimeoutMs?: number,
|
|
1010
|
+
): Promise<DashboardPngCapture> {
|
|
1011
|
+
if (totalTimeoutMs !== undefined && totalTimeoutMs <= 0) {
|
|
1012
|
+
return {
|
|
1013
|
+
pngs: null,
|
|
1014
|
+
mode: "none",
|
|
1015
|
+
error: `report delivery deadline left no time for capture; ${memoryDiagnostics()}`,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
948
1018
|
try {
|
|
949
|
-
const
|
|
1019
|
+
const capture = await captureDashboardPngChunks(
|
|
1020
|
+
sub,
|
|
1021
|
+
snapshot,
|
|
1022
|
+
allowPartialCapture,
|
|
1023
|
+
totalTimeoutMs,
|
|
1024
|
+
);
|
|
950
1025
|
if (isServerlessBrowserRuntime()) {
|
|
951
1026
|
console.info(
|
|
952
|
-
`[dashboard-report]
|
|
1027
|
+
`[dashboard-report] ${capture.mode} chunked capture finished: chunks=${capture.pngs?.filter(Boolean).length ?? 0}/${capture.pngs?.length ?? 0} ${memoryDiagnostics()}`,
|
|
953
1028
|
);
|
|
954
1029
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1030
|
+
if (capture.mode === "partial") {
|
|
1031
|
+
console.error(
|
|
1032
|
+
`[dashboard-report] partial chunked screenshot capture for subscription ${sub.id}:`,
|
|
1033
|
+
capture.error,
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
return capture;
|
|
959
1037
|
} catch (err) {
|
|
960
1038
|
const rawError = errorMessage(err).replace(/\s+/g, " ").trim();
|
|
961
1039
|
const error = rawError.includes("lambdaMemoryMb=")
|
|
@@ -980,24 +1058,25 @@ function reportDate(snapshot: ReportSnapshot): string {
|
|
|
980
1058
|
function renderReportEmailHtml(
|
|
981
1059
|
snapshot: ReportSnapshot,
|
|
982
1060
|
options: {
|
|
983
|
-
|
|
984
|
-
screenshotCount: number;
|
|
1061
|
+
screenshotParts: Array<Buffer | null>;
|
|
985
1062
|
},
|
|
986
1063
|
): string {
|
|
987
1064
|
const title = escapeHtml(snapshot.title);
|
|
988
1065
|
const dashboardUrl = escapeHtml(snapshot.dashboardUrl);
|
|
989
1066
|
const reportSettingsUrl = escapeHtml(snapshot.reportSettingsUrl);
|
|
990
1067
|
const date = escapeHtml(reportDate(snapshot));
|
|
991
|
-
const screenshotBlock = options.
|
|
992
|
-
?
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1068
|
+
const screenshotBlock = options.screenshotParts.some(Boolean)
|
|
1069
|
+
? options.screenshotParts
|
|
1070
|
+
.map((screenshot, index) =>
|
|
1071
|
+
screenshot
|
|
1072
|
+
? `<a href="${dashboardUrl}" style="display:block;text-decoration:none;">
|
|
1073
|
+
<img src="cid:${DASHBOARD_REPORT_CID}-${index + 1}" alt="${title}${options.screenshotParts.length > 1 ? ` (part ${index + 1})` : ""}" width="100%" style="display:block;width:100%;max-width:1280px;height:auto;border:0;outline:0;border-radius:0;" />
|
|
1074
|
+
</a>`
|
|
1075
|
+
: `<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;">
|
|
1076
|
+
Dashboard image part ${index + 1} was unavailable for this run. Open the live dashboard to view the latest report.
|
|
1077
|
+
</div>`,
|
|
1078
|
+
)
|
|
1079
|
+
.join("\n")
|
|
1001
1080
|
: `<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;">
|
|
1002
1081
|
The dashboard image was unavailable for this run. Open the live dashboard to view the latest report.
|
|
1003
1082
|
</div>`;
|
|
@@ -1021,7 +1100,7 @@ function renderReportEmailHtml(
|
|
|
1021
1100
|
function renderReportText(
|
|
1022
1101
|
snapshot: ReportSnapshot,
|
|
1023
1102
|
options: {
|
|
1024
|
-
|
|
1103
|
+
screenshotParts: Array<Buffer | null>;
|
|
1025
1104
|
},
|
|
1026
1105
|
): string {
|
|
1027
1106
|
const lines = [
|
|
@@ -1030,8 +1109,15 @@ function renderReportText(
|
|
|
1030
1109
|
`Open dashboard: ${snapshot.dashboardUrl}`,
|
|
1031
1110
|
`Edit subscription settings: ${snapshot.reportSettingsUrl}`,
|
|
1032
1111
|
];
|
|
1033
|
-
if (!options.
|
|
1112
|
+
if (!options.screenshotParts.some(Boolean)) {
|
|
1034
1113
|
lines.push("Dashboard image unavailable for this run.");
|
|
1114
|
+
} else {
|
|
1115
|
+
for (const [index, screenshot] of options.screenshotParts.entries()) {
|
|
1116
|
+
if (!screenshot)
|
|
1117
|
+
lines.push(
|
|
1118
|
+
`Dashboard image part ${index + 1} unavailable for this run.`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1035
1121
|
}
|
|
1036
1122
|
return lines.join("\n");
|
|
1037
1123
|
}
|
|
@@ -1051,26 +1137,46 @@ export async function sendDashboardReportSubscription(
|
|
|
1051
1137
|
options: {
|
|
1052
1138
|
requireScreenshot?: boolean;
|
|
1053
1139
|
skipEmailWithoutScreenshot?: boolean;
|
|
1140
|
+
deadlineAt?: number;
|
|
1054
1141
|
} = {},
|
|
1055
1142
|
): Promise<{
|
|
1056
1143
|
dashboardUrl: string;
|
|
1057
1144
|
recipientCount: number;
|
|
1058
1145
|
screenshotAttached: boolean;
|
|
1059
|
-
screenshotMode: "full" | "none";
|
|
1146
|
+
screenshotMode: "full" | "partial" | "none";
|
|
1060
1147
|
screenshotError?: string;
|
|
1061
1148
|
emailsSent: boolean;
|
|
1062
1149
|
}> {
|
|
1063
1150
|
const recipients = normalizeDashboardReportRecipients(sub.recipients);
|
|
1064
|
-
const snapshot = await
|
|
1065
|
-
|
|
1066
|
-
|
|
1151
|
+
const snapshot = await runWithinReportDeadline(
|
|
1152
|
+
"Dashboard report snapshot",
|
|
1153
|
+
() => collectReportSnapshot(sub),
|
|
1154
|
+
options.deadlineAt,
|
|
1155
|
+
);
|
|
1156
|
+
const captureTimeoutMs = options.deadlineAt
|
|
1157
|
+
? Math.min(
|
|
1158
|
+
SERVERLESS_CHUNKED_ATTEMPT_TIMEOUT_MS,
|
|
1159
|
+
options.deadlineAt -
|
|
1160
|
+
Date.now() -
|
|
1161
|
+
MAX_CAPTURE_CLEANUP_RESERVE_MS -
|
|
1162
|
+
recipients.length * DASHBOARD_REPORT_EMAIL_TIMEOUT_MS -
|
|
1163
|
+
DASHBOARD_REPORT_EMAIL_OVERHEAD_RESERVE_MS,
|
|
1164
|
+
)
|
|
1165
|
+
: undefined;
|
|
1166
|
+
const capture = await captureDashboardPngWithFallback(
|
|
1167
|
+
sub,
|
|
1168
|
+
snapshot,
|
|
1169
|
+
!options.skipEmailWithoutScreenshot && !options.requireScreenshot,
|
|
1170
|
+
captureTimeoutMs,
|
|
1171
|
+
);
|
|
1172
|
+
if (capture.mode !== "full" && options.requireScreenshot) {
|
|
1067
1173
|
throw new Error(
|
|
1068
1174
|
capture.error
|
|
1069
1175
|
? `Dashboard screenshot unavailable: ${capture.error}`
|
|
1070
1176
|
: "Dashboard screenshot unavailable",
|
|
1071
1177
|
);
|
|
1072
1178
|
}
|
|
1073
|
-
if (
|
|
1179
|
+
if (capture.mode !== "full" && options.skipEmailWithoutScreenshot) {
|
|
1074
1180
|
return {
|
|
1075
1181
|
dashboardUrl: snapshot.dashboardUrl,
|
|
1076
1182
|
recipientCount: recipients.length,
|
|
@@ -1080,35 +1186,53 @@ export async function sendDashboardReportSubscription(
|
|
|
1080
1186
|
...(capture.error ? { screenshotError: capture.error } : {}),
|
|
1081
1187
|
};
|
|
1082
1188
|
}
|
|
1083
|
-
const screenshotAttached = Boolean(capture.pngs?.
|
|
1189
|
+
const screenshotAttached = Boolean(capture.pngs?.some(Boolean));
|
|
1190
|
+
const screenshotParts = capture.pngs ?? [];
|
|
1084
1191
|
const html = renderReportEmailHtml(snapshot, {
|
|
1085
|
-
|
|
1086
|
-
screenshotCount: capture.pngs?.length ?? 0,
|
|
1192
|
+
screenshotParts,
|
|
1087
1193
|
});
|
|
1088
1194
|
const text = renderReportText(snapshot, {
|
|
1089
|
-
|
|
1195
|
+
screenshotParts,
|
|
1090
1196
|
});
|
|
1091
1197
|
const subject = `Daily dashboard: ${snapshot.title}`;
|
|
1092
1198
|
|
|
1093
1199
|
for (const to of recipients) {
|
|
1200
|
+
const emailTimeoutMs = options.deadlineAt
|
|
1201
|
+
? Math.min(
|
|
1202
|
+
DASHBOARD_REPORT_EMAIL_TIMEOUT_MS,
|
|
1203
|
+
options.deadlineAt - Date.now(),
|
|
1204
|
+
)
|
|
1205
|
+
: DASHBOARD_REPORT_EMAIL_TIMEOUT_MS;
|
|
1206
|
+
if (emailTimeoutMs <= 0) {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
"Dashboard email delivery exceeded the report delivery deadline",
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1094
1211
|
await sendEmail({
|
|
1095
1212
|
to,
|
|
1096
1213
|
subject,
|
|
1097
1214
|
html,
|
|
1098
1215
|
text,
|
|
1099
1216
|
attachments: capture.pngs
|
|
1100
|
-
? capture.pngs.
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1217
|
+
? capture.pngs.flatMap((png, index) =>
|
|
1218
|
+
png
|
|
1219
|
+
? [
|
|
1220
|
+
{
|
|
1221
|
+
filename: reportFilename(
|
|
1222
|
+
snapshot.title,
|
|
1223
|
+
index + 1,
|
|
1224
|
+
capture.pngs!.length,
|
|
1225
|
+
),
|
|
1226
|
+
content: png,
|
|
1227
|
+
contentType: "image/png",
|
|
1228
|
+
contentId: `${DASHBOARD_REPORT_CID}-${index + 1}`,
|
|
1229
|
+
disposition: "inline",
|
|
1230
|
+
},
|
|
1231
|
+
]
|
|
1232
|
+
: [],
|
|
1233
|
+
)
|
|
1111
1234
|
: undefined,
|
|
1235
|
+
timeoutMs: emailTimeoutMs,
|
|
1112
1236
|
});
|
|
1113
1237
|
}
|
|
1114
1238
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getDbExec } from "@agent-native/core/db";
|
|
2
2
|
import { and, eq, isNull, or } from "drizzle-orm";
|
|
3
3
|
|
|
4
|
+
import { FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS } from "../../shared/dashboard-report-timeouts.js";
|
|
4
5
|
import { getDb, schema } from "../db/index.js";
|
|
5
6
|
import {
|
|
6
7
|
EXCEPTION_EVENT_NAME,
|
|
@@ -642,6 +643,8 @@ export async function queryFirstPartyAnalytics(
|
|
|
642
643
|
const result = await exec.execute({
|
|
643
644
|
sql: `SELECT * FROM (${scoped.sql}) AS first_party_analytics_query LIMIT ${MAX_QUERY_ROWS}`,
|
|
644
645
|
args: scoped.args,
|
|
646
|
+
timeoutMs: FIRST_PARTY_ANALYTICS_QUERY_TIMEOUT_MS,
|
|
647
|
+
maxAttempts: 1,
|
|
645
648
|
});
|
|
646
649
|
const rows = result.rows as Record<string, unknown>[];
|
|
647
650
|
return { rows, schema: inferSchema(rows) };
|
|
@@ -393,6 +393,20 @@ export function repairFirstPartyObservedRetentionPanels(
|
|
|
393
393
|
description: RECURRING_USERS_WEEKLY_DESCRIPTION,
|
|
394
394
|
},
|
|
395
395
|
],
|
|
396
|
+
[
|
|
397
|
+
"dau-over-time",
|
|
398
|
+
{
|
|
399
|
+
legacySql: [LEGACY_DAU_BY_TEMPLATE_SQL],
|
|
400
|
+
sql: DAU_BY_TEMPLATE_SQL,
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
[
|
|
404
|
+
"wau-over-time",
|
|
405
|
+
{
|
|
406
|
+
legacySql: [LEGACY_WAU_BY_TEMPLATE_SQL],
|
|
407
|
+
sql: WAU_BY_TEMPLATE_SQL,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
396
410
|
...additionalReplacements.map(
|
|
397
411
|
(replacement) => [replacement.id, replacement] as const,
|
|
398
412
|
),
|
|
@@ -437,8 +451,10 @@ export function repairFirstPartyObservedRetentionPanels(
|
|
|
437
451
|
? { config: { ...config, panels }, changed }
|
|
438
452
|
: { config, changed };
|
|
439
453
|
}
|
|
440
|
-
const
|
|
441
|
-
const
|
|
454
|
+
export const LEGACY_DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT ${SIGNED_IN_ACTIVITY_KEY_SQL}) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
|
|
455
|
+
const DAU_BY_TEMPLATE_SQL = `SELECT ${EVENT_DATE_SQL} AS date, ${TEMPLATE_EXPR} AS template, COUNT(DISTINCT user_key) AS visitors FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_TIME_RANGE_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} GROUP BY ${EVENT_DATE_SQL}, ${TEMPLATE_EXPR} ORDER BY date, template`;
|
|
456
|
+
export const LEGACY_WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT ${SIGNED_IN_ACTIVITY_KEY_SQL} AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
|
|
457
|
+
const WAU_BY_TEMPLATE_SQL = `WITH base AS (SELECT user_key AS visitor_key, ${TEMPLATE_EXPR} AS template, ${EVENT_DATE_SQL} AS event_date, user_id FROM analytics_events WHERE ${SIGNED_IN_ACTIVITY_FILTER} AND ${DASHBOARD_EMAIL_FILTER} AND ${KNOWN_PRODUCT_ACTIVITY_TEMPLATE_FILTER} AND ${DASHBOARD_WAU_BASE_RANGE_FILTER}), days AS (SELECT DISTINCT event_date AS date FROM base WHERE ${DASHBOARD_EVENT_DATE_RANGE_FILTER}) SELECT d.date, b.template, COUNT(DISTINCT b.visitor_key) AS visitors FROM days d JOIN base b ON b.event_date >= to_char(d.date::date - INTERVAL '6 days', 'YYYY-MM-DD') AND b.event_date <= d.date GROUP BY d.date, b.template ORDER BY d.date, b.template`;
|
|
442
458
|
|
|
443
459
|
/**
|
|
444
460
|
* Catalog entries. Order here is the default panel order when a caller passes
|
|
@@ -35,6 +35,34 @@ Then implement working code that is cohesive, accessible, responsive, and polish
|
|
|
35
35
|
|
|
36
36
|
Default to Apple/Linear-level restraint: make the primary workflow obvious, then remove everything that does not help that workflow right now. A polished UI often has fewer visible controls, fewer borders, fewer labels, and fewer explanatory surfaces than the first reasonable implementation.
|
|
37
37
|
|
|
38
|
+
### Progressive Disclosure Is A Design Requirement
|
|
39
|
+
|
|
40
|
+
Treat an all-at-once interface as a defect to fix during design, not as a
|
|
41
|
+
styling preference. Before coding, inventory every piece of content and action
|
|
42
|
+
on the surface, then assign each one to the smallest useful visibility level:
|
|
43
|
+
|
|
44
|
+
1. **Immediate** — the page title, current state, one primary action, and the
|
|
45
|
+
compact context needed to choose what to do next.
|
|
46
|
+
2. **Expanded** — the details needed for the selected item or active workflow.
|
|
47
|
+
3. **On demand** — advanced settings, diagnostics, credentials, metadata,
|
|
48
|
+
destructive actions, documentation, and rarely used tools.
|
|
49
|
+
|
|
50
|
+
Use single-select accordions or simple disclosure rows for sibling panels when
|
|
51
|
+
the user is choosing one item at a time. Inside an expanded panel, keep another
|
|
52
|
+
layer for independent concerns instead of dumping every form, explanation, and
|
|
53
|
+
secondary action into the first reveal. Prefer one flat panel with alignment,
|
|
54
|
+
dividers, and whitespace over nested cards; provider or product icons should
|
|
55
|
+
not receive decorative borders or containers unless the container communicates
|
|
56
|
+
state or interaction. A collapsed row should still show the item name, status,
|
|
57
|
+
and a concise summary so the user can scan the whole surface without opening
|
|
58
|
+
everything.
|
|
59
|
+
|
|
60
|
+
Before shipping, ask: “What can disappear until the user asks for it?” Then
|
|
61
|
+
verify collapsed, expanded, loading, empty, error, and narrow-width states.
|
|
62
|
+
If the first viewport contains multiple forms, repeated explanatory copy,
|
|
63
|
+
documentation links, and controls for unrelated tasks, the surface has not
|
|
64
|
+
passed this requirement yet.
|
|
65
|
+
|
|
38
66
|
- **Start by subtracting**: Before adding a visible control, banner, toolbar row, card, or explanatory block, ask what can be removed, merged, renamed, or moved into an existing affordance.
|
|
39
67
|
- **One primary action**: Each surface should have one dominant next action. Secondary actions belong in menus, popovers, command palettes, disclosure rows, or contextual hover/focus states unless they are used constantly.
|
|
40
68
|
- **Progressively disclose rare work**: Advanced options, diagnostics, metadata, settings, import/export, destructive actions, and inspection tools should stay tucked away until requested. Prefer small icon triggers with tooltips, popovers, drawers, or detail panels over permanent chrome.
|
|
@@ -68,10 +68,9 @@ export function Layout({ children }: LayoutProps) {
|
|
|
68
68
|
useAgentChatHomeHandoffLinks({
|
|
69
69
|
storageKey: ASSETS_CHAT_STORAGE_KEY,
|
|
70
70
|
isChatPath: (pathname) => pathname === "/" || pathname.startsWith("/chat/"),
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
requireActiveHandoff: false,
|
|
71
|
+
// Only preserve the transition when chat activity has recorded an active
|
|
72
|
+
// handoff; an empty home chat should keep the destination sidebar closed.
|
|
73
|
+
requireActiveHandoff: true,
|
|
75
74
|
});
|
|
76
75
|
|
|
77
76
|
useEffect(() => {
|
|
@@ -92,11 +92,6 @@
|
|
|
92
92
|
background: hsl(var(--muted-foreground) / 0.4);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
.assets-create-chat-panel [data-agent-empty-state="centered"] {
|
|
96
|
-
justify-content: center;
|
|
97
|
-
padding-bottom: clamp(4rem, 12vh, 8rem);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
95
|
.assets-create-chat-intro {
|
|
101
96
|
display: none;
|
|
102
97
|
}
|