Skip to content

Commit 976bc48

Browse files
fix(seed): resolve dashboard JSON escaping with external HTML file
The dashboard config was being stored with escaped backslashes (\n, \`) which caused JavaScript syntax errors when the dashboard tried to render. Changes: - Create demo-dashboard.html as external file with clean JS template literals - Use Python to JSON-encode the HTML and generate SQL with dollar-quoting - Dollar-quoting ($...$) avoids all psql backslash interpretation - Copy SQL file into postgres container for execution This fixes the 'invalid command' errors from psql and ensures the dashboard config is stored with proper JSON escaping that JavaScript can parse. Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent a994735 commit 976bc48

2 files changed

Lines changed: 172 additions & 155 deletions

File tree

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<style>
6+
* { box-sizing: border-box; margin: 0; padding: 0; }
7+
body { font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; background: #f8f9fa; padding: 24px; }
8+
.dashboard { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; max-width: 1400px; margin: 0 auto; }
9+
.widget { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
10+
.widget-full { grid-column: span 2; }
11+
.widget h3 { font-size: 14px; font-weight: 600; color: #374151; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
12+
.metric { font-size: 32px; font-weight: 700; color: #111827; }
13+
.metric-label { font-size: 12px; color: #6b7280; margin-top: 4px; }
14+
.metric-row { display: flex; gap: 40px; }
15+
.metric-item { flex: 1; }
16+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
17+
th { text-align: left; padding: 10px 12px; background: #f9fafb; color: #6b7280; font-weight: 500; border-bottom: 1px solid #e5e7eb; }
18+
td { padding: 10px 12px; border-bottom: 1px solid #f3f4f6; color: #374151; }
19+
tr:hover { background: #f9fafb; }
20+
.bar-chart { display: flex; flex-direction: column; gap: 8px; }
21+
.bar-row { display: flex; align-items: center; gap: 12px; }
22+
.bar-label { width: 100px; font-size: 12px; color: #6b7280; text-align: right; }
23+
.bar-container { flex: 1; height: 24px; background: #f3f4f6; border-radius: 4px; overflow: hidden; }
24+
.bar { height: 100%; background: linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%); border-radius: 4px; transition: width 0.3s; }
25+
.bar-value { width: 80px; font-size: 12px; color: #374151; font-weight: 500; }
26+
.loading { color: #9ca3af; font-style: italic; }
27+
.error { color: #ef4444; font-size: 12px; }
28+
</style>
29+
</head>
30+
<body>
31+
<div class="dashboard">
32+
<div class="widget">
33+
<h3>📊 Key Metrics</h3>
34+
<div id="metrics" class="metric-row"><span class="loading">Loading...</span></div>
35+
</div>
36+
<div class="widget">
37+
<h3>📈 Orders by Status</h3>
38+
<div id="status-chart" class="bar-chart"><span class="loading">Loading...</span></div>
39+
</div>
40+
<div class="widget widget-full">
41+
<h3>💰 Daily Revenue (Last 14 Days)</h3>
42+
<div id="revenue-table"><span class="loading">Loading...</span></div>
43+
</div>
44+
<div class="widget widget-full">
45+
<h3>🏆 Top 10 Products by Revenue</h3>
46+
<div id="products-table"><span class="loading">Loading...</span></div>
47+
</div>
48+
</div>
49+
<script>
50+
async function query(sql) {
51+
return deepsql.query(sql);
52+
}
53+
async function loadDashboard() {
54+
try {
55+
const metrics = await query(`
56+
SELECT
57+
(SELECT COUNT(*) FROM orders) as total_orders,
58+
(SELECT SUM(total_amount) FROM orders WHERE status != 'cancelled') as total_revenue,
59+
(SELECT COUNT(*) FROM customers) as total_customers,
60+
(SELECT COUNT(*) FROM products WHERE is_active = true) as active_products
61+
`);
62+
if (metrics.rows && metrics.rows[0]) {
63+
const m = metrics.rows[0];
64+
document.getElementById("metrics").innerHTML = `
65+
<div class="metric-item"><div class="metric">${Number(m.total_orders || 0).toLocaleString()}</div><div class="metric-label">Total Orders</div></div>
66+
<div class="metric-item"><div class="metric">$${Number(m.total_revenue || 0).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}</div><div class="metric-label">Total Revenue</div></div>
67+
<div class="metric-item"><div class="metric">${Number(m.total_customers || 0).toLocaleString()}</div><div class="metric-label">Customers</div></div>
68+
<div class="metric-item"><div class="metric">${Number(m.active_products || 0).toLocaleString()}</div><div class="metric-label">Active Products</div></div>
69+
`;
70+
}
71+
72+
const statusData = await query(`
73+
SELECT status, COUNT(*) as count
74+
FROM orders
75+
GROUP BY status
76+
ORDER BY count DESC
77+
`);
78+
if (statusData.rows && statusData.rows.length > 0) {
79+
const maxCount = Math.max(...statusData.rows.map(r => Number(r.count)));
80+
document.getElementById("status-chart").innerHTML = statusData.rows.map(row => `
81+
<div class="bar-row">
82+
<div class="bar-label">${row.status}</div>
83+
<div class="bar-container"><div class="bar" style="width: ${(Number(row.count) / maxCount * 100)}%"></div></div>
84+
<div class="bar-value">${Number(row.count).toLocaleString()}</div>
85+
</div>
86+
`).join("");
87+
}
88+
89+
const revenue = await query(`
90+
SELECT DATE(created_at) as day, COUNT(*) as orders, SUM(total_amount) as revenue
91+
FROM orders WHERE status NOT IN ('cancelled', 'refunded')
92+
AND created_at >= CURRENT_DATE - INTERVAL '14 days'
93+
GROUP BY DATE(created_at) ORDER BY day DESC
94+
`);
95+
if (revenue.rows && revenue.rows.length > 0) {
96+
document.getElementById("revenue-table").innerHTML = `<table>
97+
<thead><tr><th>Date</th><th>Orders</th><th>Revenue</th></tr></thead>
98+
<tbody>${revenue.rows.map(r => `<tr><td>${r.day}</td><td>${Number(r.orders).toLocaleString()}</td><td>$${Number(r.revenue || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}</td></tr>`).join("")}</tbody>
99+
</table>`;
100+
}
101+
102+
const products = await query(`
103+
SELECT p.name, p.sku, SUM(oi.quantity) as units, SUM(oi.subtotal) as revenue
104+
FROM products p JOIN order_items oi ON p.id = oi.product_id
105+
JOIN orders o ON oi.order_id = o.id WHERE o.status NOT IN ('cancelled', 'refunded')
106+
GROUP BY p.id, p.name, p.sku ORDER BY revenue DESC LIMIT 10
107+
`);
108+
if (products.rows && products.rows.length > 0) {
109+
document.getElementById("products-table").innerHTML = `<table>
110+
<thead><tr><th>Product</th><th>SKU</th><th>Units Sold</th><th>Revenue</th></tr></thead>
111+
<tbody>${products.rows.map(r => `<tr><td>${r.name}</td><td>${r.sku}</td><td>${Number(r.units).toLocaleString()}</td><td>$${Number(r.revenue || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}</td></tr>`).join("")}</tbody>
112+
</table>`;
113+
}
114+
} catch (err) {
115+
console.error("Dashboard error:", err);
116+
document.querySelectorAll(".loading").forEach(el => el.innerHTML = `<span class="error">Failed to load: ${err.message || "Unknown error"}</span>`);
117+
}
118+
}
119+
loadDashboard();
120+
</script>
121+
</body>
122+
</html>

‎scripts/self-host/seed-demo-data.sh‎

Lines changed: 50 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -400,174 +400,57 @@ if [[ -n "${connection_id:-}" ]]; then
400400
"SELECT id FROM users WHERE email = '${DEEPSQL_INITIAL_ADMIN_EMAIL}' LIMIT 1" 2>/dev/null || echo "")"
401401

402402
if [[ -n "$admin_id" ]]; then
403-
# Create the sample dashboard HTML artifact
404-
dashboard_html='<!DOCTYPE html>
405-
<html>
406-
<head>
407-
<meta charset="UTF-8">
408-
<style>
409-
* { box-sizing: border-box; margin: 0; padding: 0; }
410-
body { font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif; background: #f8f9fa; padding: 24px; }
411-
.dashboard { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; max-width: 1400px; margin: 0 auto; }
412-
.widget { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
413-
.widget-full { grid-column: span 2; }
414-
.widget h3 { font-size: 14px; font-weight: 600; color: #374151; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
415-
.metric { font-size: 32px; font-weight: 700; color: #111827; }
416-
.metric-label { font-size: 12px; color: #6b7280; margin-top: 4px; }
417-
.metric-row { display: flex; gap: 40px; }
418-
.metric-item { flex: 1; }
419-
table { width: 100%; border-collapse: collapse; font-size: 13px; }
420-
th { text-align: left; padding: 10px 12px; background: #f9fafb; color: #6b7280; font-weight: 500; border-bottom: 1px solid #e5e7eb; }
421-
td { padding: 10px 12px; border-bottom: 1px solid #f3f4f6; color: #374151; }
422-
tr:hover { background: #f9fafb; }
423-
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; }
424-
.status-delivered { background: #d1fae5; color: #065f46; }
425-
.status-shipped { background: #dbeafe; color: #1e40af; }
426-
.status-processing { background: #fef3c7; color: #92400e; }
427-
.status-pending { background: #f3f4f6; color: #4b5563; }
428-
.bar-chart { display: flex; flex-direction: column; gap: 8px; }
429-
.bar-row { display: flex; align-items: center; gap: 12px; }
430-
.bar-label { width: 100px; font-size: 12px; color: #6b7280; text-align: right; }
431-
.bar-container { flex: 1; height: 24px; background: #f3f4f6; border-radius: 4px; overflow: hidden; }
432-
.bar { height: 100%; background: linear-gradient(90deg, #3b82f6 0%, #1d4ed8 100%); border-radius: 4px; transition: width 0.3s; }
433-
.bar-value { width: 80px; font-size: 12px; color: #374151; font-weight: 500; }
434-
.loading { color: #9ca3af; font-style: italic; }
435-
.error { color: #ef4444; font-size: 12px; }
436-
</style>
437-
</head>
438-
<body>
439-
<div class="dashboard">
440-
<div class="widget">
441-
<h3>📊 Key Metrics</h3>
442-
<div id="metrics" class="metric-row"><span class="loading">Loading...</span></div>
443-
</div>
444-
<div class="widget">
445-
<h3>📈 Orders by Status</h3>
446-
<div id="status-chart" class="bar-chart"><span class="loading">Loading...</span></div>
447-
</div>
448-
<div class="widget widget-full">
449-
<h3>💰 Daily Revenue (Last 14 Days)</h3>
450-
<div id="revenue-table"><span class="loading">Loading...</span></div>
451-
</div>
452-
<div class="widget widget-full">
453-
<h3>🏆 Top 10 Products by Revenue</h3>
454-
<div id="products-table"><span class="loading">Loading...</span></div>
455-
</div>
456-
</div>
457-
<script>
458-
async function query(sql) {
459-
return deepsql.query(sql);
403+
# Read dashboard HTML from separate file and create dashboard via temp SQL file
404+
# This approach avoids all shell/psql escaping issues with JSON containing JS template literals
405+
dashboard_html_file="$SCRIPT_DIR/demo-dashboard.html"
406+
if [[ -f "$dashboard_html_file" ]]; then
407+
# Create SQL file with Python (handles all escaping correctly)
408+
sql_file="$(mktemp)"
409+
python3 - "$dashboard_html_file" "$connection_id" "$admin_id" > "$sql_file" <<'PYEOF'
410+
import json
411+
import sys
412+
413+
html_file = sys.argv[1]
414+
connection_id = sys.argv[2]
415+
admin_id = sys.argv[3]
416+
417+
# Read HTML from file
418+
with open(html_file, 'r') as f:
419+
html = f.read()
420+
421+
# Build JSON config
422+
config = {
423+
'version': 3,
424+
'renderMode': 'artifact',
425+
'title': 'Demo Shop Overview',
426+
'html': html,
427+
'summary': 'A sample dashboard showing key metrics, orders by status, daily revenue, and top products from the Demo Shop database. This dashboard works without an LLM key.'
460428
}
461-
async function loadDashboard() {
462-
try {
463-
// Key Metrics
464-
const metrics = await query(\`
465-
SELECT
466-
(SELECT COUNT(*) FROM orders) as total_orders,
467-
(SELECT SUM(total_amount) FROM orders WHERE status != '"'"'cancelled'"'"') as total_revenue,
468-
(SELECT COUNT(*) FROM customers) as total_customers,
469-
(SELECT COUNT(*) FROM products WHERE is_active = true) as active_products
470-
\`);
471-
if (metrics.rows && metrics.rows[0]) {
472-
const m = metrics.rows[0];
473-
document.getElementById("metrics").innerHTML = \`
474-
<div class="metric-item"><div class="metric">\${Number(m.total_orders || 0).toLocaleString()}</div><div class="metric-label">Total Orders</div></div>
475-
<div class="metric-item"><div class="metric">$\${Number(m.total_revenue || 0).toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0})}</div><div class="metric-label">Total Revenue</div></div>
476-
<div class="metric-item"><div class="metric">\${Number(m.total_customers || 0).toLocaleString()}</div><div class="metric-label">Customers</div></div>
477-
<div class="metric-item"><div class="metric">\${Number(m.active_products || 0).toLocaleString()}</div><div class="metric-label">Active Products</div></div>
478-
\`;
479-
}
480-
481-
// Orders by Status
482-
const statusData = await query(\`
483-
SELECT status, COUNT(*) as count
484-
FROM orders
485-
GROUP BY status
486-
ORDER BY count DESC
487-
\`);
488-
if (statusData.rows && statusData.rows.length > 0) {
489-
const maxCount = Math.max(...statusData.rows.map(r => Number(r.count)));
490-
document.getElementById("status-chart").innerHTML = statusData.rows.map(row => \`
491-
<div class="bar-row">
492-
<div class="bar-label">\${row.status}</div>
493-
<div class="bar-container"><div class="bar" style="width: \${(Number(row.count) / maxCount * 100)}%"></div></div>
494-
<div class="bar-value">\${Number(row.count).toLocaleString()}</div>
495-
</div>
496-
\`).join("");
497-
}
498-
499-
// Daily Revenue
500-
const revenue = await query(\`
501-
SELECT DATE(created_at) as day, COUNT(*) as orders, SUM(total_amount) as revenue
502-
FROM orders WHERE status NOT IN ('"'"'cancelled'"'"', '"'"'refunded'"'"')
503-
AND created_at >= CURRENT_DATE - INTERVAL '"'"'14 days'"'"'
504-
GROUP BY DATE(created_at) ORDER BY day DESC
505-
\`);
506-
if (revenue.rows && revenue.rows.length > 0) {
507-
document.getElementById("revenue-table").innerHTML = \`<table>
508-
<thead><tr><th>Date</th><th>Orders</th><th>Revenue</th></tr></thead>
509-
<tbody>\${revenue.rows.map(r => \`<tr><td>\${r.day}</td><td>\${Number(r.orders).toLocaleString()}</td><td>$\${Number(r.revenue || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}</td></tr>\`).join("")}</tbody>
510-
</table>\`;
511-
}
512-
513-
// Top Products
514-
const products = await query(\`
515-
SELECT p.name, p.sku, SUM(oi.quantity) as units, SUM(oi.subtotal) as revenue
516-
FROM products p JOIN order_items oi ON p.id = oi.product_id
517-
JOIN orders o ON oi.order_id = o.id WHERE o.status NOT IN ('"'"'cancelled'"'"', '"'"'refunded'"'"')
518-
GROUP BY p.id, p.name, p.sku ORDER BY revenue DESC LIMIT 10
519-
\`);
520-
if (products.rows && products.rows.length > 0) {
521-
document.getElementById("products-table").innerHTML = \`<table>
522-
<thead><tr><th>Product</th><th>SKU</th><th>Units Sold</th><th>Revenue</th></tr></thead>
523-
<tbody>\${products.rows.map(r => \`<tr><td>\${r.name}</td><td>\${r.sku}</td><td>\${Number(r.units).toLocaleString()}</td><td>$\${Number(r.revenue || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}</td></tr>\`).join("")}</tbody>
524-
</table>\`;
525-
}
526-
} catch (err) {
527-
console.error("Dashboard error:", err);
528-
document.querySelectorAll(".loading").forEach(el => el.innerHTML = \`<span class="error">Failed to load: \${err.message || "Unknown error"}</span>\`);
529-
}
530-
}
531-
loadDashboard();
532-
</script>
533-
</body>
534-
</html>'
535429
536-
# Escape for SQL
537-
dashboard_html_escaped="${dashboard_html//\'/\'\'}"
538-
539-
dashboard_config=$(cat <<JSON
540-
{
541-
"version": 3,
542-
"renderMode": "artifact",
543-
"title": "Demo Shop Overview",
544-
"html": "${dashboard_html_escaped}",
545-
"summary": "A sample dashboard showing key metrics, orders by status, daily revenue, and top products from the Demo Shop database. This dashboard works without an LLM key."
546-
}
547-
JSON
548-
)
549-
# Escape the JSON for SQL
550-
dashboard_config_escaped="${dashboard_config//\'/\'\'}"
430+
# For PostgreSQL dollar-quoting, we only need to escape $dashboard$ if it appears in the JSON
431+
# (it won't, but this is safe). No backslash escaping needed with dollar-quoting.
432+
json_str = json.dumps(config)
551433
552-
compose exec -T postgres psql -U postgres -d dba_agent -v ON_ERROR_STOP=1 <<EOSQL
553-
-- Check if sample dashboard already exists
554-
DO \$\$
434+
# Generate SQL using dollar-quoting for the JSON to avoid all escaping issues
435+
# The $dashcfg$...$dashcfg$ delimiter won't appear in the JSON
436+
sql = f"""-- Check if sample dashboard already exists
437+
DO $do$
555438
BEGIN
556439
IF NOT EXISTS (
557440
SELECT 1 FROM saved_dashboards
558-
WHERE connection_id = '${connection_id}'
441+
WHERE connection_id = '{connection_id}'
559442
AND name = 'Demo Shop Overview'
560443
) THEN
561444
INSERT INTO saved_dashboards (
562445
id, connection_id, user_id, name, description, dashboard_config,
563446
tags, is_favorite, is_public, generation_status, created_at, updated_at, version
564447
) VALUES (
565448
gen_random_uuid(),
566-
'${connection_id}',
567-
${admin_id},
449+
'{connection_id}',
450+
{admin_id},
568451
'Demo Shop Overview',
569452
'Sample dashboard showing key e-commerce metrics. Works without an LLM key.',
570-
'${dashboard_config_escaped}'::text,
453+
$dashcfg${json_str}$dashcfg$::text,
571454
'demo,sample,overview',
572455
true,
573456
false,
@@ -580,8 +463,20 @@ BEGIN
580463
ELSE
581464
RAISE NOTICE 'Sample dashboard already exists';
582465
END IF;
583-
END \$\$;
584-
EOSQL
466+
END $do$;
467+
"""
468+
print(sql)
469+
PYEOF
470+
471+
# Copy the SQL file into the container and run it
472+
# This avoids psql interpreting backslashes in stdin mode
473+
compose cp "$sql_file" postgres:/tmp/dashboard.sql
474+
compose exec -T postgres psql -U postgres -d dba_agent -v ON_ERROR_STOP=1 -f /tmp/dashboard.sql
475+
compose exec -T postgres rm -f /tmp/dashboard.sql
476+
rm -f "$sql_file"
477+
else
478+
echo " Warning: demo-dashboard.html not found, skipping dashboard creation"
479+
fi
585480
echo " Sample dashboard created."
586481
else
587482
echo " Skipping dashboard (admin user not found)"

0 commit comments

Comments
 (0)