Improve Statistics dashboard widgets to utilize space when expanded (#72)

- Add size-aware rendering to Dashboard component, passing widget size
    (1/4, 1/2, full) to component render functions
  - PrintCalendar: Dynamic cell sizing using ResizeObserver to fill
    available width, shows 3/6/12 months based on widget size
  - SuccessRateWidget: Per-printer breakdown at larger sizes
  - TimeAccuracyWidget: Per-printer breakdown at larger sizes
  - FilamentTypesWidget: Grid layout at larger sizes
  - FailureAnalysisWidget: More failure reasons at larger sizes
  - Bump service worker cache version
This commit is contained in:
maziggy
2026-01-11 10:13:09 +01:00
parent b1653acc71
commit f7f4d88222
9 changed files with 311 additions and 149 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
// Bambuddy Service Worker
const CACHE_NAME = 'bambuddy-v4';
const STATIC_CACHE = 'bambuddy-static-v4';
const CACHE_NAME = 'bambuddy-v15';
const STATIC_CACHE = 'bambuddy-static-v15';
// Static assets to cache on install
const STATIC_ASSETS = [
+9 -7
View File
@@ -22,7 +22,8 @@ import { Button } from './Button';
export interface DashboardWidget {
id: string;
title: string;
component: ReactNode;
/** Render function that receives the current size for responsive content */
component: ReactNode | ((size: 1 | 2 | 4) => ReactNode);
defaultVisible?: boolean;
defaultSize?: 1 | 2 | 4; // 1 = quarter, 2 = half, 4 = full width (default)
}
@@ -50,7 +51,7 @@ interface LayoutState {
function SortableWidget({
id,
title,
children,
component,
isHidden,
size,
onToggleVisibility,
@@ -58,7 +59,7 @@ function SortableWidget({
}: {
id: string;
title: string;
children: ReactNode;
component: ReactNode | ((size: 1 | 2 | 4) => ReactNode);
isHidden: boolean;
size: 1 | 2 | 4;
onToggleVisibility: () => void;
@@ -127,7 +128,9 @@ function SortableWidget({
</div>
</div>
{/* Widget Content */}
<div className="p-4">{children}</div>
<div className="p-4">
{typeof component === 'function' ? component(size) : component}
</div>
</div>
);
}
@@ -329,13 +332,12 @@ export function Dashboard({ widgets, storageKey, columns = 4, hideControls = fal
key={widget.id}
id={widget.id}
title={widget.title}
component={widget.component}
isHidden={layout.hidden.includes(widget.id)}
size={layout.sizes[widget.id] || 2}
onToggleVisibility={() => toggleVisibility(widget.id)}
onToggleSize={() => toggleSize(widget.id)}
>
{widget.component}
</SortableWidget>
/>
))}
</div>
</SortableContext>
+107 -61
View File
@@ -1,4 +1,4 @@
import { useMemo } from 'react';
import { useMemo, useRef, useState, useEffect } from 'react';
interface PrintCalendarProps {
printDates: string[]; // Array of ISO date strings
@@ -6,6 +6,23 @@ interface PrintCalendarProps {
}
export function PrintCalendar({ printDates, months = 3 }: PrintCalendarProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(0);
// Measure container width
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const observer = new ResizeObserver((entries) => {
const width = entries[0]?.contentRect.width || 0;
setContainerWidth(width);
});
observer.observe(container);
return () => observer.disconnect();
}, []);
const { weeks, monthLabels, printCounts } = useMemo(() => {
// Count prints per day
const counts: Record<string, number> = {};
@@ -68,72 +85,101 @@ export function PrintCalendar({ printDates, months = 3 }: PrintCalendarProps) {
const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// Calculate cell size based on container width
const numWeeks = weeks.length;
const dayLabelWidth = 32; // Space for day labels (Mon, Wed, Fri)
const gap = 2; // Gap between cells
const availableWidth = containerWidth - dayLabelWidth - 16; // 16px padding
const calculatedCellSize = numWeeks > 0 ? Math.floor((availableWidth - (numWeeks - 1) * gap) / numWeeks) : 12;
// Clamp cell size between 8 and 20 pixels
const cellSize = Math.max(8, Math.min(20, calculatedCellSize));
const fontSize = cellSize <= 10 ? 10 : 12;
return (
<div className="overflow-x-auto">
{/* Month labels */}
<div className="flex mb-1 ml-8">
{monthLabels.map(({ month, weekIndex }, i) => (
<div
key={i}
className="text-xs text-bambu-gray"
style={{ marginLeft: i === 0 ? 0 : `${(weekIndex - (monthLabels[i - 1]?.weekIndex || 0)) * 14 - 24}px` }}
>
{month}
<div ref={containerRef} className="w-full flex justify-center">
{containerWidth > 0 && (
<div>
{/* Month labels */}
<div className="flex mb-1" style={{ marginLeft: dayLabelWidth + 4 }}>
{monthLabels.map(({ month, weekIndex }, i) => (
<div
key={i}
className="text-bambu-gray"
style={{
fontSize,
marginLeft: i === 0 ? 0 : `${(weekIndex - (monthLabels[i - 1]?.weekIndex || 0)) * (cellSize + gap) - 24}px`,
}}
>
{month}
</div>
))}
</div>
))}
</div>
<div className="flex gap-0.5">
{/* Day labels */}
<div className="flex flex-col gap-0.5 mr-1">
{dayLabels.map((day, i) => (
<div
key={day}
className="h-3 text-xs text-bambu-gray flex items-center"
style={{ visibility: i % 2 === 1 ? 'visible' : 'hidden' }}
>
{day}
</div>
))}
</div>
{/* Calendar grid */}
{weeks.map((week, weekIndex) => (
<div key={weekIndex} className="flex flex-col gap-0.5">
{[0, 1, 2, 3, 4, 5, 6].map((dayOfWeek) => {
const day = week.find((d) => d.getDay() === dayOfWeek);
if (!day) {
return <div key={dayOfWeek} className="w-3 h-3" />;
}
const dateStr = day.toISOString().split('T')[0];
const count = printCounts[dateStr] || 0;
const isToday = dateStr === new Date().toISOString().split('T')[0];
return (
<div className="flex" style={{ gap }}>
{/* Day labels */}
<div className="flex flex-col" style={{ gap, marginRight: 4, width: dayLabelWidth }}>
{dayLabels.map((day, i) => (
<div
key={dayOfWeek}
className={`w-3 h-3 rounded-sm ${getColor(count)} ${isToday ? 'ring-1 ring-white' : ''}`}
title={`${day.toLocaleDateString()}: ${count} print${count !== 1 ? 's' : ''}`}
/>
);
})}
</div>
))}
</div>
key={day}
className="text-bambu-gray flex items-center"
style={{
width: dayLabelWidth,
height: cellSize,
fontSize,
visibility: i % 2 === 1 ? 'visible' : 'hidden',
}}
>
{day}
</div>
))}
</div>
{/* Legend */}
<div className="flex items-center gap-2 mt-3 text-xs text-bambu-gray">
<span>Less</span>
<div className="flex gap-0.5">
<div className="w-3 h-3 rounded-sm bg-bambu-dark" />
<div className="w-3 h-3 rounded-sm bg-bambu-green/30" />
<div className="w-3 h-3 rounded-sm bg-bambu-green/50" />
<div className="w-3 h-3 rounded-sm bg-bambu-green/75" />
<div className="w-3 h-3 rounded-sm bg-bambu-green" />
{/* Calendar grid */}
{weeks.map((week, weekIdx) => (
<div key={weekIdx} className="flex flex-col" style={{ gap }}>
{[0, 1, 2, 3, 4, 5, 6].map((dayOfWeek) => {
const day = week.find((d) => d.getDay() === dayOfWeek);
if (!day) {
return (
<div
key={dayOfWeek}
style={{ width: cellSize, height: cellSize }}
/>
);
}
const dateStr = day.toISOString().split('T')[0];
const count = printCounts[dateStr] || 0;
const isToday = dateStr === new Date().toISOString().split('T')[0];
return (
<div
key={dayOfWeek}
className={`rounded-sm ${getColor(count)} ${isToday ? 'ring-1 ring-white' : ''}`}
style={{ width: cellSize, height: cellSize }}
title={`${day.toLocaleDateString()}: ${count} print${count !== 1 ? 's' : ''}`}
/>
);
})}
</div>
))}
</div>
{/* Legend */}
<div className="flex items-center gap-2 mt-3 text-bambu-gray" style={{ fontSize }}>
<span>Less</span>
<div className="flex" style={{ gap }}>
<div className="rounded-sm bg-bambu-dark" style={{ width: cellSize, height: cellSize }} />
<div className="rounded-sm bg-bambu-green/30" style={{ width: cellSize, height: cellSize }} />
<div className="rounded-sm bg-bambu-green/50" style={{ width: cellSize, height: cellSize }} />
<div className="rounded-sm bg-bambu-green/75" style={{ width: cellSize, height: cellSize }} />
<div className="rounded-sm bg-bambu-green" style={{ width: cellSize, height: cellSize }} />
</div>
<span>More</span>
</div>
</div>
<span>More</span>
</div>
)}
</div>
);
}
+187 -73
View File
@@ -103,48 +103,83 @@ function QuickStatsWidget({
function SuccessRateWidget({
stats,
printerMap,
size = 1,
}: {
stats: {
total_prints: number;
successful_prints: number;
failed_prints: number;
prints_by_printer: Record<string, number>;
} | undefined;
printerMap: Map<string, string>;
size?: 1 | 2 | 4;
}) {
const successRate = stats?.total_prints
? Math.round((stats.successful_prints / stats.total_prints) * 100)
: 0;
// Scale gauge size based on widget size
const gaugeSize = size === 1 ? 112 : size === 2 ? 128 : 144;
const radius = gaugeSize / 2 - 8;
const circumference = radius * 2 * Math.PI;
return (
<div className="flex items-center gap-6">
<div className="relative w-28 h-28">
<div className="relative flex-shrink-0" style={{ width: gaugeSize, height: gaugeSize }}>
<svg className="w-full h-full -rotate-90">
<circle cx="56" cy="56" r="48" fill="none" stroke="#3d3d3d" strokeWidth="10" />
<circle
cx="56"
cy="56"
r="48"
cx={gaugeSize / 2}
cy={gaugeSize / 2}
r={radius}
fill="none"
stroke="#3d3d3d"
strokeWidth="10"
/>
<circle
cx={gaugeSize / 2}
cy={gaugeSize / 2}
r={radius}
fill="none"
stroke="#00ae42"
strokeWidth="10"
strokeLinecap="round"
strokeDasharray={`${successRate * 3.02} 302`}
strokeDasharray={`${(successRate / 100) * circumference} ${circumference}`}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-xl font-bold text-white">{successRate}%</span>
<span className={`font-bold text-white ${size >= 2 ? 'text-2xl' : 'text-xl'}`}>{successRate}%</span>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-bambu-green" />
<span className="text-sm text-bambu-gray">Successful:</span>
<span className="text-sm text-white font-medium">{stats?.successful_prints || 0}</span>
</div>
<div className="flex items-center gap-2">
<XCircle className="w-4 h-4 text-red-400" />
<span className="text-sm text-bambu-gray">Failed:</span>
<span className="text-sm text-white font-medium">{stats?.failed_prints || 0}</span>
<div className="flex-1 min-w-0">
<div className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-bambu-green flex-shrink-0" />
<span className="text-sm text-bambu-gray">Successful:</span>
<span className="text-sm text-white font-medium">{stats?.successful_prints || 0}</span>
</div>
<div className="flex items-center gap-2">
<XCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
<span className="text-sm text-bambu-gray">Failed:</span>
<span className="text-sm text-white font-medium">{stats?.failed_prints || 0}</span>
</div>
</div>
{/* Show per-printer breakdown when expanded */}
{size >= 2 && stats?.prints_by_printer && Object.keys(stats.prints_by_printer).length > 0 && (
<div className="mt-4 pt-4 border-t border-bambu-dark-tertiary">
<p className="text-xs text-bambu-gray font-medium mb-2">Prints by Printer</p>
<div className={`grid gap-x-6 gap-y-1 ${size === 4 ? 'grid-cols-3' : 'grid-cols-2'}`} style={{ width: 'fit-content' }}>
{Object.entries(stats.prints_by_printer).map(([printerId, count]) => (
<div key={printerId} className="flex items-center gap-3 text-sm">
<span className="text-bambu-gray truncate max-w-[120px]">
{printerMap.get(printerId) || `Printer ${printerId}`}
</span>
<span className="text-white font-medium">{count}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
@@ -153,12 +188,14 @@ function SuccessRateWidget({
function TimeAccuracyWidget({
stats,
printerMap,
size = 1,
}: {
stats: {
average_time_accuracy: number | null;
time_accuracy_by_printer: Record<string, number> | null;
} | undefined;
printerMap: Map<string, string>;
size?: 1 | 2 | 4;
}) {
const accuracy = stats?.average_time_accuracy;
@@ -184,38 +221,56 @@ function TimeAccuracyWidget({
const color = getColor(accuracy);
const deviation = accuracy - 100;
// Scale gauge size based on widget size
const gaugeSize = size === 1 ? 112 : size === 2 ? 128 : 144;
const radius = gaugeSize / 2 - 8;
const circumference = radius * 2 * Math.PI;
// Show more printers when expanded
const maxPrinters = size === 1 ? 3 : size === 2 ? 6 : 999;
const printerEntries = stats?.time_accuracy_by_printer
? Object.entries(stats.time_accuracy_by_printer).slice(0, maxPrinters)
: [];
return (
<div className="flex items-center gap-6">
<div className="relative w-28 h-28">
<div className="relative flex-shrink-0" style={{ width: gaugeSize, height: gaugeSize }}>
<svg className="w-full h-full -rotate-90">
<circle cx="56" cy="56" r="48" fill="none" stroke="#3d3d3d" strokeWidth="10" />
<circle
cx="56"
cy="56"
r="48"
cx={gaugeSize / 2}
cy={gaugeSize / 2}
r={radius}
fill="none"
stroke="#3d3d3d"
strokeWidth="10"
/>
<circle
cx={gaugeSize / 2}
cy={gaugeSize / 2}
r={radius}
fill="none"
stroke={color}
strokeWidth="10"
strokeLinecap="round"
strokeDasharray={`${normalizedForGauge * 3.02} 302`}
strokeDasharray={`${(normalizedForGauge / 100) * circumference} ${circumference}`}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-xl font-bold text-white">{accuracy.toFixed(0)}%</span>
<span className={`font-bold text-white ${size >= 2 ? 'text-2xl' : 'text-xl'}`}>{accuracy.toFixed(0)}%</span>
<span className={`text-xs ${deviation >= 0 ? 'text-blue-400' : 'text-orange-400'}`}>
{deviation >= 0 ? '+' : ''}{deviation.toFixed(0)}%
</span>
</div>
</div>
<div className="space-y-2 flex-1">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 text-xs text-bambu-gray">
<Target className="w-3 h-3" />
<Target className="w-3 h-3 flex-shrink-0" />
<span>100% = perfect estimate</span>
</div>
{stats?.time_accuracy_by_printer && Object.keys(stats.time_accuracy_by_printer).length > 0 && (
<div className="space-y-1 mt-2">
{Object.entries(stats.time_accuracy_by_printer).slice(0, 3).map(([printerId, acc]) => (
<div key={printerId} className="flex items-center justify-between text-xs">
{printerEntries.length > 0 && (
<div className={`mt-2 ${size === 4 ? 'grid grid-cols-3 gap-x-6 gap-y-1' : size === 2 ? 'grid grid-cols-2 gap-x-6 gap-y-1' : 'space-y-1'}`} style={{ width: 'fit-content' }}>
{printerEntries.map(([printerId, acc]) => (
<div key={printerId} className="flex items-center gap-2 text-xs">
<span className="text-bambu-gray truncate max-w-[100px]">
{printerMap.get(printerId) || `Printer ${printerId}`}
</span>
@@ -236,11 +291,13 @@ function TimeAccuracyWidget({
function FilamentTypesWidget({
stats,
size = 1,
}: {
stats: {
total_prints: number;
prints_by_filament_type: Record<string, number>;
} | undefined;
size?: 1 | 2 | 4;
}) {
if (!stats?.prints_by_filament_type || Object.keys(stats.prints_by_filament_type).length === 0) {
return <p className="text-bambu-gray text-center py-4">No filament data available</p>;
@@ -251,9 +308,39 @@ function FilamentTypesWidget({
([, a], [, b]) => b - a
);
// Limit entries based on size
const maxEntries = size === 1 ? 5 : size === 2 ? 8 : 999;
const displayEntries = sortedEntries.slice(0, maxEntries);
const hasMore = sortedEntries.length > maxEntries;
// Use grid layout when expanded
if (size === 4 && displayEntries.length > 4) {
return (
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
{displayEntries.map(([type, count]) => {
const percentage = Math.round((count / (stats.total_prints || 1)) * 100);
return (
<div key={type}>
<div className="flex justify-between text-sm mb-1">
<span className="text-white truncate max-w-[120px]">{type}</span>
<span className="text-bambu-gray">{count}</span>
</div>
<div className="h-2 bg-bambu-dark rounded-full">
<div
className="h-full bg-bambu-green rounded-full transition-all"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
})}
</div>
);
}
return (
<div className="space-y-3">
{sortedEntries.map(([type, count]) => {
{displayEntries.map(([type, count]) => {
const percentage = Math.round((count / (stats.total_prints || 1)) * 100);
return (
<div key={type}>
@@ -270,12 +357,25 @@ function FilamentTypesWidget({
</div>
);
})}
{hasMore && (
<p className="text-xs text-bambu-gray text-center pt-1">
+{sortedEntries.length - maxEntries} more types
</p>
)}
</div>
);
}
function PrintActivityWidget({ printDates }: { printDates: string[] }) {
return <PrintCalendar printDates={printDates} months={4} />;
function PrintActivityWidget({
printDates,
size = 2,
}: {
printDates: string[];
size?: 1 | 2 | 4;
}) {
// Show more months when widget is larger - cell size auto-calculated
const months = size === 1 ? 3 : size === 2 ? 6 : 12;
return <PrintCalendar printDates={printDates} months={months} />;
}
function PrintsByPrinterWidget({
@@ -321,7 +421,7 @@ function FilamentTrendsWidget({
return <FilamentTrends archives={archives} currency={currency} />;
}
function FailureAnalysisWidget() {
function FailureAnalysisWidget({ size = 1 }: { size?: 1 | 2 | 4 }) {
const { data: analysis, isLoading } = useQuery({
queryKey: ['failureAnalysis'],
queryFn: () => api.getFailureAnalysis({ days: 30 }),
@@ -339,50 +439,63 @@ function FailureAnalysisWidget() {
return <p className="text-bambu-gray text-center py-4">No print data in the last 30 days</p>;
}
const topReasons = Object.entries(analysis.failures_by_reason)
.sort(([, a], [, b]) => b - a)
.slice(0, 5);
// Show more reasons when expanded
const maxReasons = size === 1 ? 5 : size === 2 ? 8 : 999;
const allReasons = Object.entries(analysis.failures_by_reason).sort(([, a], [, b]) => b - a);
const topReasons = allReasons.slice(0, maxReasons);
const hasMore = allReasons.length > maxReasons;
return (
<div className="space-y-4">
<div className={`${size >= 2 ? 'flex gap-8' : 'space-y-4'}`}>
{/* Summary */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<AlertTriangle className={`w-5 h-5 ${analysis.failure_rate > 20 ? 'text-red-400' : analysis.failure_rate > 10 ? 'text-yellow-400' : 'text-bambu-green'}`} />
<span className="text-2xl font-bold text-white">{analysis.failure_rate.toFixed(1)}%</span>
<span className="text-sm text-bambu-gray">failure rate</span>
<div className={size >= 2 ? 'flex-shrink-0' : ''}>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<AlertTriangle className={`w-5 h-5 ${analysis.failure_rate > 20 ? 'text-red-400' : analysis.failure_rate > 10 ? 'text-yellow-400' : 'text-bambu-green'}`} />
<span className={`font-bold text-white ${size >= 2 ? 'text-3xl' : 'text-2xl'}`}>{analysis.failure_rate.toFixed(1)}%</span>
</div>
</div>
<div className="text-sm text-bambu-gray">
<div className="text-sm text-bambu-gray mt-1">
{analysis.failed_prints} / {analysis.total_prints} prints failed
</div>
{/* Trend indicator */}
{analysis.trend && analysis.trend.length >= 2 && (
<div className={`${size >= 2 ? 'mt-4' : 'mt-2 pt-2 border-t border-bambu-dark-tertiary'}`}>
<div className="flex items-center gap-2 text-sm">
<TrendingDown className={`w-4 h-4 ${
analysis.trend[analysis.trend.length - 1].failure_rate < analysis.trend[analysis.trend.length - 2].failure_rate
? 'text-bambu-green'
: 'text-red-400'
}`} />
<span className="text-bambu-gray">
Last week: {analysis.trend[analysis.trend.length - 1].failure_rate.toFixed(1)}%
</span>
</div>
</div>
)}
</div>
{/* Top Failure Reasons */}
{/* Failure Reasons */}
{topReasons.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-bambu-gray font-medium">Top Failure Reasons</p>
{topReasons.map(([reason, count]) => (
<div key={reason} className="flex items-center justify-between text-sm">
<span className="text-white truncate max-w-[200px]">{reason || 'Unknown'}</span>
<span className="text-bambu-gray">{count}</span>
</div>
))}
</div>
)}
{/* Trend indicator */}
{analysis.trend && analysis.trend.length >= 2 && (
<div className="pt-2 border-t border-bambu-dark-tertiary">
<div className="flex items-center gap-2 text-sm">
<TrendingDown className={`w-4 h-4 ${
analysis.trend[analysis.trend.length - 1].failure_rate < analysis.trend[analysis.trend.length - 2].failure_rate
? 'text-bambu-green'
: 'text-red-400'
}`} />
<span className="text-bambu-gray">
Last week: {analysis.trend[analysis.trend.length - 1].failure_rate.toFixed(1)}%
</span>
<div className={`flex-1 ${size >= 2 ? 'border-l border-bambu-dark-tertiary pl-8' : 'pt-2'}`}>
<p className="text-xs text-bambu-gray font-medium mb-2">
{size >= 2 ? 'Failure Reasons' : 'Top Failure Reasons'}
</p>
<div className={`${size === 4 ? 'grid grid-cols-2 gap-x-6 gap-y-1' : 'space-y-1'}`}>
{topReasons.map(([reason, count]) => (
<div key={reason} className="flex items-center justify-between text-sm">
<span className={`text-white truncate ${size === 4 ? 'max-w-[200px]' : 'max-w-[160px]'}`}>
{reason || 'Unknown'}
</span>
<span className="text-bambu-gray ml-2">{count}</span>
</div>
))}
</div>
{hasMore && (
<p className="text-xs text-bambu-gray mt-2">
+{allReasons.length - maxReasons} more reasons
</p>
)}
</div>
)}
</div>
@@ -473,6 +586,7 @@ export function StatsPage() {
// Define dashboard widgets
// Sizes: 1 = quarter (1/4), 2 = half (1/2), 4 = full width
// Widgets can use render functions to receive the current size for responsive content
const widgets: DashboardWidget[] = [
{
id: 'quick-stats',
@@ -483,31 +597,31 @@ export function StatsPage() {
{
id: 'success-rate',
title: 'Success Rate',
component: <SuccessRateWidget stats={stats} />,
component: (size) => <SuccessRateWidget stats={stats} printerMap={printerMap} size={size} />,
defaultSize: 1,
},
{
id: 'time-accuracy',
title: 'Time Accuracy',
component: <TimeAccuracyWidget stats={stats} printerMap={printerMap} />,
component: (size) => <TimeAccuracyWidget stats={stats} printerMap={printerMap} size={size} />,
defaultSize: 1,
},
{
id: 'filament-types',
title: 'Filament Types',
component: <FilamentTypesWidget stats={stats} />,
component: (size) => <FilamentTypesWidget stats={stats} size={size} />,
defaultSize: 1,
},
{
id: 'failure-analysis',
title: 'Failure Analysis (30 days)',
component: <FailureAnalysisWidget />,
component: (size) => <FailureAnalysisWidget size={size} />,
defaultSize: 1,
},
{
id: 'print-activity',
title: 'Print Activity',
component: <PrintActivityWidget printDates={printDates} />,
component: (size) => <PrintActivityWidget printDates={printDates} size={size} />,
defaultSize: 2,
},
{
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-DNaTWne2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BEtulymk.css">
<script type="module" crossorigin src="/assets/index-CMSE_lmd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-48h_gFgJ.css">
</head>
<body>
<div id="root"></div>
+2 -2
View File
@@ -1,6 +1,6 @@
// Bambuddy Service Worker
const CACHE_NAME = 'bambuddy-v4';
const STATIC_CACHE = 'bambuddy-static-v4';
const CACHE_NAME = 'bambuddy-v15';
const STATIC_CACHE = 'bambuddy-static-v15';
// Static assets to cache on install
const STATIC_ASSETS = [