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,88 @@
<!DOCTYPE html>
<html lang="en" data-coreui-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Bambu Run{% endblock %}</title>
<!-- CoreUI 5.3 CSS CDN -->
<link href="https://cdn.jsdelivr.net/npm/@coreui/coreui@5.3.0/dist/css/coreui.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/css/all.min.css" rel="stylesheet">
{% block extra_css %}{% endblock %}
<style>
.sidebar-brand { padding: 1rem; font-size: 1.25rem; font-weight: 700; }
</style>
</head>
<body>
<div class="sidebar sidebar-dark sidebar-fixed" id="sidebar">
<div class="sidebar-brand d-none d-md-flex">
Bambu Run
</div>
<ul class="sidebar-nav" data-coreui="navigation">
<li class="nav-item">
<a class="nav-link" href="{% url 'bambu_run:printer_dashboard' %}">
<svg class="nav-icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-print"></use></svg>
3D Printer
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'bambu_run:filament_list' %}">
<svg class="nav-icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-layers"></use></svg>
Filament Inventory
</a>
</li>
</ul>
</div>
<div class="wrapper d-flex flex-column min-vh-100">
<header class="header header-sticky p-0 mb-4">
<div class="container-fluid px-4">
<button class="header-toggler" type="button" onclick="document.getElementById('sidebar').classList.toggle('show')">
<svg class="icon icon-lg"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-menu"></use></svg>
</button>
<ul class="header-nav ms-auto">
<li class="nav-item">
<button class="nav-link" id="themeToggle" type="button">
<svg class="icon icon-lg"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-moon"></use></svg>
</button>
</li>
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{% url 'logout' %}">Logout</a>
</li>
{% endif %}
</ul>
</div>
</header>
<div class="body flex-grow-1">
<div class="container-lg px-4">
{% block content %}{% endblock %}
</div>
</div>
<footer class="footer px-4">
<div>Bambu Run</div>
<div class="ms-auto">Powered by <a href="https://github.com/runnanli/Bambu-Run">Bambu Run</a></div>
</footer>
</div>
<!-- CoreUI 5.3 JS CDN -->
<script src="https://cdn.jsdelivr.net/npm/@coreui/coreui@5.3.0/dist/js/coreui.bundle.min.js"></script>
<script>
// Theme toggle
const themeToggle = document.getElementById('themeToggle');
const savedTheme = localStorage.getItem('bambu-run-theme') || 'dark';
document.documentElement.setAttribute('data-coreui-theme', savedTheme);
if (themeToggle) {
themeToggle.addEventListener('click', function() {
const current = document.documentElement.getAttribute('data-coreui-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-coreui-theme', next);
localStorage.setItem('bambu-run-theme', next);
});
}
</script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,46 @@
{% extends bambu_run_base_template %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>Delete Filament Color</h1>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="alert alert-warning">
<h5><i class="bi bi-exclamation-triangle"></i> Warning</h5>
<p>Are you sure you want to delete this filament color?</p>
</div>
<div class="mb-4">
<h5>Color Details:</h5>
<div class="row">
<div class="col-md-2">
<div style="width: 100px; height: 100px; background-color: {{ object.get_hex_color }}; border-radius: 8px; border: 2px solid #ddd;"></div>
</div>
<div class="col-md-10">
<p><strong>Color Name:</strong> {{ object.color_name }}</p>
<p><strong>Hex Code:</strong> <span class="font-monospace">{{ object.get_hex_color }}</span></p>
<p><strong>Type:</strong> {{ object.filament_type }}</p>
<p><strong>Sub Type:</strong> {{ object.filament_sub_type|default:"-" }}</p>
<p><strong>Brand:</strong> {{ object.brand }}</p>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'bambu_run:filament_color_list' %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Yes, Delete Color
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,80 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>{% if form.instance.pk %}Edit{% else %}Add{% endif %} Filament Color</h1>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<h5>Color Information</h5>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Color Name *</label>
{{ form.color_name }}
<small class="form-text text-muted">e.g., Black, Orange, Signal White</small>
{% if form.color_name.errors %}
<div class="text-danger">{{ form.color_name.errors }}</div>
{% endif %}
</div>
<div class="col-md-6">
<label class="form-label">Color Hex Code *</label>
{{ form.color_hex_input }}
<small class="form-text text-muted">Format: #RRGGBB (without FF padding)</small>
{% if form.color_hex_input.errors %}
<div class="text-danger">{{ form.color_hex_input.errors }}</div>
{% endif %}
</div>
</div>
<hr>
<h5>Filament Type (for matching)</h5>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Filament Type</label>
{{ form.filament_type_fk }}
<small class="form-text text-muted">Select from the filament type registry</small>
{% if form.filament_type_fk.errors %}
<div class="text-danger">{{ form.filament_type_fk.errors }}</div>
{% endif %}
</div>
</div>
<!-- Hidden fields for backward compatibility -->
{{ form.color_code }}
{{ form.filament_type }}
{{ form.filament_sub_type }}
{{ form.brand }}
<hr>
<div class="d-flex justify-content-between">
<a href="{% url 'bambu_run:filament_color_list' %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
{% if form.instance.pk %}Update Color{% else %}Add Color{% endif %}
</button>
</div>
{% if form.errors %}
<div class="alert alert-danger mt-3">
<strong>Please correct the following errors:</strong>
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ field }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,109 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-md-8">
<h1>Filament Color Database</h1>
<p class="text-muted">Manage filament colors for auto-matching</p>
</div>
<div class="col-md-4 text-end">
<a href="{% url 'bambu_run:filament_color_create' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Color
</a>
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Inventory
</a>
</div>
</div>
<!-- Summary Card -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Summary</h5>
<p class="card-text">
<strong>Total Colors:</strong> {{ total_colors }}
</p>
</div>
</div>
</div>
</div>
<!-- Color List -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th class="align-middle">Color Preview</th>
<th class="align-middle">Color Name</th>
<th class="align-middle">Hex Code</th>
<th class="align-middle">Type</th>
<th class="align-middle">Sub Type</th>
<th class="align-middle">Brand</th>
<th class="align-middle">Actions</th>
</tr>
</thead>
<tbody>
{% for color in colors %}
<tr>
<td class="align-middle">
<div style="width: 50px; height: 50px; background-color: {{ color.get_hex_color }}; border-radius: 4px; border: 2px solid #ddd;"></div>
</td>
<td class="align-middle"><strong>{{ color.color_name }}</strong></td>
<td class="align-middle">
<span class="font-monospace">{{ color.get_hex_color }}</span>
</td>
<td class="align-middle">
<span class="badge bg-secondary">{{ color.filament_type }}</span>
</td>
<td class="align-middle">
{% if color.filament_sub_type %}
<span class="badge bg-info">{{ color.filament_sub_type }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="align-middle">{{ color.brand }}</td>
<td class="align-middle">
<a href="{% url 'bambu_run:filament_color_update' color.pk %}" class="btn btn-sm btn-warning">Edit</a>
<a href="{% url 'bambu_run:filament_color_delete' color.pk %}" class="btn btn-sm btn-danger">Delete</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center text-muted">
No colors found. <a href="{% url 'bambu_run:filament_color_create' %}">Add your first color!</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if is_paginated %}
<nav>
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page=1">First</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a></li>
{% endif %}
<li class="page-item active"><span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li>
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,311 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'bambu_run/css/dashboard.css' %}">
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>{{ filament }}</h1>
<p class="text-body-secondary">Filament Spool Details</p>
</div>
<div class="col-auto">
<a href="{% url 'bambu_run:filament_update' filament.pk %}" class="btn btn-warning">Edit</a>
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-secondary">Back to List</a>
</div>
</div>
<!-- Filament Info Cards -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h6>Color</h6>
<div class="d-flex align-items-center">
<div style="width: 50px; height: 50px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 8px; margin-right: 15px; border: 2px solid #ddd;"></div>
<div>
<strong>{{ filament.color }}</strong><br>
<small class="text-muted">{{ filament.color_hex }}</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h6>Specifications</h6>
<p class="mb-1"><strong>Type:</strong> {{ filament.type }}</p>
{% if filament.sub_type %}
<p class="mb-1"><strong>Sub Type:</strong> {{ filament.sub_type }}</p>
{% endif %}
<p class="mb-1"><strong>Brand:</strong> {{ filament.brand }}</p>
<p class="mb-0"><strong>Diameter:</strong> {{ filament.diameter }}mm</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h6>Remaining</h6>
<div class="progress mb-2" style="height: 25px;">
<div class="progress-bar {% if filament.remaining_percent < 20 %}bg-danger{% elif filament.remaining_percent < 50 %}bg-warning{% else %}bg-success{% endif %}"
style="width: {{ filament.remaining_percent }}%;">
{{ filament.remaining_percent }}%
</div>
</div>
<small>{{ filament.remaining_weight_grams|default:"?" }}g of {{ filament.initial_weight_grams|default:"?" }}g</small>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h6>Location</h6>
{% if filament.is_loaded_in_ams %}
<span class="badge bg-success fs-6">AMS Tray {{ filament.current_tray_id }}</span>
<p class="mb-0 mt-2"><small>Loaded: {{ filament.last_loaded_date|date:"Y-m-d H:i" }}</small></p>
{% else %}
<span class="badge bg-secondary fs-6">Storage</span>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Usage Chart -->
<div class="card mb-4">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<strong>Chart Filters</strong>
<span class="text-muted" id="filamentDateRange">(Last 24 Hours)</span>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<!-- Date Range -->
<div class="d-flex align-items-center gap-1">
<label class="form-label mb-0 small text-body-secondary">From:</label>
<input type="date" class="form-control form-control-sm" id="filamentStartDate" style="width: auto;">
</div>
<div class="d-flex align-items-center gap-1">
<label class="form-label mb-0 small text-body-secondary">To:</label>
<input type="date" class="form-control form-control-sm" id="filamentEndDate" style="width: auto;">
</div>
<!-- Full Day Checkbox -->
<div class="form-check">
<input class="form-check-input" type="checkbox" id="filamentFullDayCheckbox" checked>
<label class="form-check-label small" for="filamentFullDayCheckbox">Full Day</label>
</div>
<!-- Time Range -->
<div class="d-flex align-items-center gap-1" id="filamentTimeRangeControls">
<label class="form-label mb-0 small text-body-secondary">Time:</label>
<select class="form-select form-select-sm" id="filamentStartTime" style="width: auto;" disabled></select>
<span class="text-body-secondary">-</span>
<select class="form-select form-select-sm" id="filamentEndTime" style="width: auto;" disabled></select>
</div>
<!-- Buttons -->
<button type="button" class="btn btn-primary btn-sm" id="refreshFilamentChart">
<svg class="icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-reload"></use></svg>
Refresh
</button>
<button type="button" class="btn btn-secondary btn-sm" id="resetFilamentChart">
<svg class="icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-action-undo"></use></svg>
Reset
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="chart-container" style="height: 300px;">
<canvas id="usageChart"></canvas>
</div>
</div>
</div>
<!-- Print Jobs -->
<div class="card mb-4">
<div class="card-header">
<h5>Print Jobs Using This Filament</h5>
</div>
<div class="card-body">
{% if print_usages %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Project</th>
<th>Date</th>
<th>Tray</th>
<th>Consumed</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for usage in print_usages %}
<tr>
<td>{{ usage.print_job.project_name }}</td>
<td>{{ usage.print_job.start_time|date:"Y-m-d H:i" }}</td>
<td>Tray {{ usage.tray_id }}</td>
<td>{{ usage.consumed_percent|default:"?" }}% ({{ usage.consumed_grams|default:"?" }}g)</td>
<td><span class="badge bg-{% if usage.print_job.final_status == 'FINISH' %}success{% else %}danger{% endif %}">{{ usage.print_job.final_status }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No print jobs recorded yet</p>
{% endif %}
</div>
</div>
<!-- Purchase Info -->
{% if filament.purchase_date or filament.purchase_price or filament.supplier %}
<div class="card mb-4">
<div class="card-header">
<h5>Purchase Information</h5>
</div>
<div class="card-body">
<div class="row">
{% if filament.purchase_date %}
<div class="col-md-4">
<strong>Purchase Date:</strong> {{ filament.purchase_date|date:"Y-m-d" }}
</div>
{% endif %}
{% if filament.purchase_price %}
<div class="col-md-4">
<strong>Price:</strong> ${{ filament.purchase_price }}
</div>
{% endif %}
{% if filament.supplier %}
<div class="col-md-4">
<strong>Supplier:</strong> {{ filament.supplier }}
</div>
{% endif %}
</div>
{% if filament.notes %}
<hr>
<strong>Notes:</strong>
<p>{{ filament.notes }}</p>
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
<script>
const filamentId = {{ filament.pk }};
let usageChart = null;
// Populate time selects
const startTimeSelect = document.getElementById('filamentStartTime');
const endTimeSelect = document.getElementById('filamentEndTime');
for (let h = 0; h < 24; h++) {
for (let m = 0; m < 60; m += 30) {
const timeStr = `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
startTimeSelect.add(new Option(timeStr, timeStr));
endTimeSelect.add(new Option(timeStr, timeStr));
}
}
startTimeSelect.value = '00:00';
endTimeSelect.value = '23:30';
// Initialize date inputs to last 24 hours
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
document.getElementById('filamentStartDate').value = yesterday.toISOString().split('T')[0];
document.getElementById('filamentEndDate').value = now.toISOString().split('T')[0];
// Full day checkbox handler
document.getElementById('filamentFullDayCheckbox').addEventListener('change', function() {
const isFullDay = this.checked;
startTimeSelect.disabled = isFullDay;
endTimeSelect.disabled = isFullDay;
});
// Fetch and render chart
async function fetchFilamentUsageData() {
const startDate = document.getElementById('filamentStartDate').value;
const endDate = document.getElementById('filamentEndDate').value;
const isFullDay = document.getElementById('filamentFullDayCheckbox').checked;
const startTime = isFullDay ? '00:00' : startTimeSelect.value;
const endTime = isFullDay ? '23:59' : endTimeSelect.value;
const params = new URLSearchParams();
if (startDate) params.append('start_date', startDate);
if (endDate) params.append('end_date', endDate);
if (startTime) params.append('start_time', startTime);
if (endTime) params.append('end_time', endTime);
try {
const response = await fetch(`{% url 'bambu_run:filament_usage_api' filament.pk %}?${params.toString()}`);
const data = await response.json();
// Update date range display
const dateRangeSpan = document.getElementById('filamentDateRange');
if (startDate && endDate) {
dateRangeSpan.textContent = `(${startDate} to ${endDate})`;
} else {
dateRangeSpan.textContent = '(Last 24 Hours)';
}
// Update chart
if (usageChart) {
usageChart.data.labels = data.timestamps;
usageChart.data.datasets[0].data = data.remaining;
usageChart.update();
} else {
const ctx = document.getElementById('usageChart').getContext('2d');
usageChart = new Chart(ctx, {
type: 'line',
data: {
labels: data.timestamps,
datasets: [{
label: 'Remaining %',
data: data.remaining,
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.3,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
} catch (error) {
console.error('Error fetching filament usage data:', error);
}
}
// Event listeners
document.getElementById('refreshFilamentChart').addEventListener('click', fetchFilamentUsageData);
document.getElementById('resetFilamentChart').addEventListener('click', function() {
const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
document.getElementById('filamentStartDate').value = yesterday.toISOString().split('T')[0];
document.getElementById('filamentEndDate').value = now.toISOString().split('T')[0];
document.getElementById('filamentFullDayCheckbox').checked = true;
startTimeSelect.disabled = true;
endTimeSelect.disabled = true;
fetchFilamentUsageData();
});
// Initial load
fetchFilamentUsageData();
</script>
{% endblock %}

View File

@@ -0,0 +1,303 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>{% if form.instance.pk %}Edit{% else %}Add{% endif %} Filament Spool</h1>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<h5>Identification</h5>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Spool Serial Number (SN)</label>
{{ form.tray_uuid }}
<small class="form-text text-muted">Auto-filled from MQTT tray_uuid</small>
</div>
<div class="col-md-6">
<label class="form-label">RFID Chip ID (tag_uid)</label>
{{ form.tag_uid }}
<small class="form-text text-muted">Auto-filled from MQTT RFID</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Custom Tag ID (Optional)</label>
{{ form.tag_id }}
<small class="form-text text-muted">User-defined barcode/label</small>
</div>
<div class="col-md-6">
<label class="form-label">Created By</label>
{{ form.created_by }}
<small class="form-text text-muted">How this filament was added</small>
</div>
</div>
<hr>
<h5>Specifications</h5>
<div class="row mb-3">
<div class="col-md-3">
<label class="form-label">Type *</label>
{{ form.type }}
</div>
<div class="col-md-3">
<label class="form-label">Sub Type</label>
{{ form.sub_type }}
</div>
<div class="col-md-3">
<label class="form-label">Brand *</label>
{{ form.brand }}
</div>
<div class="col-md-3">
<label class="form-label">Color *</label>
{{ form.color }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<label class="form-label">Color Picker</label>
{{ form.color_hex }}
</div>
<div class="col-md-3">
<label class="form-label">{{ form.color_hex_text.label }}</label>
{{ form.color_hex_text }}
<small class="form-text text-muted">e.g. #0A2CA5</small>
</div>
<div class="col-md-3">
<label class="form-label">Diameter (mm)</label>
{{ form.diameter }}
</div>
<div class="col-md-3">
<label class="form-label">Initial Weight (g)</label>
{{ form.initial_weight_grams }}
</div>
</div>
<hr>
<h5>Current Status</h5>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Remaining %</label>
{{ form.remaining_percent }}
</div>
<div class="col-md-6">
<label class="form-label">Remaining Weight (g)</label>
{{ form.remaining_weight_grams }}
<small class="form-text text-muted">Auto-calculated</small>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-check">
{{ form.is_loaded_in_ams }}
<label class="form-check-label">Loaded in AMS</label>
</div>
</div>
<div class="col-md-6">
<label class="form-label">AMS Tray ID (0-3)</label>
{{ form.current_tray_id }}
</div>
</div>
<hr>
<h5>Purchase Info (Optional)</h5>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Purchase Date</label>
{{ form.purchase_date }}
</div>
<div class="col-md-4">
<label class="form-label">Price</label>
{{ form.purchase_price }}
</div>
<div class="col-md-4">
<label class="form-label">Supplier</label>
{{ form.supplier }}
</div>
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
{{ form.notes }}
</div>
{% if form.errors %}
<div class="alert alert-danger">
<strong>Please correct the following errors:</strong>
{{ form.errors }}
</div>
{% endif %}
<div class="d-flex justify-content-between">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-secondary">Cancel</a>
</div>
{% if form.instance.pk %}
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal" id="deleteBtn">
<i class="bi bi-trash-fill me-1"></i>Delete
</button>
{% endif %}
</div>
</form>
</div>
</div>
</div>
{% if form.instance.pk %}
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteModalLabel">
<i class="bi bi-exclamation-triangle-fill me-2"></i>Delete Filament Spool
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'bambu_run:filament_delete' form.instance.pk %}" id="deleteForm">
{% csrf_token %}
<div class="modal-body">
<div class="alert alert-danger mb-3" role="alert">
<strong>Warning: This action cannot be undone!</strong>
</div>
<p>You are about to permanently delete:</p>
<div class="card bg-light mb-3">
<div class="card-body">
<strong>{{ form.instance }}</strong>
</div>
</div>
<p>This will remove:</p>
<ul>
<li>This filament spool record</li>
<li>All associated usage history</li>
<li>All filament snapshots</li>
</ul>
<hr>
<div class="mb-3">
<label for="deleteConfirmText" class="form-label">
To confirm deletion, type <strong class="text-danger">DELETE</strong> in the box below:
</label>
<input type="text" id="deleteConfirmText" class="form-control form-control-lg" placeholder="Type DELETE to confirm" autocomplete="off">
<div class="form-text">Must be in capital letters</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" id="confirmDeleteBtn" class="btn btn-danger" disabled>
<i class="bi bi-trash-fill me-1"></i>Confirm Delete
</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
// Sync color picker and text input
const colorPicker = document.getElementById('id_color_hex_picker');
const colorText = document.getElementById('id_color_hex_text');
if (colorPicker && colorText) {
colorPicker.addEventListener('input', function() {
colorText.value = this.value.toUpperCase();
});
colorText.addEventListener('input', function() {
const value = this.value.trim();
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
colorPicker.value = value;
this.classList.remove('is-invalid');
} else if (value.length === 7) {
this.classList.add('is-invalid');
}
});
if (colorText.value && /^#[0-9A-Fa-f]{6}$/.test(colorText.value)) {
colorPicker.value = colorText.value;
} else if (colorPicker.value && !colorText.value) {
colorText.value = colorPicker.value.toUpperCase();
}
}
// Delete confirmation logic
const deleteConfirmText = document.getElementById('deleteConfirmText');
const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
const deleteForm = document.getElementById('deleteForm');
const deleteModal = document.getElementById('deleteModal');
if (deleteConfirmText && confirmDeleteBtn) {
deleteConfirmText.addEventListener('input', function() {
const value = this.value.trim();
if (value === 'DELETE') {
confirmDeleteBtn.disabled = false;
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
confirmDeleteBtn.disabled = true;
this.classList.remove('is-valid');
if (value.length > 0) {
this.classList.add('is-invalid');
} else {
this.classList.remove('is-invalid');
}
}
});
if (deleteForm) {
deleteForm.addEventListener('submit', function(e) {
if (confirmDeleteBtn.disabled) {
e.preventDefault();
alert('Please type DELETE to confirm deletion');
return false;
}
return true;
});
}
if (deleteModal) {
deleteModal.addEventListener('hidden.bs.modal', function() {
deleteConfirmText.value = '';
confirmDeleteBtn.disabled = true;
deleteConfirmText.classList.remove('is-valid', 'is-invalid');
});
deleteModal.addEventListener('shown.bs.modal', function() {
deleteConfirmText.focus();
});
}
}
// Backup modal opener
const deleteBtn = document.getElementById('deleteBtn');
if (deleteBtn && deleteModal) {
deleteBtn.addEventListener('click', function() {
if (!deleteModal.classList.contains('show')) {
if (typeof bootstrap !== 'undefined') {
const modalInstance = bootstrap.Modal.getOrCreateInstance(deleteModal);
modalInstance.show();
} else if (typeof coreui !== 'undefined' && coreui.Modal) {
const modalInstance = coreui.Modal.getOrCreateInstance(deleteModal);
modalInstance.show();
}
}
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,206 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'bambu_run/css/dashboard.css' %}">
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>Filament Inventory</h1>
<p class="text-body-secondary">Manage your 3D printer filament spools</p>
</div>
<div class="col-auto">
<a href="{% url 'bambu_run:filament_type_list' %}" class="btn btn-outline-info me-2">
<i class="bi bi-list-ul"></i> Manage Types
</a>
<a href="{% url 'bambu_run:filament_color_list' %}" class="btn btn-outline-info me-2">
<i class="bi bi-palette"></i> Manage Colors
</a>
<a href="{% url 'bambu_run:filament_create' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Filament
</a>
</div>
</div>
<!-- Summary Cards -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card infra-card-info">
<div class="card-body">
<div class="stat-label">Total Spools</div>
<div class="stat-value">{{ total_spools }}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card infra-card-success">
<div class="card-body">
<div class="stat-label">Loaded in AMS</div>
<div class="stat-value">{{ loaded_spools }}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card infra-card-warning">
<div class="card-body">
<div class="stat-label">Low Filament (&lt;20%)</div>
<div class="stat-value">{{ low_filaments }}</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<input type="text" name="search" class="form-control" placeholder="Search..." value="{{ request.GET.search }}">
</div>
<div class="col-md-3">
<select name="type" class="form-select">
<option value="">All Types</option>
{% for type in filament_types %}
<option value="{{ type }}" {% if request.GET.type == type %}selected{% endif %}>{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<select name="loaded" class="form-select">
<option value="">All Spools</option>
<option value="yes" {% if request.GET.loaded == 'yes' %}selected{% endif %}>Loaded in AMS</option>
<option value="no" {% if request.GET.loaded == 'no' %}selected{% endif %}>Not Loaded</option>
</select>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-secondary">Filter</button>
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-outline-secondary">Reset</a>
</div>
</form>
</div>
</div>
<!-- Filament List -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th class="align-middle">SN</th>
<th class="align-middle">Color</th>
<th class="align-middle">Brand</th>
<th class="align-middle">Type</th>
<th class="align-middle">Sub Type</th>
<th class="align-middle">Remaining</th>
<th class="align-middle">Location</th>
<th class="align-middle">Created By</th>
<th class="align-middle">Last Used</th>
<th class="align-middle">Actions</th>
</tr>
</thead>
<tbody>
{% for filament in filaments %}
<tr>
<td class="align-middle">
{% if filament.tray_uuid %}
<span class="font-monospace small"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{{ filament.tray_uuid }}"
style="cursor: help;">
{{ filament.tray_uuid|slice:":8" }}...
</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="align-middle">
<div class="d-flex align-items-center">
<div style="width: 30px; height: 30px; background-color: {{ filament.color_hex|default:'#999' }}; border-radius: 4px; margin-right: 10px; border: 1px solid #ddd;"></div>
{{ filament.color }}
</div>
</td>
<td class="align-middle">{{ filament.brand }}</td>
<td class="align-middle"><span class="badge bg-secondary">{{ filament.type }}</span></td>
<td class="align-middle">
{% if filament.sub_type %}
<span class="badge bg-info">{{ filament.sub_type }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="align-middle">
<div class="progress" style="height: 20px;">
<div class="progress-bar {% if filament.remaining_percent < 20 %}bg-danger{% elif filament.remaining_percent < 50 %}bg-warning{% else %}bg-success{% endif %}"
style="width: {{ filament.remaining_percent }}%;">
{{ filament.remaining_percent }}%
</div>
</div>
</td>
<td class="align-middle">
{% if filament.is_loaded_in_ams %}
<span class="badge bg-success">AMS Tray {{ filament.current_tray_id }}</span>
{% else %}
<span class="badge bg-secondary">Storage</span>
{% endif %}
</td>
<td class="align-middle">
{% if filament.created_by == 'Auto Detection' %}
<span class="badge bg-primary">Auto</span>
{% else %}
<span class="badge bg-secondary">Manual</span>
{% endif %}
</td>
<td class="align-middle">{{ filament.last_used|date:"Y-m-d H:i"|default:"Never" }}</td>
<td class="align-middle">
<a href="{% url 'bambu_run:filament_detail' filament.pk %}" class="btn btn-sm btn-info">View</a>
<a href="{% url 'bambu_run:filament_update' filament.pk %}" class="btn btn-sm btn-warning">Edit</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="10" class="text-center text-muted">No filaments found. <a href="{% url 'bambu_run:filament_create' %}">Add your first spool!</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if is_paginated %}
<nav>
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page=1">First</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a></li>
{% endif %}
<li class="page-item active"><span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li>
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Enable Bootstrap tooltips for SN hover
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
});
</script>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends bambu_run_base_template %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>Delete Filament Type</h1>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="alert alert-warning">
<h5><i class="bi bi-exclamation-triangle"></i> Warning</h5>
<p>Are you sure you want to delete this filament type?</p>
</div>
<div class="mb-4">
<h5>Type Details:</h5>
<p><strong>Type:</strong> {{ object.type }}</p>
<p><strong>Sub Type:</strong> {{ object.sub_type|default:"-" }}</p>
<p><strong>Brand:</strong> {{ object.brand }}</p>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'bambu_run:filament_type_list' %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-danger">
<i class="bi bi-trash"></i> Yes, Delete Type
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,92 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>{% if form.instance.pk %}Edit{% else %}Add{% endif %} Filament Type</h1>
</div>
</div>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label">Type *</label>
<div class="input-group">
{{ form.type }}
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-coreui-toggle="dropdown" aria-expanded="false"></button>
<ul class="dropdown-menu dropdown-menu-end" id="type-dropdown"></ul>
</div>
<small class="form-text text-muted">Base material: PLA, PETG, ABS, etc.</small>
{% if form.type.errors %}
<div class="text-danger">{{ form.type.errors }}</div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">Sub Type</label>
<div class="input-group">
{{ form.sub_type }}
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-coreui-toggle="dropdown" aria-expanded="false"></button>
<ul class="dropdown-menu dropdown-menu-end" id="sub-type-dropdown"></ul>
</div>
<small class="form-text text-muted">Optional: PLA Basic, PLA Matte, etc.</small>
{% if form.sub_type.errors %}
<div class="text-danger">{{ form.sub_type.errors }}</div>
{% endif %}
</div>
<div class="col-md-4">
<label class="form-label">Brand *</label>
<div class="input-group">
{{ form.brand }}
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-coreui-toggle="dropdown" aria-expanded="false"></button>
<ul class="dropdown-menu dropdown-menu-end" id="brand-dropdown"></ul>
</div>
{% if form.brand.errors %}
<div class="text-danger">{{ form.brand.errors }}</div>
{% endif %}
</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<a href="{% url 'bambu_run:filament_type_list' %}" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">
{% if form.instance.pk %}Update Type{% else %}Add Type{% endif %}
</button>
</div>
{% if form.errors %}
<div class="alert alert-danger mt-3">
<strong>Please correct the following errors:</strong>
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ field }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
{{ existing_types|json_script:"existing-types" }}
{{ existing_sub_types|json_script:"existing-sub-types" }}
{{ existing_brands|json_script:"existing-brands" }}
{{ preset_types|json_script:"preset-types" }}
{{ preset_sub_types|json_script:"preset-sub-types" }}
{{ preset_brands|json_script:"preset-brands" }}
<script src="{% static 'bambu_run/js/filament_type_form.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,99 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-md-8">
<h1>Filament Type Registry</h1>
<p class="text-muted">Manage filament types (material, sub-type, brand)</p>
</div>
<div class="col-md-4 text-end">
<a href="{% url 'bambu_run:filament_type_create' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add New Type
</a>
<a href="{% url 'bambu_run:filament_list' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Inventory
</a>
</div>
</div>
<!-- Summary Card -->
<div class="row mb-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Summary</h5>
<p class="card-text">
<strong>Total Types:</strong> {{ total_types }}
</p>
</div>
</div>
</div>
</div>
<!-- Type List -->
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th class="align-middle">Type</th>
<th class="align-middle">Sub Type</th>
<th class="align-middle">Brand</th>
<th class="align-middle">Actions</th>
</tr>
</thead>
<tbody>
{% for ft in types %}
<tr>
<td class="align-middle">
<span class="badge bg-secondary">{{ ft.type }}</span>
</td>
<td class="align-middle">
{% if ft.sub_type %}
<span class="badge bg-info">{{ ft.sub_type }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="align-middle">{{ ft.brand }}</td>
<td class="align-middle">
<a href="{% url 'bambu_run:filament_type_update' ft.pk %}" class="btn btn-sm btn-warning">Edit</a>
<a href="{% url 'bambu_run:filament_type_delete' ft.pk %}" class="btn btn-sm btn-danger">Delete</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center text-muted">
No filament types found. <a href="{% url 'bambu_run:filament_type_create' %}">Add your first type!</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if is_paginated %}
<nav>
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page=1">First</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a></li>
{% endif %}
<li class="page-item active"><span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span></li>
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,390 @@
{% extends bambu_run_base_template %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'bambu_run/css/dashboard.css' %}">
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>3D Printer Dashboard</h1>
<p class="text-body-secondary">
Real-time monitoring for {{ device_name }}
</p>
</div>
</div>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% else %}
<!-- Summary Cards Row -->
<div class="row g-3 mb-4">
<!-- Nozzle Temperature Card -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card infra-card-warning">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Nozzle Temp</div>
<div class="stat-value">{{ stats.nozzle_temp|floatformat:1 }}&deg;C</div>
</div>
<i class="bi bi-thermometer-high" style="font-size: 2rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
<!-- Bed Temperature Card -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card infra-card-danger">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Bed Temp</div>
<div class="stat-value">{{ stats.bed_temp|floatformat:1 }}&deg;C</div>
</div>
<i class="bi bi-thermometer-half" style="font-size: 2rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
<!-- Print Progress Card -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card infra-card-info">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Print Progress</div>
<div class="stat-value">{{ stats.print_percent }}%</div>
</div>
<i class="bi bi-pie-chart-fill" style="font-size: 2rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
<!-- Chamber Light Card -->
<div class="col-12 col-md-6 col-lg-3">
<div class="card {% if stats.chamber_light == 'on' %}infra-card-success{% else %}infra-card-secondary{% endif %}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<div class="stat-label">Chamber Light</div>
<div class="stat-value">{{ stats.chamber_light|upper }}</div>
</div>
<i class="bi bi-lightbulb-fill" style="font-size: 2rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Current Print Job Info -->
{% if stats.subtask_name and stats.subtask_name != 'No active print' %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5>Current Print Job</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<strong>Job Name:</strong> {{ stats.subtask_name }}
</div>
<div class="col-md-3">
<strong>State:</strong> {{ stats.gcode_state }}
</div>
<div class="col-md-3">
<strong>Progress:</strong> {{ stats.print_percent }}%
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- AMS Status Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5>AMS Status</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<strong>Temperature:</strong>
{% if stats.ams_temp %}
{{ stats.ams_temp|floatformat:1 }}&deg;C
{% else %}
N/A
{% endif %}
</div>
<div class="col-md-4">
<strong>Humidity:</strong>
{% if stats.ams_humidity %}
{{ stats.ams_humidity }}%
{% else %}
N/A
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filaments Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5>Filaments</h5>
</div>
<div class="card-body">
{% if stats.filaments %}
<div class="row g-3">
{% for filament in stats.filaments %}
<div class="col-12 col-md-6 col-lg-3">
<div class="card filament-card" data-filament-color="{{ filament.color|slice:':6' }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Tray {{ filament.tray_id }}</h6>
{% if filament.filament_pk %}
<a href="{% url 'bambu_run:filament_detail' filament.filament_pk %}" class="text-decoration-none" title="View in inventory">
<svg class="icon icon-sm text-body-secondary"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-external-link"></use></svg>
</a>
{% endif %}
</div>
<p class="mb-1 small"><strong>{{ filament.type }}</strong> - {{ filament.brand }}</p>
{% if filament.color_name %}<p class="mb-1 small text-body-secondary">{{ filament.color_name }}</p>{% endif %}
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small">Remaining</span>
<span class="badge filament-badge">{{ filament.remain_percent }}%</span>
</div>
<div class="progress" style="height: 10px; background-color: rgba(0,0,0,0.1);">
<div class="progress-bar filament-progress" role="progressbar" style="width: {{ filament.remain_percent }}%;" aria-valuenow="{{ filament.remain_percent }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
{% endfor %}
{% if stats.external_spool.type %}
<div class="col-12 col-md-6 col-lg-3">
<div class="card filament-card" data-filament-color="{{ stats.external_spool.color|slice:':6' }}">
<div class="card-body">
<h6 class="mb-2">External Spool</h6>
<p class="mb-1 small"><strong>{{ stats.external_spool.type }}</strong> - External</p>
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="small">Remaining</span>
<span class="badge filament-badge">{{ stats.external_spool.remain }}%</span>
</div>
<div class="progress" style="height: 10px; background-color: rgba(0,0,0,0.1);">
<div class="progress-bar filament-progress" role="progressbar" style="width: {{ stats.external_spool.remain }}%;" aria-valuenow="{{ stats.external_spool.remain }}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% else %}
<p class="text-body-secondary">No filament data available</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Date/Time Filter Controls -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<strong>Chart Filters</strong>
<span class="text-muted" id="printerDateRange">(Last 24 Hours)</span>
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<!-- Date Range -->
<div class="d-flex align-items-center gap-1">
<label class="form-label mb-0 small text-body-secondary">From:</label>
<input type="date" class="form-control form-control-sm" id="printerStartDate" style="width: auto;">
</div>
<div class="d-flex align-items-center gap-1">
<label class="form-label mb-0 small text-body-secondary">To:</label>
<input type="date" class="form-control form-control-sm" id="printerEndDate" style="width: auto;">
</div>
<!-- Full Day Checkbox -->
<div class="form-check">
<input class="form-check-input" type="checkbox" id="printerFullDayCheckbox" checked>
<label class="form-check-label small" for="printerFullDayCheckbox">Full Day</label>
</div>
<!-- Time Range -->
<div class="d-flex align-items-center gap-1" id="printerTimeRangeControls">
<label class="form-label mb-0 small text-body-secondary">Time:</label>
<select class="form-select form-select-sm" id="printerStartTime" style="width: auto;" disabled></select>
<span class="text-body-secondary">-</span>
<select class="form-select form-select-sm" id="printerEndTime" style="width: auto;" disabled></select>
</div>
<!-- Buttons -->
<button type="button" class="btn btn-primary btn-sm" id="refreshPrinterCharts">
<svg class="icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-reload"></use></svg>
Refresh
</button>
<button type="button" class="btn btn-secondary btn-sm" id="resetPrinterCharts">
<svg class="icon"><use xlink:href="https://cdn.jsdelivr.net/npm/@coreui/icons@3.0.1/sprites/free.svg#cil-action-undo"></use></svg>
Reset
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Filament Timeline Chart - Full Width -->
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">Filament Remaining Timeline</div>
<div class="card-body">
<div class="chart-container">
<canvas id="filamentTimelineChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="row g-3 mb-4">
<!-- Nozzle Temperature Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">Nozzle Temperature</div>
<div class="card-body">
<div class="chart-container">
<canvas id="nozzleTempChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Bed Temperature Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">Bed Temperature</div>
<div class="card-body">
<div class="chart-container">
<canvas id="bedTempChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<!-- Print Progress Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">Print Progress</div>
<div class="card-body">
<div class="chart-container">
<canvas id="printProgressChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Fan Speeds Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">Fan Speeds</div>
<div class="card-body">
<div class="chart-container">
<canvas id="fanSpeedsChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<!-- WiFi Signal Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">WiFi Signal Strength</div>
<div class="card-body">
<div class="chart-container">
<canvas id="wifiSignalChart"></canvas>
</div>
</div>
</div>
</div>
<!-- AMS Conditions Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">AMS Conditions</div>
<div class="card-body">
<div class="chart-container">
<canvas id="amsConditionsChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<!-- Layer Progress Chart -->
<div class="col-12 col-lg-6">
<div class="card">
<div class="card-header">Layer Progress</div>
<div class="card-body">
<div class="chart-container">
<canvas id="layerProgressChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<p class="text-body-secondary text-end">
Last updated: {{ stats.timestamp }}
</p>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1"></script>
<script src="{% static 'bambu_run/js/printer_charts.js' %}"></script>
<script src="{% static 'bambu_run/js/printer_charts_control.js' %}"></script>
<div id="printerApiUrl" data-url="{% url 'bambu_run:printer_api' %}" style="display: none;"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const printerData = {{ printer_data_json|safe }};
const apiUrl = '{% url "bambu_run:printer_api" %}';
initPrinterCharts(printerData, apiUrl);
// Add project markers if they exist
if (printerData.project_markers && printerData.project_markers.length > 0) {
setTimeout(function() {
addProjectMarkersToCharts(printerData.project_markers, printerData.timestamps);
}, 500);
}
});
</script>
{% endblock %}