mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
Merge branch '0.1.8b' into feature/updated_plate_view
This commit is contained in:
+220
-10
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user