Files
karung-counting-feedmill-se…/templates/analytics.html
T
andrew 4bacf90e25
ci / smoke (push) Waiting to run
rebrand: Karung.AI / Karung Counter -> Feedmill Counting Platform
Brand strings only:
- UI page titles (base/monitoring/history/analytics), header logo, footer
- predict.py argparse description, systemd unit Description fields
- README + AGENTS headings, CHANGELOG entry
- header CSS shrinks below 640px so the longer name fits phone screens

Operational names intentionally unchanged (data/history continuity):
OBJECT_LABEL, DB paths, service unit filenames, repo/remote, Indonesian
UI/log strings (karung/truk).
2026-10-05 10:10:06 +07:00

380 lines
13 KiB
HTML

{% extends "base.html" %}
{% block title %}Performance Analytics & Stats - Feedmill Counting Platform{% endblock %}
{% block extra_head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
.analytics-layout {
display: flex;
flex-direction: column;
gap: 28px;
}
/* Metric Summary Grid */
.metric-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
}
.metric-card {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 140px;
transition: var(--transition);
}
.metric-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.metric-title {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.metric-title i {
font-size: 14px;
}
.metric-value {
font-size: 36px;
font-weight: 700;
letter-spacing: -1px;
color: var(--text-primary);
margin: 12px 0 4px 0;
}
.metric-subtext {
font-size: 12px;
color: var(--text-secondary);
}
.metric-subtext strong {
color: var(--text-primary);
}
/* Chart Section */
.chart-card {
display: flex;
flex-direction: column;
gap: 20px;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.chart-title-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.chart-title {
font-size: 18px;
font-weight: 600;
letter-spacing: -0.5px;
}
.chart-subtitle {
font-size: 13px;
color: var(--text-secondary);
}
.chart-filters {
display: flex;
background-color: var(--bg-base);
padding: 4px;
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
gap: 2px;
}
.filter-btn {
background: none;
border: none;
padding: 6px 12px;
font-size: 12px;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
color: var(--text-secondary);
transition: var(--transition);
}
.filter-btn:hover {
color: var(--text-primary);
}
.filter-btn.active {
background-color: var(--bg-card);
color: var(--accent-blue);
box-shadow: var(--shadow-sm);
}
.chart-container {
position: relative;
height: 380px;
width: 100%;
}
</style>
{% endblock %}
{% block content %}
<div class="analytics-layout">
<!-- Top Row: Metrics Overview -->
<div class="metric-grid">
<!-- Card 1: Today's Count -->
<div class="metric-card">
<div class="metric-title">
<i class="fa-solid fa-calendar-day" style="color: var(--accent-blue);"></i> Today's Sacks
</div>
<div class="metric-value" id="todayCount">0</div>
<div class="metric-subtext" id="todayBatches">0 batches recorded</div>
</div>
<!-- Card 2: Yesterday's Count -->
<div class="metric-card">
<div class="metric-title">
<i class="fa-solid fa-history" style="color: var(--text-secondary);"></i> Yesterday's Sacks
</div>
<div class="metric-value" id="yesterdayCount">0</div>
<div class="metric-subtext" id="yesterdayBatches">0 batches recorded</div>
</div>
<!-- Card 3: Daily Average -->
<div class="metric-card">
<div class="metric-title">
<i class="fa-solid fa-calculator" style="color: var(--accent-green);"></i> Daily Average
</div>
<div class="metric-value" id="avgCount">0</div>
<div class="metric-subtext" id="recordedDays">0 days recorded in total</div>
</div>
<!-- Card 4: Best Day Record -->
<div class="metric-card">
<div class="metric-title">
<i class="fa-solid fa-trophy" style="color: var(--accent-orange);"></i> All-Time Record
</div>
<div class="metric-value" id="bestCount">0</div>
<div class="metric-subtext" id="bestDate">Best day: --</div>
</div>
</div>
<!-- Bottom Row: Chart -->
<div class="card chart-card">
<div class="chart-header">
<div class="chart-title-group">
<h3 class="chart-title">Counting Trends</h3>
<span class="chart-subtitle">Daily count output and total batch processing volumes</span>
</div>
<div class="chart-filters">
<button class="filter-btn active" data-days="7" onclick="loadChartData(7)">7 Days</button>
<button class="filter-btn" data-days="30" onclick="loadChartData(30)">30 Days</button>
<button class="filter-btn" data-days="90" onclick="loadChartData(90)">90 Days</button>
</div>
</div>
<div class="chart-container">
<canvas id="mainChart"></canvas>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let mainChart = null;
let chartDays = 7;
async function loadMetrics() {
try {
const res = await fetch('/api/summary');
const data = await res.json();
document.getElementById('todayCount').textContent = (data.today.total_count || 0).toLocaleString();
document.getElementById('todayBatches').textContent = `${data.today.total_batches || 0} batches recorded`;
document.getElementById('yesterdayCount').textContent = (data.yesterday.total_count || 0).toLocaleString();
document.getElementById('yesterdayBatches').textContent = `${data.yesterday.total_batches || 0} batches recorded`;
document.getElementById('avgCount').textContent = Math.round(data.average_per_day || 0).toLocaleString();
document.getElementById('recordedDays').textContent = `${data.all_time.total_days || 0} active days recorded`;
document.getElementById('bestCount').textContent = (data.best_day.count || 0).toLocaleString();
document.getElementById('bestDate').textContent = data.best_day.date ? `Record date: ${data.best_day.date}` : 'Record date: --';
} catch (err) {
console.error('Failed to load metrics summary:', err);
}
}
async function loadChartData(days) {
chartDays = days;
// Update active class on buttons
document.querySelectorAll('.chart-filters button').forEach(btn => {
btn.classList.remove('active');
if (parseInt(btn.getAttribute('data-days')) === days) {
btn.classList.add('active');
}
});
try {
const res = await fetch(`/api/daily-data?days=${days}`);
const data = await res.json();
const labels = data.map(d => {
const date = new Date(d.date);
return date.toLocaleDateString('en-US', { day: 'numeric', month: 'short' });
});
const counts = data.map(d => d.total_count);
const batches = data.map(d => d.total_batches);
if (mainChart) mainChart.destroy();
const ctx = document.getElementById('mainChart').getContext('2d');
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
// Dynamic theme colors
const gridColor = isDark ? '#2C2C2E' : '#E5E7EB';
const textColor = isDark ? '#8E8E93' : '#86868B';
const barBg = isDark ? 'rgba(10, 132, 255, 0.7)' : 'rgba(0, 113, 227, 0.7)';
const barBorder = isDark ? '#0A84FF' : '#0071E3';
const lineBorder = isDark ? '#30D158' : '#34C759';
mainChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Total Sacks',
data: counts,
backgroundColor: barBg,
borderColor: barBorder,
borderWidth: 1.5,
borderRadius: 6,
yAxisID: 'y'
},
{
label: 'Total Batches',
data: batches,
type: 'line',
borderColor: lineBorder,
backgroundColor: 'transparent',
borderWidth: 2.5,
pointRadius: 4,
pointBackgroundColor: lineBorder,
tension: 0.35,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
position: 'top',
labels: {
usePointStyle: true,
padding: 16,
font: {
size: 11,
family: 'Inter, sans-serif'
},
color: textColor
}
},
tooltip: {
padding: 12,
cornerRadius: 8,
titleFont: { size: 12, family: 'Inter, sans-serif', weight: 'bold' },
bodyFont: { size: 11, family: 'Inter, sans-serif' }
}
},
scales: {
x: {
grid: { display: false },
ticks: {
font: { size: 10, family: 'Inter, sans-serif' },
color: textColor
}
},
y: {
type: 'linear',
display: true,
position: 'left',
grid: { color: gridColor },
ticks: {
color: textColor,
font: { size: 10, family: 'Inter, sans-serif' }
},
title: {
display: true,
text: 'Sacks Count',
color: textColor,
font: { size: 11, family: 'Inter, sans-serif', weight: 'bold' }
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
grid: { display: false },
ticks: {
color: textColor,
font: { size: 10, family: 'Inter, sans-serif' }
},
title: {
display: true,
text: 'Batches Count',
color: textColor,
font: { size: 11, family: 'Inter, sans-serif', weight: 'bold' }
}
}
}
}
});
} catch (err) {
console.error('Failed to load chart:', err);
}
}
// Redraw charts dynamically on theme switch
window.addEventListener('themeChanged', () => {
loadChartData(chartDays);
});
document.addEventListener('DOMContentLoaded', () => {
loadMetrics();
loadChartData(7);
});
</script>
{% endblock %}