Initial spin-off of bambu-run from my private project separation

This commit is contained in:
RNL
2026-02-15 23:51:36 +11:00
parent 37c84fcd9f
commit 441db069c5
43 changed files with 7295 additions and 1 deletions

View File

@@ -0,0 +1,61 @@
/* Bambu Run Dashboard Styles */
.chart-container {
position: relative;
height: 300px;
}
/* Card styling */
.infra-card-warning {
background: linear-gradient(135deg, #ffc107 0%, #ffb300 100%);
color: #000;
}
.infra-card-info {
background: linear-gradient(135deg, #0dcaf0 0%, #0bb5d6 100%);
color: #000;
}
.infra-card-danger {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: #fff;
}
.infra-card-success {
background: linear-gradient(135deg, #198754 0%, #157347 100%);
color: #fff;
}
/* Dark mode adjustments */
[data-coreui-theme="dark"] .infra-card-warning {
background: linear-gradient(135deg, #ffb300 0%, #ff8f00 100%);
color: #fff;
}
[data-coreui-theme="dark"] .infra-card-info {
background: linear-gradient(135deg, #0bb5d6 0%, #099cbd 100%);
color: #fff;
}
/* Stat display styling */
.stat-value {
font-size: 2rem;
font-weight: 700;
}
.stat-label {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.infra-card-warning .card-text,
.infra-card-info .card-text {
opacity: 0.75;
}
[data-coreui-theme="dark"] .infra-card-warning .card-text,
[data-coreui-theme="dark"] .infra-card-info .card-text {
opacity: 0.9;
color: rgba(255, 255, 255, 0.9);
}

View File

@@ -0,0 +1,61 @@
/**
* Dropdown-assisted text inputs for FilamentType add/edit form.
* Reads existing DB values and preset suggestions from json_script tags,
* then populates dropdown menus that fill the adjacent text input on click.
*/
/**
* Build a dropdown menu with existing DB values and preset suggestions.
* @param {string} dropdownId - ID of the <ul> dropdown menu element
* @param {string} inputId - ID of the text input to fill on click
* @param {Array<string>} existingValues - Values already in the database
* @param {Array<string>} presetValues - Pre-coded suggestion values
*/
function buildDropdown(dropdownId, inputId, existingValues, presetValues) {
const menu = document.getElementById(dropdownId);
// Add existing DB values
existingValues.forEach(val => {
const li = document.createElement('li');
li.innerHTML = `<a class="dropdown-item" href="#">${val}</a>`;
li.querySelector('a').addEventListener('click', e => {
e.preventDefault();
document.getElementById(inputId).value = val;
});
menu.appendChild(li);
});
// Add dotted separator if there were DB values
if (existingValues.length > 0) {
const sep = document.createElement('li');
sep.innerHTML = '<hr class="dropdown-divider" style="border-style: dotted;">';
menu.appendChild(sep);
}
// Add preset values (skip duplicates already in DB)
const existingSet = new Set(existingValues);
presetValues.forEach(val => {
if (existingSet.has(val)) return;
const li = document.createElement('li');
li.innerHTML = `<a class="dropdown-item text-muted" href="#">${val}</a>`;
li.querySelector('a').addEventListener('click', e => {
e.preventDefault();
document.getElementById(inputId).value = val;
});
menu.appendChild(li);
});
}
// Parse data from json_script tags and build all three dropdowns
document.addEventListener('DOMContentLoaded', () => {
const existingTypes = JSON.parse(document.getElementById('existing-types').textContent);
const existingSubTypes = JSON.parse(document.getElementById('existing-sub-types').textContent);
const existingBrands = JSON.parse(document.getElementById('existing-brands').textContent);
const presetTypes = JSON.parse(document.getElementById('preset-types').textContent);
const presetSubTypes = JSON.parse(document.getElementById('preset-sub-types').textContent);
const presetBrands = JSON.parse(document.getElementById('preset-brands').textContent);
buildDropdown('type-dropdown', 'id_type', existingTypes, presetTypes);
buildDropdown('sub-type-dropdown', 'id_sub_type', existingSubTypes, presetSubTypes);
buildDropdown('brand-dropdown', 'id_brand', existingBrands, presetBrands);
});

View File

@@ -0,0 +1,713 @@
// 3D Printer Charts Initialization and Management
// Chart.js implementation for printer metrics visualization
let nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart;
let wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart;
function initPrinterCharts(printerData, apiUrl) {
// Apply filament card colors
applyFilamentColors();
// Register the annotation plugin
if (typeof Chart !== 'undefined' && typeof ChartAnnotation !== 'undefined') {
Chart.register(ChartAnnotation);
}
// Detect current theme
const isDarkMode = document.documentElement.getAttribute('data-coreui-theme') === 'dark';
// Set colors based on theme
const tickColor = isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)';
const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
// Initialize Nozzle Temperature Chart
const nozzleCtx = document.getElementById('nozzleTempChart').getContext('2d');
nozzleTempChart = new Chart(nozzleCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: [
{
label: 'Actual Temp',
data: printerData.nozzle_temp,
borderColor: 'rgb(255, 159, 64)',
backgroundColor: 'rgba(255, 159, 64, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
},
{
label: 'Target Temp',
data: printerData.nozzle_target_temp,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.05)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
}
]
},
options: getTemperatureChartOptions(tickColor, gridColor, '°C')
});
// Initialize Bed Temperature Chart
const bedCtx = document.getElementById('bedTempChart').getContext('2d');
bedTempChart = new Chart(bedCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: [
{
label: 'Actual Temp',
data: printerData.bed_temp,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
},
{
label: 'Target Temp',
data: printerData.bed_target_temp,
borderColor: 'rgb(255, 159, 64)',
backgroundColor: 'rgba(255, 159, 64, 0.05)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
}
]
},
options: getTemperatureChartOptions(tickColor, gridColor, '°C')
});
// Initialize Print Progress Chart
const progressCtx = document.getElementById('printProgressChart').getContext('2d');
printProgressChart = new Chart(progressCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: [
{
label: 'Print Progress',
data: printerData.print_percent,
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
fill: true
}
]
},
options: getPercentageChartOptions(tickColor, gridColor, 'Print Progress')
});
// Initialize Fan Speeds Chart
const fanCtx = document.getElementById('fanSpeedsChart').getContext('2d');
fanSpeedsChart = new Chart(fanCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: [
{
label: 'Cooling Fan',
data: printerData.cooling_fan_speed,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
},
{
label: 'Heatbreak Fan',
data: printerData.heatbreak_fan_speed,
borderColor: 'rgb(153, 102, 255)',
backgroundColor: 'rgba(153, 102, 255, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
}
]
},
options: getPercentageChartOptions(tickColor, gridColor, 'Fan Speed')
});
// Initialize WiFi Signal Chart
const wifiCtx = document.getElementById('wifiSignalChart').getContext('2d');
wifiSignalChart = new Chart(wifiCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: [
{
label: 'WiFi Signal',
data: printerData.wifi_signal_dbm,
borderColor: 'rgb(255, 205, 86)',
backgroundColor: 'rgba(255, 205, 86, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
annotation: {
annotations: {}
},
legend: {
position: 'top',
labels: {
color: tickColor
}
},
tooltip: {
callbacks: {
label: function(context) {
return 'Signal: ' + context.parsed.y + ' dBm';
}
}
}
},
scales: {
x: {
ticks: { color: tickColor },
grid: { color: gridColor }
},
y: {
reverse: false, // -30 dBm (better) should be higher than -40 dBm (worse)
ticks: {
color: tickColor,
callback: function(value) {
return value + ' dBm';
}
},
grid: { color: gridColor }
}
}
}
});
// Initialize AMS Conditions Chart
const amsCtx = document.getElementById('amsConditionsChart').getContext('2d');
amsConditionsChart = new Chart(amsCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: [
{
label: 'Humidity (Raw)',
data: printerData.ams_humidity_raw,
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
yAxisID: 'y',
spanGaps: true
},
{
label: 'Temperature',
data: printerData.ams_temp,
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
yAxisID: 'y1',
spanGaps: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
annotation: {
annotations: {}
},
legend: {
position: 'top',
labels: {
color: tickColor
}
}
},
scales: {
x: {
ticks: { color: tickColor },
grid: { color: gridColor }
},
y: {
type: 'linear',
display: true,
position: 'left',
title: {
display: true,
text: 'Humidity',
color: tickColor
},
ticks: {
color: 'rgb(54, 162, 235)',
callback: function(value) {
return value;
}
},
grid: { color: gridColor }
},
y1: {
type: 'linear',
display: true,
position: 'right',
title: {
display: true,
text: 'Temperature (°C)',
color: tickColor
},
ticks: {
color: 'rgb(255, 99, 132)',
callback: function(value) {
return value + '°C';
}
},
grid: {
drawOnChartArea: false,
}
}
}
}
});
// Initialize Layer Progress Chart
const layerCtx = document.getElementById('layerProgressChart').getContext('2d');
layerProgressChart = new Chart(layerCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: [
{
label: 'Current Layer',
data: printerData.layer_num,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
fill: true
},
{
label: 'Total Layers',
data: printerData.total_layer_num,
borderColor: 'rgb(201, 203, 207)',
backgroundColor: 'rgba(201, 203, 207, 0.05)',
borderDash: [5, 5],
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
annotation: {
annotations: {}
},
legend: {
position: 'top',
labels: {
color: tickColor
}
}
},
scales: {
x: {
ticks: { color: tickColor },
grid: { color: gridColor }
},
y: {
beginAtZero: true,
ticks: {
color: tickColor,
stepSize: 1
},
grid: { color: gridColor }
}
}
}
});
// Initialize Filament Timeline Chart
const filamentCtx = document.getElementById('filamentTimelineChart').getContext('2d');
const filamentDatasets = createFilamentDatasets(printerData.filament_timeline, printerData.timestamps);
filamentTimelineChart = new Chart(filamentCtx, {
type: 'line',
data: {
labels: printerData.timestamps,
datasets: filamentDatasets
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
annotation: {
annotations: {}
},
legend: {
position: 'top',
labels: {
color: tickColor,
boxWidth: 12,
padding: 8
}
},
tooltip: {
callbacks: {
label: function(context) {
const datasetLabel = context.dataset.label || '';
const value = context.parsed.y;
return datasetLabel + ': ' + value + '% remaining';
}
}
}
},
scales: {
x: {
ticks: { color: tickColor },
grid: { color: gridColor }
},
y: {
min: -10, // Allow for negative filament readings (e.g., -4%)
max: 110, // 10% higher than 100% to make 100% line more visible
ticks: {
color: tickColor,
callback: function(value) {
return value + '%';
}
},
grid: { color: gridColor }
}
}
}
});
// Set up theme observer for dynamic theme switching
setupThemeObserver();
}
function getTemperatureChartOptions(tickColor, gridColor, unit) {
return {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
annotation: {
annotations: {}
},
legend: {
position: 'top',
labels: {
color: tickColor
}
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
label += context.parsed.y.toFixed(1) + unit;
}
return label;
}
}
}
},
scales: {
x: {
ticks: {
color: tickColor
},
grid: {
color: gridColor
}
},
y: {
beginAtZero: true,
ticks: {
color: tickColor,
callback: function(value) {
return value + unit;
}
},
grid: {
color: gridColor
}
}
}
};
}
function getPercentageChartOptions(tickColor, gridColor, label) {
return {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
annotation: {
annotations: {}
},
legend: {
position: 'top',
labels: {
color: tickColor
}
},
tooltip: {
callbacks: {
label: function(context) {
return label + ': ' + context.parsed.y + '%';
}
}
}
},
scales: {
x: {
ticks: {
color: tickColor
},
grid: {
color: gridColor
}
},
y: {
beginAtZero: true,
max: 100,
ticks: {
color: tickColor,
callback: function(value) {
return value + '%';
}
},
grid: {
color: gridColor
}
}
}
};
}
function createFilamentDatasets(filamentTimeline, timestamps) {
const datasets = [];
const filamentKeys = Object.keys(filamentTimeline);
// Convert to array for sorting
const filamentEntries = filamentKeys.map(key => ({
key: key,
data: filamentTimeline[key]
}));
// Sort by tray_id (numeric first, External last), then by start_idx (chronological)
filamentEntries.sort((a, b) => {
const trayA = a.data.tray_id;
const trayB = b.data.tray_id;
// Handle External vs numeric
if (trayA === 'External' && trayB !== 'External') return 1;
if (trayB === 'External' && trayA !== 'External') return -1;
if (trayA === 'External' && trayB === 'External') {
return a.data.start_idx - b.data.start_idx;
}
// Both numeric - sort by tray_id first, then by start_idx
const trayNumA = parseInt(trayA);
const trayNumB = parseInt(trayB);
if (trayNumA !== trayNumB) {
return trayNumA - trayNumB;
}
return a.data.start_idx - b.data.start_idx;
});
// Create datasets
filamentEntries.forEach(entry => {
const filament = entry.data;
const color = '#' + filament.color.substring(0, 6);
// Build descriptive label
let displayLabel;
if (filament.tray_id === 'External') {
displayLabel = `External (${filament.type})`;
} else {
displayLabel = `Tray ${filament.tray_id} (${filament.type})`;
}
// Add brand if it's different from type (avoid redundancy)
if (filament.brand && filament.brand !== filament.type && filament.brand !== 'External') {
displayLabel += ` - ${filament.brand}`;
}
datasets.push({
label: displayLabel,
data: filament.remain_data,
borderColor: color,
backgroundColor: hexToRgba(color, 0.1),
tension: 0.3,
borderWidth: 2,
pointRadius: 0,
pointHoverRadius: 5,
spanGaps: false // Don't connect across null values (filament changes)
});
});
return datasets;
}
function hexToRgba(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
function applyFilamentColors() {
// Apply colors to filament cards
document.querySelectorAll('.filament-card').forEach(card => {
const colorHex = card.getAttribute('data-filament-color');
if (colorHex) {
const color = '#' + colorHex;
// Set card background with gradient
card.style.background = `linear-gradient(135deg, ${hexToRgba(color, 0.12)} 0%, ${hexToRgba(color, 0.03)} 100%)`;
card.style.borderLeft = `4px solid ${color}`;
// Set badge color
const badge = card.querySelector('.filament-badge');
if (badge) {
badge.style.backgroundColor = color;
badge.style.color = getContrastColor(color);
}
// Set progress bar color
const progressBar = card.querySelector('.filament-progress');
if (progressBar) {
progressBar.style.backgroundColor = color;
}
}
});
}
function getContrastColor(hexColor) {
// Convert hex to RGB
const r = parseInt(hexColor.slice(1, 3), 16);
const g = parseInt(hexColor.slice(3, 5), 16);
const b = parseInt(hexColor.slice(5, 7), 16);
// Calculate luminance
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
// Return black or white based on luminance
return luminance > 0.5 ? '#000000' : '#ffffff';
}
function updateChartTheme() {
const isDarkMode = document.documentElement.getAttribute('data-coreui-theme') === 'dark';
const tickColor = isDarkMode ? 'rgba(255, 255, 255, 0.8)' : 'rgba(0, 0, 0, 0.8)';
const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
// Update all charts
const charts = [
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];
charts.forEach(chart => {
if (chart) {
// Update legend colors
chart.options.plugins.legend.labels.color = tickColor;
// Update x-axis colors
chart.options.scales.x.ticks.color = tickColor;
chart.options.scales.x.grid.color = gridColor;
// Update y-axis colors
if (chart.options.scales.y) {
chart.options.scales.y.ticks.color = tickColor;
chart.options.scales.y.grid.color = gridColor;
}
// Update y1-axis if exists (for dual-axis charts)
if (chart.options.scales.y1) {
if (chart.options.scales.y1.title) {
chart.options.scales.y1.title.color = tickColor;
}
}
chart.update();
}
});
}
function setupThemeObserver() {
// Watch for theme changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'data-coreui-theme') {
updateChartTheme();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-coreui-theme']
});
}

