+
+ Save
+
CSV
@@ -181,7 +215,7 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
Template
handleTemplateChange(event.target.value)} style={fieldStyle}>
- {REPORT_TEMPLATES.map((reportTemplate) => (
+ {getAllReportTemplates().map((reportTemplate) => (
{reportTemplate.name}
@@ -189,6 +223,15 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
+
+ Prompt
+
+
+
+ Generate report from prompt
+
+
Horizon resource
@@ -299,6 +342,34 @@ export default function CustomReports({ analytics }: CustomReportsProps) {
))}
+ {reportData.insights && reportData.insights.length > 0 && (
+
+
AI insights
+
+ {reportData.insights.map((insight) => (
+ {insight}
+ ))}
+
+
+ )}
+
+
+ Parsed focus: {parsedRequest.focusAreas.join(", ")} {parsedRequest.accountId ? `· account ${parsedRequest.accountId}` : ""}
+
+
+ {savedTemplates.length > 0 && (
+
+
Saved templates
+
+ {savedTemplates.map((item) => (
+ handleTemplateChange(item.id)}>
+ {item.name}
+
+ ))}
+
+
+ )}
+
Cron
diff --git a/src/lib/__tests__/customReports.test.ts b/src/lib/__tests__/customReports.test.ts
index 4e48d988..08e01fd0 100644
--- a/src/lib/__tests__/customReports.test.ts
+++ b/src/lib/__tests__/customReports.test.ts
@@ -8,7 +8,10 @@ import {
exportReportAsCsv,
exportReportAsJson,
getNextRunPreview,
+ loadSavedReportTemplates,
+ parseNaturalLanguageRequest,
REPORT_TEMPLATES,
+ saveReportTemplate,
transformReportData,
} from "../customReports";
@@ -38,15 +41,33 @@ const analytics = {
describe("custom reports", () => {
it("ships the required Stellar report templates", () => {
- expect(REPORT_TEMPLATES.map((template) => template.id)).toEqual([
- "account-activity",
- "portfolio-performance",
- "transaction-analysis",
- "network-health",
- ]);
+ expect(REPORT_TEMPLATES.length).toBeGreaterThanOrEqual(20);
+ expect(REPORT_TEMPLATES.map((template) => template.id)).toContain("account-activity");
+ expect(REPORT_TEMPLATES.map((template) => template.id)).toContain("network-health");
expect(REPORT_TEMPLATES.every((template) => template.components.length >= 3)).toBe(true);
});
+ it("parses natural-language requests and persists saved report templates", () => {
+ const parsed = parseNaturalLanguageRequest("Create a weekly account activity report for GABC with emphasis on risks and fees");
+
+ expect(parsed.templateId).toBe("account-activity");
+ expect(parsed.accountId).toBe("GABC");
+ expect(parsed.focusAreas).toEqual(expect.arrayContaining(["risks", "fees"]));
+
+ const saved = saveReportTemplate({
+ id: "saved-weekly-account",
+ name: "Weekly Account Summary",
+ description: "A saved report template",
+ resource: "accounts",
+ components: REPORT_TEMPLATES[0].components,
+ category: "account",
+ tags: ["weekly", "account"],
+ });
+
+ expect(saved.id).toBe("saved-weekly-account");
+ expect(loadSavedReportTemplates()).toEqual(expect.arrayContaining([expect.objectContaining({ id: "saved-weekly-account" })]));
+ });
+
it("builds account-scoped Horizon queries with filters", () => {
const query = buildHorizonQuery({
resource: "transactions",
diff --git a/src/lib/customReports.ts b/src/lib/customReports.ts
index dd260f63..65dc251f 100644
--- a/src/lib/customReports.ts
+++ b/src/lib/customReports.ts
@@ -15,6 +15,9 @@ export interface ReportTemplate {
description: string;
resource: ReportResource;
components: ReportComponentDefinition[];
+ category?: string;
+ tags?: string[];
+ aiHint?: string;
}
export interface HorizonQueryConfig {
@@ -49,6 +52,18 @@ export interface TransformedReportData {
chartData: Array>;
rows: Array>;
columns: string[];
+ insights?: string[];
+ summary?: string;
+ focusAreas?: string[];
+}
+
+export interface ParsedReportRequest {
+ templateId: string;
+ accountId?: string;
+ focusAreas: string[];
+ frequency?: string;
+ resource: ReportResource;
+ prompt: string;
}
export interface ReportSchedule {
@@ -82,12 +97,15 @@ export const REPORT_COMPONENT_PALETTE: ReportComponentDefinition[] = [
{ id: "table-records", type: "table", label: "Table", dataKey: "rows" },
];
-export const REPORT_TEMPLATES: ReportTemplate[] = [
+const REPORT_TEMPLATE_DEFINITIONS: ReportTemplate[] = [
{
id: "account-activity",
name: "Account Activity",
description: "Track balances, signer risk, and recent transaction volume for an account.",
resource: "accounts",
+ category: "account",
+ tags: ["account", "activity", "risk"],
+ aiHint: "Summarize balances, signer risk, and recent volume for a specific account.",
components: [
{ id: "account-balance", type: "metric", label: "XLM Balance", dataKey: "account.xlmBalance" },
{ id: "account-activity-chart", type: "chart", label: "Daily Activity", dataKey: "activity" },
@@ -99,6 +117,9 @@ export const REPORT_TEMPLATES: ReportTemplate[] = [
name: "Portfolio Performance",
description: "Summarize trustlines, asset counts, and fee movement over time.",
resource: "accounts",
+ category: "portfolio",
+ tags: ["portfolio", "assets", "performance"],
+ aiHint: "Highlight asset exposure and fee trends across a portfolio.",
components: [
{ id: "portfolio-assets", type: "metric", label: "Assets", dataKey: "account.totalAssets" },
{ id: "portfolio-fees", type: "chart", label: "Fee Trend", dataKey: "activity" },
@@ -110,6 +131,9 @@ export const REPORT_TEMPLATES: ReportTemplate[] = [
name: "Transaction Analysis",
description: "Inspect transaction success rates and operation mix.",
resource: "transactions",
+ category: "transactions",
+ tags: ["transactions", "payments", "success"],
+ aiHint: "Inspect success rates and the mix of operations in a transaction stream.",
components: [
{ id: "tx-success-rate", type: "metric", label: "Success Rate", dataKey: "transactions.successRate" },
{ id: "tx-activity", type: "chart", label: "Transaction Trend", dataKey: "activity" },
@@ -121,14 +145,243 @@ export const REPORT_TEMPLATES: ReportTemplate[] = [
name: "Network Health",
description: "Monitor ledger freshness, fees, and close-time health.",
resource: "ledgers",
+ category: "network",
+ tags: ["network", "ledger", "health"],
+ aiHint: "Monitor ledger freshness, fee pressure, and network stability.",
components: [
{ id: "network-ledger", type: "metric", label: "Latest Ledger", dataKey: "network.latestLedgerSequence" },
{ id: "network-close-time", type: "chart", label: "Close Time", dataKey: "activity" },
{ id: "network-fees", type: "table", label: "Network Fees", dataKey: "network" },
],
},
+ {
+ id: "asset-allocation",
+ name: "Asset Allocation",
+ description: "Review trustline concentration and balance allocation across assets.",
+ resource: "accounts",
+ category: "portfolio",
+ tags: ["asset", "allocation", "trustline"],
+ aiHint: "Summarize concentration by asset and trustline coverage.",
+ components: [
+ { id: "asset-count", type: "metric", label: "Trustlines", dataKey: "account.trustlineCount" },
+ { id: "asset-chart", type: "chart", label: "Asset Mix", dataKey: "activity" },
+ { id: "asset-table", type: "table", label: "Asset Distribution", dataKey: "account" },
+ ],
+ },
+ {
+ id: "signer-risk-review",
+ name: "Signer Risk Review",
+ description: "Focus on signature policy and single-signer exposure.",
+ resource: "accounts",
+ category: "security",
+ tags: ["signer", "risk", "security"],
+ aiHint: "Call out singlesigner exposure and signing policy anomalies.",
+ components: [
+ { id: "signer-metric", type: "metric", label: "Risk Signals", dataKey: "risks.length" },
+ { id: "signer-chart", type: "chart", label: "Risk Timeline", dataKey: "risks" },
+ { id: "signer-table", type: "table", label: "Signer Risks", dataKey: "risks" },
+ ],
+ },
+ {
+ id: "fee-efficiency",
+ name: "Fee Efficiency",
+ description: "Compare activity volume against fees to surface efficiency opportunities.",
+ resource: "payments",
+ category: "cost",
+ tags: ["fee", "cost", "efficiency"],
+ aiHint: "Compare transaction volume and fees to estimate efficiency.",
+ components: [
+ { id: "fee-metric", type: "metric", label: "Base Fee", dataKey: "network.baseFee" },
+ { id: "fee-chart", type: "chart", label: "Fee Trend", dataKey: "activity" },
+ { id: "fee-table", type: "table", label: "Fee Summary", dataKey: "network" },
+ ],
+ },
+ {
+ id: "payment-velocity",
+ name: "Payment Velocity",
+ description: "Review throughput and cadence of payments over time.",
+ resource: "payments",
+ category: "payments",
+ tags: ["payment", "velocity", "throughput"],
+ aiHint: "Track payment cadence and volume spikes.",
+ components: [
+ { id: "velocity-metric", type: "metric", label: "Payment Count", dataKey: "activity.length" },
+ { id: "velocity-chart", type: "chart", label: "Payment Trend", dataKey: "activity" },
+ { id: "velocity-table", type: "table", label: "Payment Snapshot", dataKey: "activity" },
+ ],
+ },
+ {
+ id: "liquidity-watch",
+ name: "Liquidity Watch",
+ description: "Measure available liquidity and reserve pressure for key accounts.",
+ resource: "accounts",
+ category: "liquidity",
+ tags: ["liquidity", "reserve", "balance"],
+ aiHint: "Highlight reserve pressure and balance sufficiency.",
+ components: [
+ { id: "liquidity-metric", type: "metric", label: "Available Balance", dataKey: "account.xlmBalance" },
+ { id: "liquidity-chart", type: "chart", label: "Balance Trend", dataKey: "activity" },
+ { id: "liquidity-table", type: "table", label: "Liquidity Signals", dataKey: "account" },
+ ],
+ },
+ {
+ id: "ledger-freshness",
+ name: "Ledger Freshness",
+ description: "Inspect the last ledger close and proximity to current activity.",
+ resource: "ledgers",
+ category: "network",
+ tags: ["ledger", "freshness", "timing"],
+ aiHint: "Compare close-time cadence with activity volume.",
+ components: [
+ { id: "ledger-metric", type: "metric", label: "Latest Ledger", dataKey: "network.latestLedgerSequence" },
+ { id: "ledger-chart", type: "chart", label: "Close Timeline", dataKey: "activity" },
+ { id: "ledger-table", type: "table", label: "Ledger Snapshot", dataKey: "network" },
+ ],
+ },
+ {
+ id: "compliance-review",
+ name: "Compliance Review",
+ description: "Surface policy-sensitive activity for audit and review workflows.",
+ resource: "transactions",
+ category: "compliance",
+ tags: ["compliance", "audit", "review"],
+ aiHint: "Summarize reviewable activity that may need policy attention.",
+ components: [
+ { id: "compliance-metric", type: "metric", label: "Review Items", dataKey: "risks.length" },
+ { id: "compliance-chart", type: "chart", label: "Review Trend", dataKey: "activity" },
+ { id: "compliance-table", type: "table", label: "Compliance Signals", dataKey: "risks" },
+ ],
+ },
+ {
+ id: "recovery-readiness",
+ name: "Recovery Readiness",
+ description: "Assess how prepared an account is for recovery or incident response.",
+ resource: "accounts",
+ category: "operations",
+ tags: ["recovery", "incident", "prepare"],
+ aiHint: "Highlight account resiliency and operational readiness.",
+ components: [
+ { id: "recovery-metric", type: "metric", label: "Risk Signals", dataKey: "risks.length" },
+ { id: "recovery-chart", type: "chart", label: "Operational Trend", dataKey: "activity" },
+ { id: "recovery-table", type: "table", label: "Recovery Notes", dataKey: "risks" },
+ ],
+ },
+ {
+ id: "validator-health",
+ name: "Validator Health",
+ description: "Monitor validator-related network stability indicators.",
+ resource: "ledgers",
+ category: "network",
+ tags: ["validator", "stability", "network"],
+ aiHint: "Review network health cues around validator and ledger performance.",
+ components: [
+ { id: "validator-metric", type: "metric", label: "Latest Ledger", dataKey: "network.latestLedgerSequence" },
+ { id: "validator-chart", type: "chart", label: "Health Trend", dataKey: "activity" },
+ { id: "validator-table", type: "table", label: "Validator Snapshot", dataKey: "network" },
+ ],
+ },
+ {
+ id: "market-activity",
+ name: "Market Activity",
+ description: "Capture trading and flow shifts that indicate momentum.",
+ resource: "transactions",
+ category: "market",
+ tags: ["market", "momentum", "flow"],
+ aiHint: "Summarize trend shifts in transaction flow and trading behavior.",
+ components: [
+ { id: "market-metric", type: "metric", label: "Transactions", dataKey: "activity.length" },
+ { id: "market-chart", type: "chart", label: "Flow Trend", dataKey: "activity" },
+ { id: "market-table", type: "table", label: "Market Notes", dataKey: "activity" },
+ ],
+ },
+ {
+ id: "trustline-risk",
+ name: "Trustline Risk",
+ description: "Focus on trustline concentration and exposure hotspots.",
+ resource: "accounts",
+ category: "risk",
+ tags: ["trustline", "exposure", "risk"],
+ aiHint: "Surface concentration risk and trustline coverage gaps.",
+ components: [
+ { id: "trustline-metric", type: "metric", label: "Trustlines", dataKey: "account.trustlineCount" },
+ { id: "trustline-chart", type: "chart", label: "Exposure Trend", dataKey: "activity" },
+ { id: "trustline-table", type: "table", label: "Trustline Risks", dataKey: "account" },
+ ],
+ },
+ {
+ id: "reserve-management",
+ name: "Reserve Management",
+ description: "Monitor reserve usage and balance sustainability.",
+ resource: "accounts",
+ category: "operations",
+ tags: ["reserve", "balance", "sustainability"],
+ aiHint: "Flag reserve strain and sustainability concerns.",
+ components: [
+ { id: "reserve-metric", type: "metric", label: "Balance", dataKey: "account.xlmBalance" },
+ { id: "reserve-chart", type: "chart", label: "Reserve Trend", dataKey: "activity" },
+ { id: "reserve-table", type: "table", label: "Reserve Snapshot", dataKey: "account" },
+ ],
+ },
+ {
+ id: "ops-drift",
+ name: "Operations Drift",
+ description: "Identify changes in operations mix over consecutive windows.",
+ resource: "operations",
+ category: "operations",
+ tags: ["operations", "drift", "change"],
+ aiHint: "Spot drift in the operations mix over time.",
+ components: [
+ { id: "ops-metric", type: "metric", label: "Operations", dataKey: "activity.length" },
+ { id: "ops-chart", type: "chart", label: "Drift Trend", dataKey: "activity" },
+ { id: "ops-table", type: "table", label: "Operations Mix", dataKey: "activity" },
+ ],
+ },
+ {
+ id: "asset-flow",
+ name: "Asset Flow",
+ description: "Trace asset movement and transfer patterns across accounts.",
+ resource: "payments",
+ category: "payments",
+ tags: ["asset", "flow", "transfer"],
+ aiHint: "Trace transfer flow and highlight movement hotspots.",
+ components: [
+ { id: "asset-flow-metric", type: "metric", label: "Payments", dataKey: "activity.length" },
+ { id: "asset-flow-chart", type: "chart", label: "Flow Trend", dataKey: "activity" },
+ { id: "asset-flow-table", type: "table", label: "Asset Flow", dataKey: "activity" },
+ ],
+ },
+ {
+ id: "activity-forecast",
+ name: "Activity Forecast",
+ description: "Generate a forecast-style snapshot for recurring activity patterns.",
+ resource: "transactions",
+ category: "forecast",
+ tags: ["forecast", "trend", "activity"],
+ aiHint: "Generate a forward-looking review of recurring activity patterns.",
+ components: [
+ { id: "forecast-metric", type: "metric", label: "Trend Score", dataKey: "activity.length" },
+ { id: "forecast-chart", type: "chart", label: "Forecast Trend", dataKey: "activity" },
+ { id: "forecast-table", type: "table", label: "Forecast Notes", dataKey: "activity" },
+ ],
+ },
+ {
+ id: "route-coverage",
+ name: "Route Coverage",
+ description: "Review how routing and payment coverage hold up across the network.",
+ resource: "payments",
+ category: "network",
+ tags: ["routing", "payments", "coverage"],
+ aiHint: "Review the breadth of payment routing coverage.",
+ components: [
+ { id: "route-metric", type: "metric", label: "Payments", dataKey: "activity.length" },
+ { id: "route-chart", type: "chart", label: "Coverage Trend", dataKey: "activity" },
+ { id: "route-table", type: "table", label: "Routing Snapshot", dataKey: "activity" },
+ ],
+ },
];
+export const REPORT_TEMPLATES: ReportTemplate[] = REPORT_TEMPLATE_DEFINITIONS;
+
function normalizeLimit(limit?: number) {
const parsed = Number(limit);
if (!Number.isFinite(parsed)) return "200";
@@ -160,8 +413,93 @@ function sanitizeFilePart(value: string) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "report";
}
+const SAVED_REPORT_TEMPLATE_STORAGE_KEY = "stellar-dev-dashboard-saved-report-templates";
+let savedReportTemplateMemory: ReportTemplate[] = [];
+
+function readSavedReportTemplatesFromStorage(): ReportTemplate[] {
+ if (typeof globalThis === "undefined") return savedReportTemplateMemory;
+ const storage = (globalThis as typeof globalThis & { localStorage?: Storage }).localStorage;
+ if (!storage) return savedReportTemplateMemory;
+
+ try {
+ const rawValue = storage.getItem(SAVED_REPORT_TEMPLATE_STORAGE_KEY);
+ if (!rawValue) return savedReportTemplateMemory;
+ const parsed = JSON.parse(rawValue);
+ if (!Array.isArray(parsed)) return savedReportTemplateMemory;
+ savedReportTemplateMemory = parsed as ReportTemplate[];
+ return savedReportTemplateMemory;
+ } catch {
+ return savedReportTemplateMemory;
+ }
+}
+
+export function getAllReportTemplates() {
+ return [...REPORT_TEMPLATES, ...readSavedReportTemplatesFromStorage()];
+}
+
export function getReportTemplate(templateId: string) {
- return REPORT_TEMPLATES.find((template) => template.id === templateId) || REPORT_TEMPLATES[0];
+ return getAllReportTemplates().find((template) => template.id === templateId) || REPORT_TEMPLATES[0];
+}
+
+export function parseNaturalLanguageRequest(request: string): ParsedReportRequest {
+ const normalizedRequest = request.trim();
+ const lower = normalizedRequest.toLowerCase();
+
+ const templateIds: Array<{ id: string; keywords: string[] }> = [
+ { id: "account-activity", keywords: ["account", "balance", "signer", "risk", "trustline", "review"] },
+ { id: "portfolio-performance", keywords: ["portfolio", "asset", "allocation", "holdings", "balance"] },
+ { id: "transaction-analysis", keywords: ["transaction", "payment", "operation", "volume", "flow"] },
+ { id: "network-health", keywords: ["network", "fee", "ledger", "validator", "health", "stability"] },
+ ];
+
+ const templateId = templateIds.find(({ keywords }) => keywords.some((keyword) => lower.includes(keyword)))?.id || "account-activity";
+ const accountIdMatch = normalizedRequest.match(/\b(G[A-Z2-7]{3,})\b/i);
+ const frequency = ["daily", "weekly", "monthly", "quarterly"].find((candidate) => lower.includes(candidate));
+ const focusAreas = [
+ { area: "risks", keywords: ["risk", "signer", "security", "compliance"] },
+ { area: "fees", keywords: ["fee", "cost", "expense"] },
+ { area: "assets", keywords: ["asset", "portfolio", "holdings", "allocation"] },
+ { area: "volume", keywords: ["volume", "throughput", "flow", "velocity"] },
+ { area: "health", keywords: ["health", "stability", "ledger", "network"] },
+ ]
+ .filter(({ keywords }) => keywords.some((keyword) => lower.includes(keyword)))
+ .map(({ area }) => area);
+
+ const resource: ReportResource = lower.includes("ledger") || lower.includes("network") || lower.includes("validator")
+ ? "ledgers"
+ : lower.includes("payment") || lower.includes("transfer")
+ ? "payments"
+ : lower.includes("operation")
+ ? "operations"
+ : "accounts";
+
+ return {
+ templateId,
+ accountId: accountIdMatch?.[1],
+ focusAreas: focusAreas.length > 0 ? focusAreas : ["summary"],
+ frequency,
+ resource,
+ prompt: normalizedRequest,
+ };
+}
+
+export function saveReportTemplate(template: ReportTemplate) {
+ const stored = readSavedReportTemplatesFromStorage();
+ const nextTemplates = [...stored.filter((item) => item.id !== template.id), template];
+ savedReportTemplateMemory = nextTemplates;
+
+ if (typeof globalThis !== "undefined") {
+ const storage = (globalThis as typeof globalThis & { localStorage?: Storage }).localStorage;
+ if (storage) {
+ storage.setItem(SAVED_REPORT_TEMPLATE_STORAGE_KEY, JSON.stringify(nextTemplates));
+ }
+ }
+
+ return template;
+}
+
+export function loadSavedReportTemplates() {
+ return readSavedReportTemplatesFromStorage();
}
export function buildHorizonQuery(config: HorizonQueryConfig): BuiltHorizonQuery {
@@ -198,7 +536,43 @@ export function buildHorizonQuery(config: HorizonQueryConfig): BuiltHorizonQuery
};
}
-export function transformReportData(templateId: string, dataSet: ReportDataSet): TransformedReportData {
+function buildNarrativeInsights(
+ template: ReportTemplate,
+ metrics: Array<{ label: string; value: string | number }>,
+ rows: Array>,
+ focusAreas: string[],
+ dataSet: ReportDataSet,
+) {
+ const insights: string[] = [];
+
+ if (metrics.length > 0) {
+ const firstMetric = metrics[0];
+ insights.push(`${firstMetric.label} is ${firstMetric.value} in the current snapshot.`);
+ }
+
+ if (focusAreas.includes("risks") && rows.length > 0) {
+ const activeSignals = rows.filter((row) => row.active === true || row.risk === true || row.label?.toLowerCase().includes("risk"));
+ if (activeSignals.length > 0) {
+ insights.push(`Risk signals are visible across ${activeSignals.length} entry${activeSignals.length === 1 ? "" : "ies"}.`);
+ }
+ }
+
+ if (focusAreas.includes("fees") && dataSet.network?.baseFee) {
+ insights.push(`The current base fee is ${dataSet.network.baseFee} stroops.`);
+ }
+
+ if (template.aiHint) {
+ insights.push(template.aiHint);
+ }
+
+ return insights.slice(0, 3);
+}
+
+export function transformReportData(
+ templateId: string,
+ dataSet: ReportDataSet,
+ options?: { focusAreas?: string[]; request?: string },
+): TransformedReportData {
const template = getReportTemplate(templateId);
const source = {
account: dataSet.account || {},
@@ -223,6 +597,7 @@ export function transformReportData(templateId: string, dataSet: ReportDataSet):
const chartData = normalizeRowSet(getNestedValue(source, chartComponent?.dataKey || "activity"));
const rows = normalizeRowSet(getNestedValue(source, tableComponent?.dataKey || template.resource));
const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row)))).slice(0, 8);
+ const focusAreas = options?.focusAreas || [];
return {
templateId: template.id,
@@ -231,6 +606,9 @@ export function transformReportData(templateId: string, dataSet: ReportDataSet):
chartData,
rows,
columns,
+ insights: buildNarrativeInsights(template, metrics, rows, focusAreas, dataSet),
+ summary: options?.request ? `AI-generated ${template.name} summary based on: ${options.request}` : `${template.name} summary`,
+ focusAreas,
};
}
@@ -289,6 +667,7 @@ export function createPdfReportPayload(template: ReportTemplate, data: Transform
const metricHtml = data.metrics
.map((metric) => `${metric.label} : ${metric.value} `)
.join("");
+ const insightHtml = (data.insights || []).map((insight) => `${insight} `).join("");
const tableHead = data.columns.map((column) => `${column} `).join("");
const tableBody = data.rows
.slice(0, 25)
@@ -298,7 +677,7 @@ export function createPdfReportPayload(template: ReportTemplate, data: Transform
return {
filename: `${sanitizeFilePart(template.name)}.pdf.html`,
contentType: "text/html",
- html: `${template.name} ${template.name} ${template.description}
Metrics Data `,
+ html: `${template.name} ${template.name} ${template.description}
Metrics Insights Data `,
chartData: data.chartData,
};
}
From f4cfc09d86f362e15e8cdaa9fd7d3311132546da Mon Sep 17 00:00:00 2001
From: Abba073 <168185628+Abba073@users.noreply.github.com>
Date: Mon, 27 Jul 2026 18:43:38 +0000
Subject: [PATCH 3/3] fix: resolve JSX parse error in AdvancedSearch.tsx
The {!semanticMode && ( conditional block opened at line 422 was not
closed correctly. A stray )} and orphan comment were placed after the
Search History section (line 921-922) instead of immediately after the
search bar at line 784.
Move the closing )} to line 785 (right after the search bar div ends)
so the Search History block remains outside the !semanticMode wrapper,
matching the original structure.
This fixes the two TypeScript parse errors blocking CI:
TS1005: ')' expected at AdvancedSearch.tsx(786,7)
TS1381: Unexpected token at AdvancedSearch.tsx(922,8)
---
src/components/dashboard/AdvancedSearch.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/components/dashboard/AdvancedSearch.tsx b/src/components/dashboard/AdvancedSearch.tsx
index e2a7d745..8ac3ab80 100644
--- a/src/components/dashboard/AdvancedSearch.tsx
+++ b/src/components/dashboard/AdvancedSearch.tsx
@@ -782,6 +782,7 @@ export default function AdvancedSearch() {
)}
+ )}
{/* Search History */}
{showHistory && (
@@ -918,8 +919,6 @@ export default function AdvancedSearch() {
)}
- {/* end keyword search bar */}
- )}
{/* Search Results */}
{!semanticMode && searchResults && (