Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 175 additions & 2 deletions dashboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import os

from dotenv import load_dotenv
from fasthtml.common import Link, Script, fast_app, serve
from fasthtml.common import Link, Script, Style, fast_app, serve
from monsterui.all import *

from dashboard.constants import POSTHOG_SCRIPT
Expand Down Expand Up @@ -56,7 +56,180 @@ def load_env_with_custom_path():
app, rt = fast_app(
hdrs=(
Theme.blue.headers(),
Script(src="https://cdn.plot.ly/plotly-2.32.0.min.js"),
Script(src="https://cdn.jsdelivr.net/npm/apexcharts@latest"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Consider using a specific version for the ApexCharts CDN

Using @latest in the CDN URL could lead to unexpected breaking changes in production. Pin to a specific version for stability.

-        Script(src="https://cdn.jsdelivr.net/npm/apexcharts@latest"),
+        Script(src="https://cdn.jsdelivr.net/npm/apexcharts@3.45.0"),
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Script(src="https://cdn.jsdelivr.net/npm/apexcharts@latest"),
Script(src="https://cdn.jsdelivr.net/npm/apexcharts@3.45.0"),
πŸ€– Prompt for AI Agents
In dashboard/app.py at line 59, the ApexCharts CDN URL uses '@latest', which can
cause instability due to unexpected breaking changes. Replace '@latest' with a
specific version number to ensure consistent and stable behavior in production.

Style("""
uk-chart {
display: block !important;
width: 100% !important;
min-height: 300px !important;
height: auto !important;
}
uk-chart .apexcharts-canvas {
Comment on lines +61 to +67

Copilot AI Aug 7, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CSS selector 'uk-chart' targets a custom element but lacks specificity. Consider using a more specific selector like '.uk-chart' or 'uk-chart[data-chart]' to avoid potential conflicts with other elements.

Suggested change
uk-chart {
display: block !important;
width: 100% !important;
min-height: 300px !important;
height: auto !important;
}
uk-chart .apexcharts-canvas {
uk-chart[data-chart-initialized] {
display: block !important;
width: 100% !important;
min-height: 300px !important;
height: auto !important;
}
uk-chart[data-chart-initialized] .apexcharts-canvas {

Copilot uses AI. Check for mistakes.
width: 100% !important;
height: 100% !important;
}
/* Sparklines should fill their grid cell for consistent alignment */
/* We now size sparkline charts via JS by detecting chart.sparkline.enabled */
.sparkline-cell uk-chart[id^="sparkline-"] {
min-height: 32px !important;
height: 32px !important;
width: 260px !important;
}
.sparkline-cell uk-chart[id^="sparkline-"] .apexcharts-canvas {
width: 100% !important;
height: 100% !important;
}
.sparkline-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
"""),
Script("""
const __chartInitState = {
observer: null,
seen: new WeakSet(),
};

function renderChartElement(el) {
const script = el.querySelector('script[type="application/json"]');
if (!script) return false;
if (el.hasAttribute('data-chart-initialized')) return true;
try {
const config = JSON.parse(script.textContent);
if (config.yaxis && config.yaxis[1]) {
config.yaxis[1].labels = config.yaxis[1].labels || {};
config.yaxis[1].labels.formatter = function(val) { return Math.round(val * 100) + '%'; };
}
const isSpark = !!(config.chart && config.chart.sparkline && config.chart.sparkline.enabled);
el.style.display = 'block';
el.style.width = '100%';
el.style.minHeight = isSpark ? '32px' : '300px';
el.style.height = isSpark ? '32px' : 'auto';
// Ensure ApexCharts receives an explicit width to avoid 0-width renders
const width = Math.max(220, Math.floor(el.getBoundingClientRect().width || el.clientWidth || 0));
config.chart = config.chart || {};
config.chart.width = width;
config.chart.height = isSpark ? 32 : (config.chart.height || 300);
if (typeof ApexCharts === 'undefined') return false;
const chart = new ApexCharts(el, config);
chart.render().then(() => el.setAttribute('data-chart-initialized', 'true'));
return true;
} catch (e) {
console.error('renderChartElement error', e, el);
return false;
}
}

function ensureChartObserver() {
if (__chartInitState.observer) return __chartInitState.observer;
__chartInitState.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
const el = entry.target;
if (!entry.isIntersecting) return;
if (__chartInitState.seen.has(el) || el.hasAttribute('data-chart-initialized')) {
__chartInitState.observer.unobserve(el);
return;
}
const script = el.querySelector('script[type="application/json"]');
if (!script) return;
try {
const config = JSON.parse(script.textContent);
if (config.yaxis && config.yaxis[1]) {
config.yaxis[1].labels = config.yaxis[1].labels || {};
config.yaxis[1].labels.formatter = function(val) { return Math.round(val * 100) + '%'; };
}
const isSpark = !!(config.chart && config.chart.sparkline && config.chart.sparkline.enabled);
el.style.display = 'block';
el.style.width = '100%';
el.style.minHeight = isSpark ? '32px' : '300px';
el.style.height = isSpark ? '32px' : 'auto';
// Explicit width
const width = Math.max(220, Math.floor(el.getBoundingClientRect().width || el.clientWidth || 0));
config.chart = config.chart || {};
config.chart.width = width;
config.chart.height = isSpark ? 32 : (config.chart.height || 300);
requestAnimationFrame(() => {
const chart = new ApexCharts(el, config);
chart.render().then(() => {
el.setAttribute('data-chart-initialized', 'true');
__chartInitState.seen.add(el);
__chartInitState.observer.unobserve(el);
});
});
} catch (e) {
console.error('Error initializing chart:', e, el);
}
});
}, { root: null, rootMargin: '200px 0px', threshold: 0.01 });
return __chartInitState.observer;
}

function initializeCharts() {
// Prefer immediate render inside anomaly list to avoid races
const anomalyContainer = document.getElementById('anomaly-list');
if (anomalyContainer) {
const charts = Array.from(anomalyContainer.querySelectorAll('uk-chart:not([data-chart-initialized])'));
let rendered = 0;
charts.forEach((el) => { if (renderChartElement(el)) rendered++; });
if (rendered < charts.length) {
// Retry after a short delay (e.g. if ApexCharts not ready yet)
setTimeout(() => {
charts.forEach((el) => { if (!el.hasAttribute('data-chart-initialized')) renderChartElement(el); });
}, 300);
}
return; // Skip observer path for anomalies page
}

// Fallback: use lazy observer for other pages
const observer = ensureChartObserver();
document.querySelectorAll('uk-chart:not([data-chart-initialized])').forEach((el) => observer.observe(el));
}

// Wait for both DOM and ApexCharts to be ready
document.addEventListener('DOMContentLoaded', function() {
// Check if ApexCharts is loaded
if (typeof ApexCharts !== 'undefined') {
initializeCharts();
} else {
// Wait a bit more for ApexCharts to load
setTimeout(() => {
if (typeof ApexCharts !== 'undefined') {
initializeCharts();
} else {
console.error('ApexCharts library not loaded');
}
}, 1000);
}
});

// Re-initialize charts after HTMX requests
document.addEventListener('htmx:afterSwap', initializeCharts);
document.addEventListener('htmx:afterSettle', initializeCharts);
// Debug: log counts and row metadata after HTMX updates
document.addEventListener('htmx:afterSettle', function() {
try {
const container = document.getElementById('anomaly-list');
if (!container) return;
const rows = Array.from(container.querySelectorAll('[data-row]'));
const charts = Array.from(container.querySelectorAll('uk-chart'));
const visibleCharts = charts.filter(c => c.offsetParent !== null);
console.log('[anomstack] rows:', rows.length, 'charts:', charts.length, 'visibleCharts:', visibleCharts.length);
rows.slice(0, 5).forEach(r => {
const ds = r.dataset || {};
console.log('[row]', ds.row, ds.metric, ds.ts);
});
const initialized = container.querySelectorAll('uk-chart[data-chart-initialized]');
console.log('[anomstack] initialized charts:', initialized.length);
// Force initialize any visible charts not initialized yet (safety net)
if (visibleCharts.length && initialized.length < visibleCharts.length) {
visibleCharts.forEach((el) => { if (!el.hasAttribute('data-chart-initialized')) renderChartElement(el); });
}
} catch (e) { console.warn('debug error', e); }
});
"""),
Script(POSTHOG_SCRIPT) if posthog_api_key else None,
Link(
rel="icon",
Expand Down
Loading