Merge branch '0.1.8b' into feature/updated_plate_view

This commit is contained in:
MartinNYHC
2026-02-03 17:15:21 +01:00
committed by GitHub
120 changed files with 24410 additions and 3206 deletions
+220 -10
View File
@@ -771,6 +771,8 @@ export interface CloudLoginResponse {
success: boolean;
needs_verification: boolean;
message: string;
verification_type?: 'email' | 'totp' | null;
tfa_key?: string | null;
}
export interface SlicerSetting {
@@ -1069,6 +1071,7 @@ export interface PrintQueueItem {
id: number;
printer_id: number | null; // null = unassigned
target_model: string | null; // Target printer model for model-based assignment
target_location: string | null; // Target location filter for model-based assignment
required_filament_types: string[] | null; // Required filament types for model-based assignment
waiting_reason: string | null; // Why a model-based job hasn't started yet
// Either archive_id OR library_file_id must be set (archive created at print start)
@@ -1107,6 +1110,7 @@ export interface PrintQueueItem {
export interface PrintQueueItemCreate {
printer_id?: number | null; // null = unassigned
target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
target_location?: string | null; // Target location filter (only used with target_model)
// Either archive_id OR library_file_id must be provided
archive_id?: number | null;
library_file_id?: number | null;
@@ -1128,6 +1132,7 @@ export interface PrintQueueItemCreate {
export interface PrintQueueItemUpdate {
printer_id?: number | null; // null = unassign
target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
target_location?: string | null; // Target location filter (only used with target_model)
position?: number;
scheduled_time?: string | null;
require_previous_success?: boolean;
@@ -1603,6 +1608,10 @@ export interface UnlinkedSpool {
location: string | null;
}
export interface LinkedSpoolsMap {
linked: Record<string, number>; // tag (uppercase) -> spool_id
}
// Update types
export interface VersionInfo {
version: string;
@@ -2083,10 +2092,40 @@ export const api = {
}>(`/printers/${printerId}/files/plates?path=${encodeURIComponent(path)}`),
getPrinterFilePlateThumbnail: (printerId: number, plateIndex: number, path: string) =>
`${API_BASE}/printers/${printerId}/files/plate-thumbnail/${plateIndex}?path=${encodeURIComponent(path)}`,
downloadPrinterFile: async (printerId: number, path: string): Promise<void> => {
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(
`${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
{ headers }
);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
const disposition = response.headers.get('Content-Disposition');
const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
const filename = filenameMatch?.[1] || path.split('/').pop() || 'download';
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
downloadPrinterFilesAsZip: async (printerId: number, paths: string[]): Promise<Blob> => {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/printers/${printerId}/files/download-zip`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers,
body: JSON.stringify({ paths }),
});
if (!response.ok) {
@@ -2195,7 +2234,11 @@ export const api = {
if (options?.dateTo) params.set('date_to', options.dateTo);
if (options?.search) params.set('search', options.search);
const response = await fetch(`${API_BASE}/archives/export?${params}`);
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/archives/export?${params}`, { headers });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
@@ -2223,7 +2266,11 @@ export const api = {
if (options?.printerId) params.set('printer_id', String(options.printerId));
if (options?.projectId) params.set('project_id', String(options.projectId));
const response = await fetch(`${API_BASE}/archives/stats/export?${params}`);
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/archives/stats/export?${params}`, { headers });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
@@ -2249,6 +2296,29 @@ export const api = {
getArchivePlateThumbnail: (id: number, plateIndex: number) =>
`${API_BASE}/archives/${id}/plate-thumbnail/${plateIndex}`,
getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
downloadArchive: async (id: number, filename?: string): Promise<void> => {
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/archives/${id}/download`, { headers });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
const disposition = response.headers.get('Content-Disposition');
const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
const downloadFilename = filenameMatch?.[1] || filename || `archive_${id}.3mf`;
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = downloadFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
getArchiveGcode: (id: number) => `${API_BASE}/archives/${id}/gcode`,
getArchivePlatePreview: (id: number) => `${API_BASE}/archives/${id}/plate-preview`,
getArchiveTimelapse: (id: number) => `${API_BASE}/archives/${id}/timelapse?v=${Date.now()}`,
@@ -2367,6 +2437,29 @@ export const api = {
// Source 3MF (original slicer project file)
getSource3mfDownloadUrl: (archiveId: number) =>
`${API_BASE}/archives/${archiveId}/source`,
downloadSource3mf: async (archiveId: number): Promise<void> => {
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, { headers });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
const disposition = response.headers.get('Content-Disposition');
const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
const filename = filenameMatch?.[1] || `source_${archiveId}.3mf`;
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
getSource3mfForSlicer: (archiveId: number, filename: string) =>
`${API_BASE}/archives/${archiveId}/source/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
uploadSource3mf: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
@@ -2394,6 +2487,29 @@ export const api = {
// F3D (Fusion 360 design file)
getF3dDownloadUrl: (archiveId: number) =>
`${API_BASE}/archives/${archiveId}/f3d`,
downloadF3d: async (archiveId: number): Promise<void> => {
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/archives/${archiveId}/f3d`, { headers });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
const disposition = response.headers.get('Content-Disposition');
const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
const filename = filenameMatch?.[1] || `archive_${archiveId}.f3d`;
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
uploadF3d: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
const formData = new FormData();
formData.append('file', file);
@@ -2562,7 +2678,11 @@ export const api = {
exportBackup: async (): Promise<{ blob: Blob; filename: string }> => {
// New simplified backup - complete database + all files
const url = `${API_BASE}/settings/backup`;
const response = await fetch(url);
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(url, { headers });
// Check for errors
if (!response.ok) {
@@ -2610,10 +2730,10 @@ export const api = {
method: 'POST',
body: JSON.stringify({ email, password, region }),
}),
cloudVerify: (email: string, code: string) =>
cloudVerify: (email: string, code: string, tfaKey?: string) =>
request<CloudLoginResponse>('/cloud/verify', {
method: 'POST',
body: JSON.stringify({ email, code }),
body: JSON.stringify({ email, code, tfa_key: tfaKey }),
}),
cloudSetToken: (access_token: string) =>
request<CloudAuthStatus>('/cloud/token', {
@@ -2950,11 +3070,20 @@ export const api = {
request<{ filaments: unknown[] }>('/spoolman/filaments'),
getUnlinkedSpools: () =>
request<UnlinkedSpool[]>('/spoolman/spools/unlinked'),
getLinkedSpools: () =>
request<LinkedSpoolsMap>('/spoolman/spools/linked'),
linkSpool: (spoolId: number, trayUuid: string) =>
request<{ success: boolean; message: string }>(`/spoolman/spools/${spoolId}/link`, {
method: 'POST',
body: JSON.stringify({ tray_uuid: trayUuid }),
}),
getSpoolmanSettings: () =>
request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string }>('/settings/spoolman'),
updateSpoolmanSettings: (data: { spoolman_enabled?: string; spoolman_url?: string; spoolman_sync_mode?: string }) =>
request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string }>('/settings/spoolman', {
method: 'PUT',
body: JSON.stringify(data),
}),
// Updates
getVersion: () => request<VersionInfo>('/updates/version'),
@@ -3016,6 +3145,8 @@ export const api = {
`${API_BASE}/printers/${printerId}/camera/snapshot`,
testCameraConnection: (printerId: number) =>
request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
getCameraStatus: (printerId: number) =>
request<{ active: boolean; stalled: boolean }>(`/printers/${printerId}/camera/status`),
// Plate Detection - Multi-reference calibration (stores up to 5 references per printer)
checkPlateEmpty: (printerId: number, options?: { useExternal?: boolean; includeDebugImage?: boolean }) => {
@@ -3220,6 +3351,42 @@ export const api = {
method: 'POST',
body: JSON.stringify(data),
}),
importProjectFile: async (file: File): Promise<Project> => {
const formData = new FormData();
formData.append('file', file);
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/projects/import/file`, {
method: 'POST',
headers,
body: formData,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
return response.json();
},
exportProjectZip: async (projectId: number): Promise<{ blob: Blob; filename: string }> => {
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/projects/${projectId}/export`, {
headers,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
const contentDisposition = response.headers.get('Content-Disposition');
const filenameMatch = contentDisposition?.match(/filename="(.+)"/);
const filename = filenameMatch?.[1] || `project_${projectId}.zip`;
const blob = await response.blob();
return { blob, filename };
},
// API Keys
getAPIKeys: () => request<APIKey[]>('/api-keys/'),
@@ -3333,6 +3500,29 @@ export const api = {
deleteLibraryFile: (id: number) =>
request<{ status: string; message: string }>(`/library/files/${id}`, { method: 'DELETE' }),
getLibraryFileDownloadUrl: (id: number) => `${API_BASE}/library/files/${id}/download`,
downloadLibraryFile: async (id: number, filename?: string): Promise<void> => {
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/library/files/${id}/download`, { headers });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);
}
const disposition = response.headers.get('Content-Disposition');
const filenameMatch = disposition?.match(/filename="?([^";\n]+)"?/);
const downloadFilename = filenameMatch?.[1] || filename || `file_${id}`;
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = downloadFilename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
getLibraryFileThumbnailUrl: (id: number) => `${API_BASE}/library/files/${id}/thumbnail`,
getLibraryFilePlateThumbnail: (id: number, plateIndex: number) =>
`${API_BASE}/library/files/${id}/plate-thumbnail/${plateIndex}`,
@@ -3755,22 +3945,36 @@ export const discoveryApi = {
};
// Virtual Printer types
export type VirtualPrinterMode = 'immediate' | 'queue' | 'review' | 'print_queue' | 'proxy'; // 'queue' is legacy, normalized to 'review'
export interface VirtualPrinterProxyStatus {
running: boolean;
target_host: string;
ftp_port: number;
mqtt_port: number;
ftp_connections: number;
mqtt_connections: number;
}
export interface VirtualPrinterStatus {
enabled: boolean;
running: boolean;
mode: 'immediate' | 'queue' | 'review' | 'print_queue'; // 'queue' is legacy, normalized to 'review'
mode: VirtualPrinterMode;
name: string;
serial: string;
model: string;
model_name: string;
pending_files: number;
target_printer_ip?: string; // For proxy mode
proxy?: VirtualPrinterProxyStatus; // For proxy mode
}
export interface VirtualPrinterSettings {
enabled: boolean;
access_code_set: boolean;
mode: 'immediate' | 'queue' | 'review' | 'print_queue'; // 'queue' is legacy, normalized to 'review'
mode: VirtualPrinterMode;
model: string;
target_printer_id: number | null; // For proxy mode
status: VirtualPrinterStatus;
}
@@ -3800,14 +4004,16 @@ export const virtualPrinterApi = {
updateSettings: (data: {
enabled?: boolean;
access_code?: string;
mode?: 'immediate' | 'review' | 'print_queue';
mode?: 'immediate' | 'review' | 'print_queue' | 'proxy';
model?: string;
target_printer_id?: number;
}) => {
const params = new URLSearchParams();
if (data.enabled !== undefined) params.set('enabled', String(data.enabled));
if (data.access_code !== undefined) params.set('access_code', data.access_code);
if (data.mode !== undefined) params.set('mode', data.mode);
if (data.model !== undefined) params.set('model', data.model);
if (data.target_printer_id !== undefined) params.set('target_printer_id', String(data.target_printer_id));
return request<VirtualPrinterSettings>(`/settings/virtual-printer?${params.toString()}`, {
method: 'PUT',
@@ -3925,7 +4131,11 @@ export const supportApi = {
}),
downloadSupportBundle: async () => {
const response = await fetch(`${API_BASE}/support/bundle`);
const headers: Record<string, string> = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/support/bundle`, { headers });
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.detail || `HTTP ${response.status}`);