mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
Wired up printer parts button
This commit is contained in:
@@ -595,3 +595,29 @@ async def set_liveview(
|
||||
success=success,
|
||||
message=f"Live view {'enabled' if request.enable else 'disabled'}" if success else "Failed to set live view"
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Status Refresh Endpoint
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/{printer_id}/control/refresh", response_model=ControlResponse)
|
||||
async def refresh_status(
|
||||
printer_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Request a full status update from the printer.
|
||||
|
||||
This sends a 'pushall' command to get the latest data including nozzle info,
|
||||
AMS status, and all other printer state.
|
||||
"""
|
||||
await get_printer_or_404(printer_id, db)
|
||||
|
||||
success = printer_manager.request_status_update(printer_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=503, detail="Printer not connected")
|
||||
|
||||
return ControlResponse(
|
||||
success=success,
|
||||
message="Status refresh requested"
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ from backend.app.schemas.printer import (
|
||||
HMSErrorResponse,
|
||||
AMSUnit,
|
||||
AMSTray,
|
||||
NozzleInfoResponse,
|
||||
)
|
||||
from backend.app.services.printer_manager import printer_manager
|
||||
from backend.app.services.bambu_ftp import (
|
||||
@@ -183,6 +184,15 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
k=vt_data.get("k"),
|
||||
)
|
||||
|
||||
# Convert nozzle info to response format
|
||||
nozzles = [
|
||||
NozzleInfoResponse(
|
||||
nozzle_type=n.nozzle_type,
|
||||
nozzle_diameter=n.nozzle_diameter,
|
||||
)
|
||||
for n in (state.nozzles or [])
|
||||
]
|
||||
|
||||
return PrinterStatus(
|
||||
id=printer_id,
|
||||
name=printer.name,
|
||||
@@ -204,6 +214,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db)
|
||||
sdcard=state.sdcard,
|
||||
timelapse=state.timelapse,
|
||||
ipcam=state.ipcam,
|
||||
nozzles=nozzles,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,11 @@ class AMSUnit(BaseModel):
|
||||
tray: list[AMSTray] = []
|
||||
|
||||
|
||||
class NozzleInfoResponse(BaseModel):
|
||||
nozzle_type: str = "" # "stainless_steel" or "hardened_steel"
|
||||
nozzle_diameter: str = "" # e.g., "0.4"
|
||||
|
||||
|
||||
class PrinterStatus(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
@@ -79,3 +84,4 @@ class PrinterStatus(BaseModel):
|
||||
sdcard: bool = False # SD card inserted
|
||||
timelapse: bool = False # Timelapse recording active
|
||||
ipcam: bool = False # Live view enabled
|
||||
nozzles: list[NozzleInfoResponse] = [] # Nozzle hardware info (index 0=left/primary, 1=right)
|
||||
|
||||
@@ -47,6 +47,13 @@ class KProfile:
|
||||
setting_id: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class NozzleInfo:
|
||||
"""Nozzle hardware configuration."""
|
||||
nozzle_type: str = "" # "stainless_steel" or "hardened_steel"
|
||||
nozzle_diameter: str = "" # e.g., "0.4"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrinterState:
|
||||
connected: bool = False
|
||||
@@ -66,6 +73,8 @@ class PrinterState:
|
||||
sdcard: bool = False # SD card inserted
|
||||
timelapse: bool = False # Timelapse recording active
|
||||
ipcam: bool = False # Live view / camera streaming enabled
|
||||
# Nozzle hardware info (for dual nozzle printers, index 0 = left, 1 = right)
|
||||
nozzles: list = field(default_factory=lambda: [NozzleInfo(), NozzleInfo()])
|
||||
|
||||
|
||||
class BambuMQTTClient:
|
||||
@@ -120,8 +129,10 @@ class BambuMQTTClient:
|
||||
if rc == 0:
|
||||
self.state.connected = True
|
||||
client.subscribe(self.topic_subscribe)
|
||||
# Request full status update
|
||||
# Request full status update (includes nozzle info in push_status response)
|
||||
self._request_push_all()
|
||||
# Note: get_accessories returns stale nozzle data on H2D, so we don't use it.
|
||||
# The correct nozzle data comes from push_status.
|
||||
# Prime K-profile request (Bambu printers often ignore first request)
|
||||
self._prime_kprofile_request()
|
||||
# Immediately broadcast connection state change
|
||||
@@ -176,13 +187,19 @@ class BambuMQTTClient:
|
||||
# Handle xcam data (camera settings) at top level
|
||||
if "xcam" in payload:
|
||||
xcam_data = payload["xcam"]
|
||||
logger.info(f"[{self.serial_number}] Received xcam data: {xcam_data}")
|
||||
logger.debug(f"[{self.serial_number}] Received xcam data: {xcam_data}")
|
||||
if isinstance(xcam_data, dict):
|
||||
if "ipcam_record" in xcam_data:
|
||||
self.state.ipcam = xcam_data.get("ipcam_record") == "enable"
|
||||
if "timelapse" in xcam_data:
|
||||
self.state.timelapse = xcam_data.get("timelapse") == "enable"
|
||||
|
||||
# Handle system responses (accessories info, etc.)
|
||||
if "system" in payload:
|
||||
system_data = payload["system"]
|
||||
logger.info(f"[{self.serial_number}] Received system data: {system_data}")
|
||||
self._handle_system_response(system_data)
|
||||
|
||||
if "print" in payload:
|
||||
print_data = payload["print"]
|
||||
# Log when we see gcode_state changes
|
||||
@@ -211,6 +228,22 @@ class BambuMQTTClient:
|
||||
|
||||
self._update_state(print_data)
|
||||
|
||||
def _handle_system_response(self, data: dict):
|
||||
"""Handle system responses including accessories info.
|
||||
|
||||
Note: get_accessories returns stale/incorrect nozzle_type data on H2D.
|
||||
The correct nozzle data comes from push_status, so we don't update
|
||||
nozzle type/diameter from get_accessories. We just log the response
|
||||
for debugging purposes.
|
||||
"""
|
||||
command = data.get("command")
|
||||
|
||||
if command == "get_accessories":
|
||||
# Log response for debugging - but DON'T use it to update nozzle data
|
||||
# because it returns stale values (e.g., 'stainless_steel' when the
|
||||
# actual nozzle is 'HH01' hardened steel high-flow)
|
||||
logger.info(f"[{self.serial_number}] Accessories response (not used for nozzle data): {data}")
|
||||
|
||||
def _handle_ams_data(self, ams_data):
|
||||
"""Handle AMS data changes for Spoolman integration.
|
||||
|
||||
@@ -283,6 +316,12 @@ class BambuMQTTClient:
|
||||
if temp_fields and not hasattr(self, '_temp_fields_logged'):
|
||||
logger.info(f"[{self.serial_number}] Temperature fields in MQTT data: {temp_fields}")
|
||||
self._temp_fields_logged = True
|
||||
|
||||
# Log nozzle hardware info fields (once)
|
||||
nozzle_fields = {k: v for k, v in data.items() if 'nozzle' in k.lower() or 'hw' in k.lower() or 'extruder' in k.lower() or 'upgrade' in k.lower()}
|
||||
if nozzle_fields and not hasattr(self, '_nozzle_fields_logged'):
|
||||
logger.info(f"[{self.serial_number}] Nozzle/hardware fields in MQTT data: {nozzle_fields}")
|
||||
self._nozzle_fields_logged = True
|
||||
if "bed_temper" in data:
|
||||
temps["bed"] = float(data["bed_temper"])
|
||||
if "bed_target_temper" in data:
|
||||
@@ -356,6 +395,30 @@ class BambuMQTTClient:
|
||||
else:
|
||||
self.state.ipcam = ipcam_data is True
|
||||
|
||||
# Parse nozzle hardware info (single nozzle printers)
|
||||
if "nozzle_type" in data:
|
||||
self.state.nozzles[0].nozzle_type = str(data["nozzle_type"])
|
||||
if "nozzle_diameter" in data:
|
||||
self.state.nozzles[0].nozzle_diameter = str(data["nozzle_diameter"])
|
||||
|
||||
# Parse nozzle hardware info (dual nozzle printers - H2D series)
|
||||
# Left nozzle
|
||||
if "left_nozzle_type" in data:
|
||||
self.state.nozzles[0].nozzle_type = str(data["left_nozzle_type"])
|
||||
if "left_nozzle_diameter" in data:
|
||||
self.state.nozzles[0].nozzle_diameter = str(data["left_nozzle_diameter"])
|
||||
# Right nozzle
|
||||
if "right_nozzle_type" in data:
|
||||
self.state.nozzles[1].nozzle_type = str(data["right_nozzle_type"])
|
||||
if "right_nozzle_diameter" in data:
|
||||
self.state.nozzles[1].nozzle_diameter = str(data["right_nozzle_diameter"])
|
||||
|
||||
# Alternative format for dual nozzle (nozzle_type_2, etc.)
|
||||
if "nozzle_type_2" in data:
|
||||
self.state.nozzles[1].nozzle_type = str(data["nozzle_type_2"])
|
||||
if "nozzle_diameter_2" in data:
|
||||
self.state.nozzles[1].nozzle_diameter = str(data["nozzle_diameter_2"])
|
||||
|
||||
# Preserve AMS and vt_tray data when updating raw_data
|
||||
ams_data = self.state.raw_data.get("ams")
|
||||
vt_tray_data = self.state.raw_data.get("vt_tray")
|
||||
@@ -467,6 +530,36 @@ class BambuMQTTClient:
|
||||
message = {"pushing": {"command": "pushall"}}
|
||||
self._client.publish(self.topic_publish, json.dumps(message))
|
||||
|
||||
def request_status_update(self) -> bool:
|
||||
"""Request a full status update from the printer (public API).
|
||||
|
||||
Sends both pushall and get_accessories commands to refresh all data
|
||||
including nozzle hardware info.
|
||||
|
||||
Returns:
|
||||
True if the request was sent, False if not connected.
|
||||
"""
|
||||
if not self._client or not self.state.connected:
|
||||
return False
|
||||
self._request_push_all()
|
||||
# Note: get_accessories returns stale nozzle data on H2D.
|
||||
# The correct nozzle data comes from push_status response.
|
||||
return True
|
||||
|
||||
def _request_accessories(self):
|
||||
"""Request accessories info (nozzle type, etc.) from printer."""
|
||||
if self._client:
|
||||
self._sequence_id += 1
|
||||
message = {
|
||||
"system": {
|
||||
"sequence_id": str(self._sequence_id),
|
||||
"command": "get_accessories",
|
||||
"accessory_type": "none"
|
||||
}
|
||||
}
|
||||
logger.debug(f"[{self.serial_number}] Requesting accessories info")
|
||||
self._client.publish(self.topic_publish, json.dumps(message))
|
||||
|
||||
def _prime_kprofile_request(self):
|
||||
"""Send a priming K-profile request on connect.
|
||||
|
||||
|
||||
@@ -229,6 +229,15 @@ class PrinterManager:
|
||||
return self._clients[printer_id].logging_enabled
|
||||
return False
|
||||
|
||||
def request_status_update(self, printer_id: int) -> bool:
|
||||
"""Request a full status update from the printer.
|
||||
|
||||
This sends a 'pushall' command to get the latest data including nozzle info.
|
||||
"""
|
||||
if printer_id in self._clients:
|
||||
return self._clients[printer_id].request_status_update()
|
||||
return False
|
||||
|
||||
async def test_connection(
|
||||
self,
|
||||
ip_address: str,
|
||||
|
||||
@@ -56,6 +56,11 @@ export interface AMSUnit {
|
||||
tray: AMSTray[];
|
||||
}
|
||||
|
||||
export interface NozzleInfo {
|
||||
nozzle_type: string; // "stainless_steel" or "hardened_steel"
|
||||
nozzle_diameter: string; // e.g., "0.4"
|
||||
}
|
||||
|
||||
export interface PrinterStatus {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -83,6 +88,7 @@ export interface PrinterStatus {
|
||||
sdcard: boolean; // SD card inserted
|
||||
timelapse: boolean; // Timelapse recording active
|
||||
ipcam: boolean; // Live view enabled
|
||||
nozzles: NozzleInfo[]; // Nozzle hardware info (index 0=left/primary, 1=right)
|
||||
}
|
||||
|
||||
export interface PrinterCreate {
|
||||
@@ -1176,4 +1182,10 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ enable }),
|
||||
}),
|
||||
|
||||
// Request full status update from printer (pushall)
|
||||
refreshStatus: (printerId: number) =>
|
||||
request<ControlResponse>(`/printers/${printerId}/control/refresh`, {
|
||||
method: 'POST',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../../api/client';
|
||||
import type { Printer, PrinterStatus } from '../../api/client';
|
||||
import { X, RefreshCw, Loader2 } from 'lucide-react';
|
||||
import { Card, CardContent } from '../Card';
|
||||
|
||||
interface PrinterPartsModalProps {
|
||||
printer: Printer;
|
||||
status: PrinterStatus | null | undefined;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Convert API nozzle_type to display name
|
||||
// Bambu nozzle codes: SS = Stainless Steel, HS = Hardened Steel, HH = Hardened Steel High-flow
|
||||
function getNozzleTypeName(type: string): string {
|
||||
const upperType = type.toUpperCase();
|
||||
|
||||
// Handle Bambu nozzle codes (e.g., HH01, HS00, SS00)
|
||||
if (upperType.startsWith('HH')) {
|
||||
return 'Hardened Steel';
|
||||
}
|
||||
if (upperType.startsWith('HS')) {
|
||||
return 'Hardened Steel';
|
||||
}
|
||||
if (upperType.startsWith('SS')) {
|
||||
return 'Stainless Steel';
|
||||
}
|
||||
|
||||
// Handle full names from API
|
||||
switch (type) {
|
||||
case 'hardened_steel':
|
||||
return 'Hardened Steel';
|
||||
case 'stainless_steel':
|
||||
return 'Stainless Steel';
|
||||
default:
|
||||
return type || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Determine flow type based on nozzle type code
|
||||
// HH = High-flow, HS/SS = Standard
|
||||
function getFlowType(type: string): string {
|
||||
const upperType = type.toUpperCase();
|
||||
if (upperType.startsWith('HH')) {
|
||||
return 'High flow';
|
||||
}
|
||||
return 'Standard';
|
||||
}
|
||||
|
||||
export function PrinterPartsModal({ printer, status, onClose }: PrinterPartsModalProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const isDualNozzle = printer.nozzle_count > 1;
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
// Get nozzle data from status
|
||||
// On H2D, both nozzles must be identical - if right nozzle is empty, copy from left
|
||||
const leftNozzle = status?.nozzles?.[0];
|
||||
const rightNozzleRaw = status?.nozzles?.[1];
|
||||
const rightNozzle = (rightNozzleRaw?.nozzle_type || rightNozzleRaw?.nozzle_diameter)
|
||||
? rightNozzleRaw
|
||||
: leftNozzle;
|
||||
|
||||
// Refresh mutation - sends pushall command to printer
|
||||
const refreshMutation = useMutation({
|
||||
mutationFn: () => api.refreshStatus(printer.id),
|
||||
onSuccess: () => {
|
||||
// Invalidate queries to get updated data
|
||||
queryClient.invalidateQueries({ queryKey: ['printerStatuses'] });
|
||||
},
|
||||
});
|
||||
|
||||
const handleRefresh = () => {
|
||||
refreshMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Card className="w-full max-w-2xl" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<CardContent className="p-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-bambu-dark-tertiary">
|
||||
<span className="text-sm font-medium text-white">Printer Parts</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-bambu-dark-tertiary text-bambu-gray hover:text-white"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Left Nozzle */}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white mb-3">
|
||||
{isDualNozzle ? 'Left Nozzle' : 'Nozzle'}
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1.5">Type</label>
|
||||
<div className="px-3 py-2 bg-bambu-dark rounded border border-bambu-dark-tertiary text-sm text-bambu-gray">
|
||||
{getNozzleTypeName(leftNozzle?.nozzle_type || '')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1.5">Diameter</label>
|
||||
<div className="px-3 py-2 bg-bambu-dark rounded border border-bambu-dark-tertiary text-sm text-bambu-gray">
|
||||
{leftNozzle?.nozzle_diameter || '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1.5">Flow</label>
|
||||
<div className="px-3 py-2 bg-bambu-dark rounded border border-bambu-dark-tertiary text-sm text-bambu-gray">
|
||||
{getFlowType(leftNozzle?.nozzle_type || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Nozzle - only for dual nozzle printers */}
|
||||
{isDualNozzle && (
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-white mb-3">Right Nozzle</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1.5">Type</label>
|
||||
<div className="px-3 py-2 bg-bambu-dark rounded border border-bambu-dark-tertiary text-sm text-bambu-gray">
|
||||
{getNozzleTypeName(rightNozzle?.nozzle_type || '')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1.5">Diameter</label>
|
||||
<div className="px-3 py-2 bg-bambu-dark rounded border border-bambu-dark-tertiary text-sm text-bambu-gray">
|
||||
{rightNozzle?.nozzle_diameter || '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-bambu-gray mb-1.5">Flow</label>
|
||||
<div className="px-3 py-2 bg-bambu-dark rounded border border-bambu-dark-tertiary text-sm text-bambu-gray">
|
||||
{getFlowType(rightNozzle?.nozzle_type || '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info text */}
|
||||
<p className="text-sm text-bambu-gray">
|
||||
Please change the nozzle settings on the printer.{' '}
|
||||
<a
|
||||
href="https://wiki.bambulab.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bambu-green hover:underline"
|
||||
>
|
||||
View wiki
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex justify-end px-4 py-3 border-t border-bambu-dark-tertiary">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-bambu-green hover:bg-bambu-green-dark disabled:opacity-50 text-white text-sm font-medium rounded"
|
||||
>
|
||||
{refreshMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
)}
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,12 +11,14 @@ import { BedControls } from '../components/control/BedControls';
|
||||
import { ExtruderControls } from '../components/control/ExtruderControls';
|
||||
import { AMSSectionDual } from '../components/control/AMSSectionDual';
|
||||
import { CameraSettingsModal } from '../components/control/CameraSettingsModal';
|
||||
import { PrinterPartsModal } from '../components/control/PrinterPartsModal';
|
||||
import { Loader2, WifiOff, Video, Webcam, Settings } from 'lucide-react';
|
||||
|
||||
export function ControlPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedPrinterId, setSelectedPrinterId] = useState<number | null>(null);
|
||||
const [showCameraSettings, setShowCameraSettings] = useState(false);
|
||||
const [showPrinterParts, setShowPrinterParts] = useState(false);
|
||||
|
||||
// Fetch all printers
|
||||
const { data: printers, isLoading: loadingPrinters } = useQuery({
|
||||
@@ -170,7 +172,10 @@ export function ControlPage() {
|
||||
<div className="flex items-center justify-between px-3 py-2.5 border-b border-bambu-dark-tertiary min-h-[44px]">
|
||||
<span className="text-sm text-bambu-gray">Control</span>
|
||||
<div className="flex gap-2">
|
||||
<button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
|
||||
<button
|
||||
onClick={() => setShowPrinterParts(true)}
|
||||
className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark"
|
||||
>
|
||||
Printer Parts
|
||||
</button>
|
||||
<button className="px-4 py-1.5 text-xs rounded bg-bambu-green text-white hover:bg-bambu-green-dark">
|
||||
@@ -247,6 +252,15 @@ export function ControlPage() {
|
||||
onClose={() => setShowCameraSettings(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Printer Parts Modal */}
|
||||
{showPrinterParts && selectedPrinter && (
|
||||
<PrinterPartsModal
|
||||
printer={selectedPrinter}
|
||||
status={selectedStatus}
|
||||
onClose={() => setShowPrinterParts(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -7,7 +7,7 @@
|
||||
<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-CHgTUeZT.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DVDHG02m.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DaEsKk03.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user