View File

@@ -0,0 +1,418 @@
// 3D Printer Charts Control - Date/Time Filtering and Project Markers
// Handles date range picker, time selection, and chart updates with annotations
// Global state
const printerChartControls = {
isFullDay: true,
isCustomRange: false,
apiUrl: null
};
/**
* Initialize on page load
*/
document.addEventListener('DOMContentLoaded', function() {
const apiUrlElement = document.getElementById('printerApiUrl');
if (apiUrlElement) {
printerChartControls.apiUrl = apiUrlElement.dataset.url;
initializePrinterControls();
}
});
/**
* Initialize printer chart date/time controls
*/
function initializePrinterControls() {
const startDateInput = document.getElementById('printerStartDate');
const endDateInput = document.getElementById('printerEndDate');
const startTimeSelect = document.getElementById('printerStartTime');
const endTimeSelect = document.getElementById('printerEndTime');
const fullDayCheckbox = document.getElementById('printerFullDayCheckbox');
const refreshBtn = document.getElementById('refreshPrinterCharts');
const resetBtn = document.getElementById('resetPrinterCharts');
// Set max date to today
const today = formatDate(new Date());
startDateInput.max = today;
endDateInput.max = today;
// Populate time dropdowns with 30-minute intervals
populateTimeDropdowns(startTimeSelect, endTimeSelect);
// Set default values
setDefaultPrinterDateTimeValues();
// Date input change handling
startDateInput.addEventListener('change', handlePrinterDateChange);
endDateInput.addEventListener('change', handlePrinterDateChange);
// Full Day checkbox toggle
fullDayCheckbox.addEventListener('change', function() {
printerChartControls.isFullDay = this.checked;
togglePrinterTimeControls(!this.checked);
updatePrinterDateRangeLabel();
});
// Refresh button
refreshBtn.addEventListener('click', function() {
refreshPrinterChartsData();
});
// Reset button
resetBtn.addEventListener('click', function() {
resetPrinterControls();
});
}
/**
* Populate time dropdowns with 30-minute intervals
*/
function populateTimeDropdowns(startSelect, endSelect) {
const times = [];
for (let hour = 0; hour < 24; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
times.push(timeStr);
}
}
times.forEach(time => {
const option1 = new Option(time, time);
const option2 = new Option(time, time);
startSelect.add(option1);
endSelect.add(option2);
});
}
/**
* Toggle time picker controls
*/
function togglePrinterTimeControls(enabled) {
document.getElementById('printerStartTime').disabled = !enabled;
document.getElementById('printerEndTime').disabled = !enabled;
}
/**
* Set default date/time values (last 24 hours)
*/
function setDefaultPrinterDateTimeValues() {
const now = new Date();
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
document.getElementById('printerStartDate').value = formatDate(yesterday);
document.getElementById('printerEndDate').value = formatDate(now);
document.getElementById('printerStartTime').value = '00:00';
document.getElementById('printerEndTime').value = '23:59';
const fullDayCheckbox = document.getElementById('printerFullDayCheckbox');
fullDayCheckbox.checked = true;
printerChartControls.isFullDay = true;
togglePrinterTimeControls(false);
document.getElementById('printerDateRange').textContent = '(Last 24 Hours)';
}
/**
* Handle date input changes
*/
function handlePrinterDateChange() {
const startDate = document.getElementById('printerStartDate').value;
const endDate = document.getElementById('printerEndDate').value;
// Ensure end date is not before start date
if (startDate && endDate && startDate > endDate) {
document.getElementById('printerEndDate').value = startDate;
}
printerChartControls.isCustomRange = true;
updatePrinterDateRangeLabel();
}
/**
* Update the date range label
*/
function updatePrinterDateRangeLabel() {
const startDate = document.getElementById('printerStartDate').value;
const endDate = document.getElementById('printerEndDate').value;
let label = '';
if (startDate === endDate) {
label = '(' + startDate + ')';
} else {
label = '(' + startDate + ' to ' + endDate + ')';
}
document.getElementById('printerDateRange').textContent = label;
}
/**
* Refresh printer charts data from API
*/
async function refreshPrinterChartsData() {
const startDate = document.getElementById('printerStartDate').value;
const endDate = document.getElementById('printerEndDate').value;
const isFullDay = printerChartControls.isFullDay;
const startTime = isFullDay ? '00:00' : document.getElementById('printerStartTime').value;
const endTime = isFullDay ? '23:59' : document.getElementById('printerEndTime').value;
// Show loading state (you can add a spinner here if needed)
console.log('Refreshing printer charts...');
try {
const params = new URLSearchParams({
start_date: startDate,
end_date: endDate,
start_time: startTime,
end_time: endTime
});
const response = await fetch(printerChartControls.apiUrl + '?' + params.toString());
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
// Update all charts with new data and project markers
updateAllPrinterCharts(data);
updatePrinterDateRangeLabel();
} catch (error) {
console.error('Error refreshing printer charts:', error);
alert('Error loading chart data: ' + error.message);
}
}
/**
* Update all printer charts with new data
*/
function updateAllPrinterCharts(data) {
// Update chart data
updateChartData(nozzleTempChart, data.timestamps, [
{ data: data.nozzle_temp, datasetIndex: 0 },
{ data: data.nozzle_target_temp, datasetIndex: 1 }
]);
updateChartData(bedTempChart, data.timestamps, [
{ data: data.bed_temp, datasetIndex: 0 },
{ data: data.bed_target_temp, datasetIndex: 1 }
]);
updateChartData(printProgressChart, data.timestamps, [
{ data: data.print_percent, datasetIndex: 0 }
]);
updateChartData(fanSpeedsChart, data.timestamps, [
{ data: data.cooling_fan_speed, datasetIndex: 0 },
{ data: data.heatbreak_fan_speed, datasetIndex: 1 }
]);
updateChartData(wifiSignalChart, data.timestamps, [
{ data: data.wifi_signal_dbm, datasetIndex: 0 }
]);
updateChartData(amsConditionsChart, data.timestamps, [
{ data: data.ams_humidity_raw, datasetIndex: 0 },
{ data: data.ams_temp, datasetIndex: 1 }
]);
updateChartData(layerProgressChart, data.timestamps, [
{ data: data.layer_num, datasetIndex: 0 },
{ data: data.total_layer_num, datasetIndex: 1 }
]);
// Update filament timeline chart
if (data.filament_timeline) {
const filamentDatasets = createFilamentDatasets(data.filament_timeline, data.timestamps);
filamentTimelineChart.data.labels = data.timestamps;
filamentTimelineChart.data.datasets = filamentDatasets;
filamentTimelineChart.update();
}
// Add project markers to all charts
if (data.project_markers) {
addProjectMarkersToCharts(data.project_markers, data.timestamps);
}
}
/**
* Helper to update chart data
*/
function updateChartData(chart, labels, datasets) {
if (!chart) return;
chart.data.labels = labels;
datasets.forEach(({ data, datasetIndex }) => {
if (chart.data.datasets[datasetIndex]) {
chart.data.datasets[datasetIndex].data = data;
}
});
chart.update();
}
/**
* Add project markers (start/end lines) to all charts
*/
function addProjectMarkersToCharts(markers, timestamps) {
console.log('Adding project markers:', markers);
const charts = [
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];
charts.forEach(chart => {
if (!chart) return;
// Initialize annotations plugin if not already
if (!chart.options.plugins.annotation) {
chart.options.plugins.annotation = { annotations: {} };
}
// Clear existing project markers
chart.options.plugins.annotation.annotations = {};
// Track active tooltip
let activeMarkerTooltip = null;
// Add markers
markers.forEach((marker, idx) => {
const isStart = marker.type === 'start';
const xValue = marker.index; // Use the index directly, not the timestamp string
const projectName = marker.project_name || 'Unknown';
const markerId = `marker_${idx}`;
chart.options.plugins.annotation.annotations[markerId] = {
type: 'line',
scaleID: 'x',
value: xValue,
borderColor: isStart ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.7)',
borderWidth: 2,
borderDash: [5, 5],
drawTime: 'beforeDatasetsDraw',
// Tighter hit detection - only trigger when very close to the line
borderDashOffset: 0,
display: true,
enter: (ctx, event) => {
// Verify we're actually hovering over THIS specific annotation line
// Check if mouse X position is close to the line's X position
if (event && event.native) {
const chartArea = chart.chartArea;
const xScale = chart.scales.x;
const lineXPixel = xScale.getPixelForValue(xValue);
const mouseX = event.native.offsetX;
// Only show tooltip if mouse is within 10 pixels of the line
const distance = Math.abs(mouseX - lineXPixel);
if (distance > 10) {
return; // Too far from this line, don't show tooltip
}
}
// Only show tooltip if not already showing from another marker
if (activeMarkerTooltip && activeMarkerTooltip !== markerId) {
return;
}
activeMarkerTooltip = markerId;
const tooltipText = isStart
? `Print Start: ${projectName}`
: `Print End: ${projectName}`;
// Change line appearance on hover
ctx.element.options.borderWidth = 3;
ctx.element.options.borderColor = isStart ? 'rgba(34, 197, 94, 1)' : 'rgba(239, 68, 68, 1)';
chart.update('none');
// Create or update tooltip element
let tooltip = document.getElementById('annotation-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.id = 'annotation-tooltip';
tooltip.style.position = 'fixed';
tooltip.style.backgroundColor = 'rgba(0, 0, 0, 0.85)';
tooltip.style.color = 'white';
tooltip.style.padding = '6px 10px';
tooltip.style.borderRadius = '4px';
tooltip.style.fontSize = '13px';
tooltip.style.pointerEvents = 'none';
tooltip.style.zIndex = '9999';
tooltip.style.display = 'none';
tooltip.style.whiteSpace = 'nowrap';
document.body.appendChild(tooltip);
}
tooltip.textContent = tooltipText;
tooltip.style.display = 'block';
tooltip.dataset.markerId = markerId;
// Position at mouse location
if (event && event.native) {
tooltip.style.left = (event.native.clientX + 12) + 'px';
tooltip.style.top = (event.native.clientY - 10) + 'px';
}
},
leave: (ctx) => {
// Only hide if this is the active marker
if (activeMarkerTooltip === markerId) {
activeMarkerTooltip = null;
// Restore line appearance
ctx.element.options.borderWidth = 2;
ctx.element.options.borderColor = isStart ? 'rgba(34, 197, 94, 0.7)' : 'rgba(239, 68, 68, 0.7)';
chart.update('none');
const tooltip = document.getElementById('annotation-tooltip');
if (tooltip && tooltip.dataset.markerId === markerId) {
tooltip.style.display = 'none';
tooltip.dataset.markerId = '';
}
}
}
};
});
chart.update();
});
}
/**
* Reset printer controls to default
*/
function resetPrinterControls() {
setDefaultPrinterDateTimeValues();
// Clear annotations and reload with original data
const charts = [
nozzleTempChart, bedTempChart, printProgressChart, fanSpeedsChart,
wifiSignalChart, amsConditionsChart, layerProgressChart, filamentTimelineChart
];
charts.forEach(chart => {
if (chart && chart.options.plugins.annotation) {
chart.options.plugins.annotation.annotations = {};
chart.update();
}
});
// Reload page to get default data
location.reload();
}
/**
* Format date as YYYY-MM-DD
*/
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}