@@ -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$
555438BEGIN
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