mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
Add AMS humidity/temperature indicators with configurable thresholds
- Add dynamic humidity indicator with Bambu Lab style water drop icons
- Empty drop for good (dry), half-filled for fair, full for bad (wet)
- Configurable thresholds via Settings page
- Add dynamic temperature indicator with thermometer icons
- Empty for good, half-filled for fair, full for hot
- Colors match humidity: green/gold/red
- Add AMS Display Thresholds settings card
- Humidity thresholds (good/fair) configurable
- Temperature thresholds (good/fair) configurable
- Persisted in backend settings
- Fix AMS card stability issues
- Add ams_extruder_map to WebSocket broadcasts
- Cache AMS data and extruder map to prevent bouncing
- Fix L/R nozzle indicator for dual-nozzle H2 printers
- Fix status summary bar counting
- Don't count printers with unknown status as offline
- Theme-aware L/R nozzle badges (light/dark theme support)
This commit is contained in:
@@ -22,6 +22,12 @@ v∆v
|
||||
## Features
|
||||
|
||||
- **Multi-Printer Support** - Connect and monitor multiple Bambu Lab printers (H2C, H2D, H2S, X1, X1C, P1P, P1S, A1, A1 Mini)
|
||||
- **AMS Humidity & Temperature Monitoring** - Visual indicators for AMS dryness conditions:
|
||||
- Dynamic water drop icons (empty/half/full) based on humidity level
|
||||
- Dynamic thermometer icons showing temperature status
|
||||
- Color-coded values: green (good), gold (fair), red (bad)
|
||||
- Configurable thresholds in Settings
|
||||
- Dual-nozzle support with L/R indicators for H2 series
|
||||
- **Automatic Print Archiving** - Automatically saves 3MF files with full metadata extraction
|
||||
- **3D Model Preview** - Interactive Three.js viewer for archived prints
|
||||
- **Real-time Monitoring** - Live printer status via WebSocket with print progress, temperatures, layer count, and more
|
||||
|
||||
@@ -23,6 +23,12 @@ class AppSettings(BaseModel):
|
||||
# Language
|
||||
notification_language: str = Field(default="en", description="Language for push notifications (en, de)")
|
||||
|
||||
# AMS threshold settings for humidity and temperature coloring
|
||||
ams_humidity_good: int = Field(default=40, description="Humidity threshold for good (green): <= this value")
|
||||
ams_humidity_fair: int = Field(default=60, description="Humidity threshold for fair (orange): <= this value, > is red")
|
||||
ams_temp_good: float = Field(default=28.0, description="Temperature threshold for good (blue): <= this value")
|
||||
ams_temp_fair: float = Field(default=35.0, description="Temperature threshold for fair (orange): <= this value, > is red")
|
||||
|
||||
|
||||
class AppSettingsUpdate(BaseModel):
|
||||
"""Schema for updating settings (all fields optional)."""
|
||||
@@ -39,3 +45,7 @@ class AppSettingsUpdate(BaseModel):
|
||||
spoolman_sync_mode: str | None = None
|
||||
check_updates: bool | None = None
|
||||
notification_language: str | None = None
|
||||
ams_humidity_good: int | None = None
|
||||
ams_humidity_fair: int | None = None
|
||||
ams_temp_good: float | None = None
|
||||
ams_temp_fair: float | None = None
|
||||
|
||||
@@ -336,6 +336,9 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) ->
|
||||
"tag_uid": vt_tag_uid,
|
||||
}
|
||||
|
||||
# Get ams_extruder_map from raw_data (populated by MQTT handler from AMS info field)
|
||||
ams_extruder_map = raw_data.get("ams_extruder_map", {})
|
||||
|
||||
result = {
|
||||
"connected": state.connected,
|
||||
"state": state.state,
|
||||
@@ -358,6 +361,8 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None) ->
|
||||
"ams_status_main": state.ams_status_main,
|
||||
"ams_status_sub": state.ams_status_sub,
|
||||
"tray_now": state.tray_now,
|
||||
# Per-AMS extruder map: {ams_id: extruder_id} where 0=right, 1=left
|
||||
"ams_extruder_map": ams_extruder_map,
|
||||
# WiFi signal strength
|
||||
"wifi_signal": state.wifi_signal,
|
||||
}
|
||||
|
||||
@@ -247,6 +247,11 @@ export interface AppSettings {
|
||||
energy_tracking_mode: 'print' | 'total';
|
||||
check_updates: boolean;
|
||||
notification_language: string;
|
||||
// AMS threshold settings
|
||||
ams_humidity_good: number; // <= this is green
|
||||
ams_humidity_fair: number; // <= this is orange, > is red
|
||||
ams_temp_good: number; // <= this is green/blue
|
||||
ams_temp_fair: number; // <= this is orange, > is red
|
||||
}
|
||||
|
||||
export type AppSettingsUpdate = Partial<AppSettings>;
|
||||
|
||||
@@ -83,23 +83,63 @@ function WaterDropFull({ className }: { className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Thermometer SVG - empty outline
|
||||
function ThermometerEmpty({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 12 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z" stroke="#C3C2C1" strokeWidth="1" fill="none"/>
|
||||
<circle cx="6" cy="15" r="2.5" stroke="#C3C2C1" strokeWidth="1" fill="none"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Thermometer SVG - half filled (gold - same as humidity fair)
|
||||
function ThermometerHalf({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 12 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4.5" y="8" width="3" height="4.5" fill="#d4a017" rx="0.5"/>
|
||||
<circle cx="6" cy="15" r="2" fill="#d4a017"/>
|
||||
<path d="M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z" stroke="#C3C2C1" strokeWidth="1" fill="none"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Thermometer SVG - fully filled (red - same as humidity bad)
|
||||
function ThermometerFull({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 12 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4.5" y="3" width="3" height="9.5" fill="#c62828" rx="0.5"/>
|
||||
<circle cx="6" cy="15" r="2" fill="#c62828"/>
|
||||
<path d="M6 0.5C4.6 0.5 3.5 1.6 3.5 3V12.1C2.6 12.8 2 13.9 2 15C2 17.2 3.8 19 6 19C8.2 19 10 17.2 10 15C10 13.9 9.4 12.8 8.5 12.1V3C8.5 1.6 7.4 0.5 6 0.5Z" stroke="#C3C2C1" strokeWidth="1" fill="none"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Humidity indicator with water drop that fills based on level (Bambu Lab style)
|
||||
// Reference: https://github.com/theicedmango/bambu-humidity
|
||||
function HumidityIndicator({ humidity }: { humidity: number | string }) {
|
||||
const humidityValue = typeof humidity === 'string' ? parseInt(humidity, 10) : humidity;
|
||||
interface HumidityIndicatorProps {
|
||||
humidity: number | string;
|
||||
goodThreshold?: number; // <= this is green
|
||||
fairThreshold?: number; // <= this is orange, > is red
|
||||
}
|
||||
|
||||
// Status thresholds from bambu-humidity:
|
||||
// Good: ≤40% (green #22a352), Fair: 41-60% (gold #d4a017), Bad: >60% (red #c62828)
|
||||
function HumidityIndicator({ humidity, goodThreshold = 40, fairThreshold = 60 }: HumidityIndicatorProps) {
|
||||
const humidityValue = typeof humidity === 'string' ? parseInt(humidity, 10) : humidity;
|
||||
const good = typeof goodThreshold === 'number' ? goodThreshold : 40;
|
||||
const fair = typeof fairThreshold === 'number' ? fairThreshold : 60;
|
||||
|
||||
// Status thresholds (configurable via settings)
|
||||
// Good: ≤goodThreshold (green #22a352), Fair: ≤fairThreshold (gold #d4a017), Bad: >fairThreshold (red #c62828)
|
||||
let textColor: string;
|
||||
let statusText: string;
|
||||
|
||||
if (isNaN(humidityValue)) {
|
||||
textColor = '#C3C2C1';
|
||||
statusText = 'Unknown';
|
||||
} else if (humidityValue <= 40) {
|
||||
} else if (humidityValue <= good) {
|
||||
textColor = '#22a352'; // Green - Good
|
||||
statusText = 'Good';
|
||||
} else if (humidityValue <= 60) {
|
||||
} else if (humidityValue <= fair) {
|
||||
textColor = '#d4a017'; // Gold - Fair
|
||||
statusText = 'Fair';
|
||||
} else {
|
||||
@@ -107,25 +147,60 @@ function HumidityIndicator({ humidity }: { humidity: number | string }) {
|
||||
statusText = 'Bad';
|
||||
}
|
||||
|
||||
// Fill level based on humidity (matching bambu-humidity visual style)
|
||||
// ≤40% (Good): Half fill, 41-60% (Fair): Full fill, >60% (Bad): Full fill
|
||||
// Fill level based on status: Good=Empty (dry), Fair=Half, Bad=Full (wet)
|
||||
let DropComponent: React.FC<{ className?: string }>;
|
||||
if (isNaN(humidityValue)) {
|
||||
DropComponent = WaterDropEmpty;
|
||||
} else if (humidityValue <= 40) {
|
||||
DropComponent = WaterDropHalf;
|
||||
} else if (humidityValue <= good) {
|
||||
DropComponent = WaterDropEmpty; // Good - empty drop (dry)
|
||||
} else if (humidityValue <= fair) {
|
||||
DropComponent = WaterDropHalf; // Fair - half filled
|
||||
} else {
|
||||
DropComponent = WaterDropFull;
|
||||
DropComponent = WaterDropFull; // Bad - full (too humid)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1" title={`Humidity: ${humidityValue}% - ${statusText}`}>
|
||||
<div className="flex items-center justify-end gap-1" title={`Humidity: ${humidityValue}% - ${statusText}`}>
|
||||
<DropComponent className="w-3 h-4" />
|
||||
<span className="text-xs font-medium" style={{ color: textColor }}>{humidityValue}%</span>
|
||||
<span className="text-xs font-medium tabular-nums w-8 text-right" style={{ color: textColor }}>{humidityValue}%</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Temperature indicator with dynamic icon and coloring
|
||||
interface TemperatureIndicatorProps {
|
||||
temp: number;
|
||||
goodThreshold?: number; // <= this is blue
|
||||
fairThreshold?: number; // <= this is orange, > is red
|
||||
}
|
||||
|
||||
function TemperatureIndicator({ temp, goodThreshold = 28, fairThreshold = 35 }: TemperatureIndicatorProps) {
|
||||
// Ensure thresholds are numbers
|
||||
const good = typeof goodThreshold === 'number' ? goodThreshold : 28;
|
||||
const fair = typeof fairThreshold === 'number' ? fairThreshold : 35;
|
||||
|
||||
let textColor: string;
|
||||
let ThermoComponent: React.FC<{ className?: string }>;
|
||||
|
||||
if (temp <= good) {
|
||||
textColor = '#22a352'; // Green - good (same as humidity)
|
||||
ThermoComponent = ThermometerEmpty;
|
||||
} else if (temp <= fair) {
|
||||
textColor = '#d4a017'; // Gold - fair (same as humidity)
|
||||
ThermoComponent = ThermometerHalf;
|
||||
} else {
|
||||
textColor = '#c62828'; // Red - bad (same as humidity)
|
||||
ThermoComponent = ThermometerFull;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-1" title="Temperature">
|
||||
<ThermoComponent className="w-3 h-4" />
|
||||
<span className="tabular-nums w-12 text-right" style={{ color: textColor }}>{temp}°C</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Get AMS label: AMS-A/B/C/D for regular AMS, HT-A/B for AMS-HT (single spool)
|
||||
// Always use tray count as the source of truth (1 tray = AMS-HT, 4 trays = regular AMS)
|
||||
// AMS-HT uses IDs 128+ while regular AMS uses 0-3
|
||||
@@ -259,10 +334,14 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
let printing = 0;
|
||||
let idle = 0;
|
||||
let offline = 0;
|
||||
let loading = 0;
|
||||
|
||||
printers?.forEach((printer) => {
|
||||
const status = queryClient.getQueryData<{ connected: boolean; state: string | null }>(['printerStatus', printer.id]);
|
||||
if (!status?.connected) {
|
||||
if (status === undefined) {
|
||||
// Status not yet loaded - don't count as offline yet
|
||||
loading++;
|
||||
} else if (!status.connected) {
|
||||
offline++;
|
||||
} else if (status.state === 'RUNNING') {
|
||||
printing++;
|
||||
@@ -271,7 +350,7 @@ function StatusSummaryBar({ printers }: { printers: Printer[] | undefined }) {
|
||||
}
|
||||
});
|
||||
|
||||
return { printing, idle, offline, total: (printers?.length || 0) };
|
||||
return { printing, idle, offline, loading, total: (printers?.length || 0) };
|
||||
}, [printers, queryClient]);
|
||||
|
||||
// Subscribe to query cache changes to re-render when status updates
|
||||
@@ -323,11 +402,18 @@ function PrinterCard({
|
||||
hideIfDisconnected,
|
||||
maintenanceInfo,
|
||||
viewMode = 'expanded',
|
||||
amsThresholds,
|
||||
}: {
|
||||
printer: Printer;
|
||||
hideIfDisconnected?: boolean;
|
||||
maintenanceInfo?: PrinterMaintenanceInfo;
|
||||
viewMode?: ViewMode;
|
||||
amsThresholds?: {
|
||||
humidityGood: number;
|
||||
humidityFair: number;
|
||||
tempGood: number;
|
||||
tempFair: number;
|
||||
};
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
@@ -851,13 +937,20 @@ function PrinterCard({
|
||||
{(ams.humidity != null || ams.temp != null) && (
|
||||
<div className="flex items-center gap-3 text-xs">
|
||||
{ams.humidity != null && (
|
||||
<HumidityIndicator humidity={ams.humidity} />
|
||||
<div className="w-14 text-right">
|
||||
<HumidityIndicator
|
||||
humidity={ams.humidity}
|
||||
goodThreshold={amsThresholds?.humidityGood}
|
||||
fairThreshold={amsThresholds?.humidityFair}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{ams.temp != null && (
|
||||
<span className="flex items-center gap-1 text-orange-400" title="Temperature">
|
||||
<Thermometer className="w-3 h-3" />
|
||||
{ams.temp}°C
|
||||
</span>
|
||||
<TemperatureIndicator
|
||||
temp={ams.temp}
|
||||
goodThreshold={amsThresholds?.tempGood}
|
||||
fairThreshold={amsThresholds?.tempFair}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1480,6 +1573,12 @@ export function PrintersPage() {
|
||||
queryFn: api.getPrinters,
|
||||
});
|
||||
|
||||
// Fetch app settings for AMS thresholds
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['settings'],
|
||||
queryFn: api.getSettings,
|
||||
});
|
||||
|
||||
// Fetch all smart plugs to know which printers have them
|
||||
const { data: smartPlugs } = useQuery({
|
||||
queryKey: ['smart-plugs'],
|
||||
@@ -1761,6 +1860,12 @@ export function PrintersPage() {
|
||||
hideIfDisconnected={hideDisconnected}
|
||||
maintenanceInfo={maintenanceByPrinter[printer.id]}
|
||||
viewMode={viewMode}
|
||||
amsThresholds={settings ? {
|
||||
humidityGood: Number(settings.ams_humidity_good) || 40,
|
||||
humidityFair: Number(settings.ams_humidity_fair) || 60,
|
||||
tempGood: Number(settings.ams_temp_good) || 28,
|
||||
tempFair: Number(settings.ams_temp_fair) || 35,
|
||||
} : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1781,6 +1886,12 @@ export function PrintersPage() {
|
||||
hideIfDisconnected={hideDisconnected}
|
||||
maintenanceInfo={maintenanceByPrinter[printer.id]}
|
||||
viewMode={viewMode}
|
||||
amsThresholds={settings ? {
|
||||
humidityGood: Number(settings.ams_humidity_good) || 40,
|
||||
humidityFair: Number(settings.ams_humidity_fair) || 60,
|
||||
tempGood: Number(settings.ams_temp_good) || 28,
|
||||
tempFair: Number(settings.ams_temp_fair) || 35,
|
||||
} : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Save, Loader2, Check, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe } from 'lucide-react';
|
||||
import { Save, Loader2, Check, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api } from '../api/client';
|
||||
import type { AppSettings, SmartPlug, NotificationProvider, UpdateStatus } from '../api/client';
|
||||
@@ -106,7 +106,11 @@ export function SettingsPage() {
|
||||
settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
|
||||
settings.energy_tracking_mode !== localSettings.energy_tracking_mode ||
|
||||
settings.check_updates !== localSettings.check_updates ||
|
||||
settings.notification_language !== localSettings.notification_language;
|
||||
settings.notification_language !== localSettings.notification_language ||
|
||||
settings.ams_humidity_good !== localSettings.ams_humidity_good ||
|
||||
settings.ams_humidity_fair !== localSettings.ams_humidity_fair ||
|
||||
settings.ams_temp_good !== localSettings.ams_temp_good ||
|
||||
settings.ams_temp_fair !== localSettings.ams_temp_fair;
|
||||
setHasChanges(changed);
|
||||
}
|
||||
}, [settings, localSettings]);
|
||||
@@ -382,6 +386,109 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold text-white">AMS Display Thresholds</h2>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-bambu-gray">
|
||||
Configure color thresholds for AMS humidity and temperature indicators.
|
||||
</p>
|
||||
|
||||
{/* Humidity Thresholds */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-white">
|
||||
<Droplets className="w-4 h-4 text-blue-400" />
|
||||
<span className="font-medium">Humidity</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Good (green) ≤
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={localSettings.ams_humidity_good ?? 40}
|
||||
onChange={(e) => updateSetting('ams_humidity_good', parseInt(e.target.value) || 40)}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
<span className="text-bambu-gray">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Fair (orange) ≤
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={localSettings.ams_humidity_fair ?? 60}
|
||||
onChange={(e) => updateSetting('ams_humidity_fair', parseInt(e.target.value) || 60)}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
<span className="text-bambu-gray">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
Above fair threshold shows as red (bad)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Temperature Thresholds */}
|
||||
<div className="space-y-3 pt-2 border-t border-bambu-dark-tertiary">
|
||||
<div className="flex items-center gap-2 text-white">
|
||||
<Thermometer className="w-4 h-4 text-orange-400" />
|
||||
<span className="font-medium">Temperature</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Good (blue) ≤
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="60"
|
||||
value={localSettings.ams_temp_good ?? 28}
|
||||
onChange={(e) => updateSetting('ams_temp_good', parseFloat(e.target.value) || 28)}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
<span className="text-bambu-gray">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1">
|
||||
Fair (orange) ≤
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="60"
|
||||
value={localSettings.ams_temp_fair ?? 35}
|
||||
onChange={(e) => updateSetting('ams_temp_fair', parseFloat(e.target.value) || 35)}
|
||||
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
|
||||
/>
|
||||
<span className="text-bambu-gray">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-bambu-gray">
|
||||
Above fair threshold shows as red (hot)
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Second Column - Spoolman & Updates */}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -7,8 +7,8 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
|
||||
<script type="module" crossorigin src="/assets/index-DzQp7Kty.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-SGO6jLr2.css">
|
||||
<script type="module" crossorigin src="/assets/index-CUSzovgk.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DL3S7zom.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user