mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
Features:
- Add print options UI to reprint modal (bed leveling, flow calibration,
vibration calibration, first layer inspection, timelapse)
- Collapsible "Print Options" section with toggle switches
- Default values: bed leveling=on, vibration_cali=on, others=off
Fixes:
- Fix AMS mapping format to match Bambu Studio exactly
- Use slot_id from 3MF to position tray IDs correctly
- Fill unused slots with -1 placeholder
- Add ams_mapping2 with detailed {ams_id, slot_id} pairs
- Fix print command format for proper printer recognition
- Add auto_bed_leveling, cfg, extrude_cali_flag, nozzle_offset_cali
- Use American spelling "bed_leveling" (not British "bed_levelling")
- Match sequence_id format from official apps
- Prevent duplicate tray assignment in filament matching
This resolves the "stuck at filament change" issue on multi-color reprints
affecting X1C and other multi-AMS printer configurations.
2646 lines
83 KiB
TypeScript
2646 lines
83 KiB
TypeScript
const API_BASE = '/api/v1';
|
|
|
|
async function request<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
const detail = error.detail;
|
|
const message = typeof detail === 'string'
|
|
? detail
|
|
: (detail ? JSON.stringify(detail) : `HTTP ${response.status}`);
|
|
throw new Error(message);
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
// Printer types
|
|
export interface Printer {
|
|
id: number;
|
|
name: string;
|
|
serial_number: string;
|
|
ip_address: string;
|
|
access_code: string;
|
|
model: string | null;
|
|
location: string | null; // Group/location name
|
|
nozzle_count: number; // 1 or 2, auto-detected from MQTT
|
|
is_active: boolean;
|
|
auto_archive: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface HMSError {
|
|
code: string;
|
|
attr: number; // Attribute value for constructing wiki URL
|
|
module: number;
|
|
severity: number; // 1=fatal, 2=serious, 3=common, 4=info
|
|
}
|
|
|
|
export interface AMSTray {
|
|
id: number;
|
|
tray_color: string | null;
|
|
tray_type: string | null;
|
|
tray_sub_brands: string | null; // Full name like "PLA Basic", "PETG HF"
|
|
tray_id_name: string | null; // Bambu filament ID like "A00-Y2" (can decode to color)
|
|
tray_info_idx: string | null; // Filament preset ID like "GFA00" - maps to cloud setting_id
|
|
remain: number;
|
|
k: number | null; // Pressure advance value (from tray or K-profile lookup)
|
|
cali_idx: number | null; // Calibration index for K-profile lookup
|
|
tag_uid: string | null; // RFID tag UID (any tag)
|
|
tray_uuid: string | null; // Bambu Lab spool UUID (32-char hex, only valid for Bambu Lab spools)
|
|
nozzle_temp_min: number | null; // Min nozzle temperature
|
|
nozzle_temp_max: number | null; // Max nozzle temperature
|
|
}
|
|
|
|
export interface AMSUnit {
|
|
id: number;
|
|
humidity: number | null;
|
|
temp: number | null;
|
|
is_ams_ht: boolean; // True for AMS-HT (single spool), False for regular AMS (4 spools)
|
|
tray: AMSTray[];
|
|
}
|
|
|
|
export interface NozzleInfo {
|
|
nozzle_type: string; // "stainless_steel" or "hardened_steel"
|
|
nozzle_diameter: string; // e.g., "0.4"
|
|
}
|
|
|
|
export interface PrintOptions {
|
|
// Core AI detectors
|
|
spaghetti_detector: boolean;
|
|
print_halt: boolean;
|
|
halt_print_sensitivity: string; // "low", "medium", "high" - spaghetti sensitivity
|
|
first_layer_inspector: boolean;
|
|
printing_monitor: boolean;
|
|
buildplate_marker_detector: boolean;
|
|
allow_skip_parts: boolean;
|
|
// Additional AI detectors (decoded from cfg bitmask)
|
|
nozzle_clumping_detector: boolean;
|
|
nozzle_clumping_sensitivity: string; // "low", "medium", "high"
|
|
pileup_detector: boolean;
|
|
pileup_sensitivity: string; // "low", "medium", "high"
|
|
airprint_detector: boolean;
|
|
airprint_sensitivity: string; // "low", "medium", "high"
|
|
auto_recovery_step_loss: boolean;
|
|
filament_tangle_detect: boolean;
|
|
}
|
|
|
|
export interface PrinterStatus {
|
|
id: number;
|
|
name: string;
|
|
connected: boolean;
|
|
state: string | null;
|
|
current_print: string | null;
|
|
subtask_name: string | null;
|
|
gcode_file: string | null;
|
|
progress: number | null;
|
|
remaining_time: number | null;
|
|
layer_num: number | null;
|
|
total_layers: number | null;
|
|
temperatures: {
|
|
bed?: number;
|
|
bed_target?: number;
|
|
bed_heating?: boolean; // Actual heater state from MQTT
|
|
nozzle?: number;
|
|
nozzle_target?: number;
|
|
nozzle_heating?: boolean; // Actual heater state from MQTT
|
|
nozzle_2?: number; // Second nozzle for H2 series (dual nozzle)
|
|
nozzle_2_target?: number;
|
|
nozzle_2_heating?: boolean; // Actual heater state from MQTT
|
|
chamber?: number;
|
|
chamber_target?: number;
|
|
chamber_heating?: boolean; // Actual heater state from MQTT
|
|
} | null;
|
|
cover_url: string | null;
|
|
hms_errors: HMSError[];
|
|
ams: AMSUnit[];
|
|
ams_exists: boolean;
|
|
vt_tray: AMSTray | null; // Virtual tray / external spool
|
|
sdcard: boolean; // SD card inserted
|
|
store_to_sdcard: boolean; // Store sent files on SD card
|
|
timelapse: boolean; // Timelapse recording active
|
|
ipcam: boolean; // Live view enabled
|
|
wifi_signal: number | null; // WiFi signal strength in dBm
|
|
nozzles: NozzleInfo[]; // Nozzle hardware info (index 0=left/primary, 1=right)
|
|
print_options: PrintOptions | null; // AI detection and print options
|
|
// Calibration stage tracking
|
|
stg_cur: number; // Current stage number (-1 = not calibrating)
|
|
stg_cur_name: string | null; // Human-readable current stage name
|
|
stg: number[]; // List of stage numbers in calibration sequence
|
|
// Air conditioning mode (0=cooling, 1=heating)
|
|
airduct_mode: number;
|
|
// Print speed level (1=silent, 2=standard, 3=sport, 4=ludicrous)
|
|
speed_level: number;
|
|
// Chamber light on/off
|
|
chamber_light: boolean;
|
|
// Active extruder for dual nozzle (0=right, 1=left)
|
|
active_extruder: number;
|
|
// AMS mapping - which AMS is connected to which nozzle
|
|
// Format: [ams_id_for_nozzle0, ams_id_for_nozzle1, ...] where -1 means no AMS
|
|
ams_mapping: number[];
|
|
// Per-AMS extruder mapping - extracted from each AMS unit's info field
|
|
// Format: {ams_id: extruder_id} where extruder 0=right, 1=left
|
|
// Note: JSON keys are always strings
|
|
ams_extruder_map: Record<string, number>;
|
|
// Currently loaded tray (global tray ID, 255 = no filament loaded, 254 = external spool)
|
|
tray_now: number;
|
|
// AMS status for filament change tracking (0=idle, 1=filament_change, 2=rfid_identifying, 3=assist, 4=calibration)
|
|
ams_status_main: number;
|
|
// AMS sub-status for filament change step (when main=1): 4=retraction, 6=load verification, 7=purge
|
|
ams_status_sub: number;
|
|
// mc_print_sub_stage - filament change step indicator used by OrcaSlicer/BambuStudio
|
|
mc_print_sub_stage: number;
|
|
// Timestamp of last AMS data update (for RFID refresh detection)
|
|
last_ams_update: number;
|
|
// Number of printable objects in current print (for skip objects feature)
|
|
printable_objects_count: number;
|
|
// Fan speeds (0-100 percentage, null if not available for this model)
|
|
cooling_fan_speed: number | null; // Part cooling fan
|
|
big_fan1_speed: number | null; // Auxiliary fan
|
|
big_fan2_speed: number | null; // Chamber/exhaust fan
|
|
heatbreak_fan_speed: number | null; // Hotend heatbreak fan
|
|
}
|
|
|
|
export interface PrinterCreate {
|
|
name: string;
|
|
serial_number: string;
|
|
ip_address: string;
|
|
access_code: string;
|
|
model?: string;
|
|
location?: string;
|
|
auto_archive?: boolean;
|
|
}
|
|
|
|
// Archive types
|
|
export interface ArchiveDuplicate {
|
|
id: number;
|
|
print_name: string | null;
|
|
created_at: string;
|
|
match_type: 'exact' | 'similar'; // 'exact' = hash match, 'similar' = name match
|
|
}
|
|
|
|
export interface Archive {
|
|
id: number;
|
|
printer_id: number | null;
|
|
project_id: number | null;
|
|
project_name: string | null;
|
|
filename: string;
|
|
file_path: string;
|
|
file_size: number;
|
|
content_hash: string | null;
|
|
thumbnail_path: string | null;
|
|
timelapse_path: string | null;
|
|
source_3mf_path: string | null;
|
|
duplicates: ArchiveDuplicate[] | null;
|
|
duplicate_count: number;
|
|
print_name: string | null;
|
|
print_time_seconds: number | null;
|
|
actual_time_seconds: number | null; // Computed from started_at/completed_at
|
|
time_accuracy: number | null; // Percentage: 100 = perfect, >100 = faster than estimated
|
|
filament_used_grams: number | null;
|
|
filament_type: string | null;
|
|
filament_color: string | null;
|
|
layer_height: number | null;
|
|
total_layers: number | null;
|
|
nozzle_diameter: number | null;
|
|
bed_temperature: number | null;
|
|
nozzle_temperature: number | null;
|
|
status: string;
|
|
started_at: string | null;
|
|
completed_at: string | null;
|
|
extra_data: Record<string, unknown> | null;
|
|
makerworld_url: string | null;
|
|
designer: string | null;
|
|
is_favorite: boolean;
|
|
tags: string | null;
|
|
notes: string | null;
|
|
cost: number | null;
|
|
photos: string[] | null;
|
|
failure_reason: string | null;
|
|
quantity: number;
|
|
energy_kwh: number | null;
|
|
energy_cost: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ArchiveStats {
|
|
total_prints: number;
|
|
successful_prints: number;
|
|
failed_prints: number;
|
|
total_print_time_hours: number;
|
|
total_filament_grams: number;
|
|
total_cost: number;
|
|
prints_by_filament_type: Record<string, number>;
|
|
prints_by_printer: Record<string, number>;
|
|
average_time_accuracy: number | null;
|
|
time_accuracy_by_printer: Record<string, number> | null;
|
|
total_energy_kwh: number;
|
|
total_energy_cost: number;
|
|
}
|
|
|
|
export interface FailureAnalysis {
|
|
period_days: number;
|
|
total_prints: number;
|
|
failed_prints: number;
|
|
failure_rate: number;
|
|
failures_by_reason: Record<string, number>;
|
|
failures_by_filament: Record<string, number>;
|
|
failures_by_printer: Record<string, number>;
|
|
failures_by_hour: Record<number, number>;
|
|
recent_failures: Array<{
|
|
id: number;
|
|
print_name: string;
|
|
failure_reason: string | null;
|
|
filament_type: string | null;
|
|
printer_id: number | null;
|
|
created_at: string | null;
|
|
}>;
|
|
trend: Array<{
|
|
week_start: string;
|
|
total_prints: number;
|
|
failed_prints: number;
|
|
failure_rate: number;
|
|
}>;
|
|
}
|
|
|
|
export interface BulkUploadResult {
|
|
uploaded: number;
|
|
failed: number;
|
|
results: Array<{ filename: string; id: number; status: string }>;
|
|
errors: Array<{ filename: string; error: string }>;
|
|
}
|
|
|
|
// Archive Comparison types
|
|
export interface ComparisonArchiveInfo {
|
|
id: number;
|
|
print_name: string;
|
|
status: string;
|
|
created_at: string | null;
|
|
printer_id: number | null;
|
|
project_name: string | null;
|
|
}
|
|
|
|
export interface ComparisonField {
|
|
field: string;
|
|
label: string;
|
|
unit: string | null;
|
|
values: (string | number | null)[];
|
|
raw_values: (string | number | null)[];
|
|
has_difference: boolean;
|
|
}
|
|
|
|
export interface SuccessCorrelationInsight {
|
|
field: string;
|
|
label: string;
|
|
insight: string;
|
|
success_avg?: number;
|
|
failed_avg?: number;
|
|
success_values?: string[];
|
|
failed_values?: string[];
|
|
}
|
|
|
|
export interface SuccessCorrelation {
|
|
has_both_outcomes: boolean;
|
|
message?: string;
|
|
successful_count?: number;
|
|
failed_count?: number;
|
|
insights?: SuccessCorrelationInsight[];
|
|
}
|
|
|
|
export interface ArchiveComparison {
|
|
archives: ComparisonArchiveInfo[];
|
|
comparison: ComparisonField[];
|
|
differences: ComparisonField[];
|
|
success_correlation: SuccessCorrelation;
|
|
}
|
|
|
|
export interface SimilarArchive {
|
|
archive: {
|
|
id: number;
|
|
print_name: string;
|
|
status: string;
|
|
created_at: string | null;
|
|
};
|
|
match_reason: string;
|
|
match_score: number;
|
|
}
|
|
|
|
// Project types
|
|
export interface ProjectStats {
|
|
total_archives: number;
|
|
total_items: number; // Sum of quantities (total items printed)
|
|
completed_prints: number;
|
|
failed_prints: number;
|
|
queued_prints: number;
|
|
in_progress_prints: number;
|
|
total_print_time_hours: number;
|
|
total_filament_grams: number;
|
|
progress_percent: number | null;
|
|
estimated_cost: number;
|
|
total_energy_kwh: number;
|
|
total_energy_cost: number;
|
|
remaining_prints: number | null;
|
|
bom_total_items: number;
|
|
bom_completed_items: number;
|
|
}
|
|
|
|
export interface ProjectChildPreview {
|
|
id: number;
|
|
name: string;
|
|
color: string | null;
|
|
status: string;
|
|
progress_percent: number | null;
|
|
}
|
|
|
|
export interface Project {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
color: string | null;
|
|
status: string; // active, completed, archived
|
|
target_count: number | null;
|
|
notes: string | null;
|
|
attachments: ProjectAttachment[] | null;
|
|
tags: string | null;
|
|
due_date: string | null;
|
|
priority: string; // low, normal, high, urgent
|
|
budget: number | null;
|
|
is_template: boolean;
|
|
template_source_id: number | null;
|
|
parent_id: number | null;
|
|
parent_name: string | null;
|
|
children: ProjectChildPreview[];
|
|
created_at: string;
|
|
updated_at: string;
|
|
stats?: ProjectStats;
|
|
}
|
|
|
|
export interface ProjectAttachment {
|
|
filename: string;
|
|
original_name: string;
|
|
size: number;
|
|
uploaded_at: string;
|
|
}
|
|
|
|
export interface ArchivePreview {
|
|
id: number;
|
|
print_name: string | null;
|
|
thumbnail_path: string | null;
|
|
status: string;
|
|
filament_type: string | null;
|
|
filament_color: string | null;
|
|
}
|
|
|
|
export interface ProjectListItem {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
color: string | null;
|
|
status: string;
|
|
target_count: number | null;
|
|
created_at: string;
|
|
archive_count: number; // Number of print jobs
|
|
total_items: number; // Sum of quantities (total items printed)
|
|
queue_count: number;
|
|
progress_percent: number | null;
|
|
archives: ArchivePreview[];
|
|
}
|
|
|
|
export interface ProjectCreate {
|
|
name: string;
|
|
description?: string;
|
|
color?: string;
|
|
target_count?: number;
|
|
notes?: string;
|
|
tags?: string;
|
|
due_date?: string;
|
|
priority?: string;
|
|
budget?: number;
|
|
parent_id?: number;
|
|
}
|
|
|
|
export interface ProjectUpdate {
|
|
name?: string;
|
|
description?: string;
|
|
color?: string;
|
|
status?: string;
|
|
target_count?: number;
|
|
notes?: string;
|
|
tags?: string;
|
|
due_date?: string;
|
|
priority?: string;
|
|
budget?: number;
|
|
parent_id?: number;
|
|
}
|
|
|
|
// BOM Types - Tracks sourced/purchased parts (hardware, electronics, etc.)
|
|
export interface BOMItem {
|
|
id: number;
|
|
project_id: number;
|
|
name: string;
|
|
quantity_needed: number;
|
|
quantity_acquired: number;
|
|
unit_price: number | null;
|
|
sourcing_url: string | null;
|
|
archive_id: number | null;
|
|
archive_name: string | null;
|
|
stl_filename: string | null;
|
|
remarks: string | null;
|
|
sort_order: number;
|
|
is_complete: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface BOMItemCreate {
|
|
name: string;
|
|
quantity_needed?: number;
|
|
unit_price?: number;
|
|
sourcing_url?: string;
|
|
archive_id?: number;
|
|
stl_filename?: string;
|
|
remarks?: string;
|
|
}
|
|
|
|
export interface BOMItemUpdate {
|
|
name?: string;
|
|
quantity_needed?: number;
|
|
quantity_acquired?: number;
|
|
unit_price?: number;
|
|
sourcing_url?: string;
|
|
archive_id?: number;
|
|
stl_filename?: string;
|
|
remarks?: string;
|
|
}
|
|
|
|
// Timeline Types
|
|
export interface TimelineEvent {
|
|
event_type: string;
|
|
timestamp: string;
|
|
title: string;
|
|
description: string | null;
|
|
metadata: Record<string, unknown> | null;
|
|
}
|
|
|
|
// API Key types
|
|
export interface APIKey {
|
|
id: number;
|
|
name: string;
|
|
key_prefix: string;
|
|
can_queue: boolean;
|
|
can_control_printer: boolean;
|
|
can_read_status: boolean;
|
|
printer_ids: number[] | null;
|
|
enabled: boolean;
|
|
last_used: string | null;
|
|
created_at: string;
|
|
expires_at: string | null;
|
|
}
|
|
|
|
export interface APIKeyCreate {
|
|
name: string;
|
|
can_queue?: boolean;
|
|
can_control_printer?: boolean;
|
|
can_read_status?: boolean;
|
|
printer_ids?: number[] | null;
|
|
expires_at?: string | null;
|
|
}
|
|
|
|
export interface APIKeyCreateResponse extends APIKey {
|
|
key: string; // Full key, only shown on creation
|
|
}
|
|
|
|
export interface APIKeyUpdate {
|
|
name?: string;
|
|
can_queue?: boolean;
|
|
can_control_printer?: boolean;
|
|
can_read_status?: boolean;
|
|
printer_ids?: number[] | null;
|
|
enabled?: boolean;
|
|
expires_at?: string | null;
|
|
}
|
|
|
|
// Settings types
|
|
export interface AppSettings {
|
|
auto_archive: boolean;
|
|
save_thumbnails: boolean;
|
|
capture_finish_photo: boolean;
|
|
default_filament_cost: number;
|
|
currency: string;
|
|
energy_cost_per_kwh: number;
|
|
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
|
|
ams_history_retention_days: number; // days to keep AMS sensor history
|
|
// Date/time format settings
|
|
date_format: 'system' | 'us' | 'eu' | 'iso';
|
|
time_format: 'system' | '12h' | '24h';
|
|
// Default printer
|
|
default_printer_id: number | null;
|
|
// Telemetry
|
|
telemetry_enabled: boolean;
|
|
// Dark mode theme settings
|
|
dark_style: 'classic' | 'glow' | 'vibrant';
|
|
dark_background: 'neutral' | 'warm' | 'cool' | 'oled' | 'slate' | 'forest';
|
|
dark_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red';
|
|
// Light mode theme settings
|
|
light_style: 'classic' | 'glow' | 'vibrant';
|
|
light_background: 'neutral' | 'warm' | 'cool';
|
|
light_accent: 'green' | 'teal' | 'blue' | 'orange' | 'purple' | 'red';
|
|
}
|
|
|
|
export type AppSettingsUpdate = Partial<AppSettings>;
|
|
|
|
// Cloud types
|
|
export interface CloudAuthStatus {
|
|
is_authenticated: boolean;
|
|
email: string | null;
|
|
}
|
|
|
|
export interface CloudLoginResponse {
|
|
success: boolean;
|
|
needs_verification: boolean;
|
|
message: string;
|
|
}
|
|
|
|
export interface SlicerSetting {
|
|
setting_id: string;
|
|
name: string;
|
|
type: string;
|
|
version: string | null;
|
|
user_id: string | null;
|
|
updated_time: string | null;
|
|
}
|
|
|
|
export interface SlicerSettingsResponse {
|
|
filament: SlicerSetting[];
|
|
printer: SlicerSetting[];
|
|
process: SlicerSetting[];
|
|
}
|
|
|
|
export interface SlicerSettingDetail {
|
|
message?: string | null;
|
|
code?: string | null;
|
|
error?: string | null;
|
|
public: boolean;
|
|
version?: string | null;
|
|
type: string;
|
|
name: string;
|
|
update_time?: string | null;
|
|
nickname?: string | null;
|
|
base_id?: string | null;
|
|
setting: Record<string, unknown>;
|
|
filament_id?: string | null;
|
|
setting_id?: string | null;
|
|
}
|
|
|
|
export interface SlicerSettingCreate {
|
|
type: string; // 'filament', 'print', or 'printer'
|
|
name: string;
|
|
base_id: string;
|
|
setting: Record<string, unknown>;
|
|
}
|
|
|
|
export interface SlicerSettingUpdate {
|
|
name?: string;
|
|
setting?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface SlicerSettingDeleteResponse {
|
|
success: boolean;
|
|
message: string;
|
|
}
|
|
|
|
export interface FieldOption {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface FieldDefinition {
|
|
key: string;
|
|
label: string;
|
|
type: 'text' | 'number' | 'boolean' | 'select';
|
|
category: string;
|
|
description?: string;
|
|
options?: FieldOption[];
|
|
unit?: string;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
}
|
|
|
|
export interface FieldDefinitionsResponse {
|
|
version: string;
|
|
description: string;
|
|
fields: FieldDefinition[];
|
|
}
|
|
|
|
export interface CloudDevice {
|
|
dev_id: string;
|
|
name: string;
|
|
dev_model_name: string | null;
|
|
dev_product_name: string | null;
|
|
online: boolean;
|
|
}
|
|
|
|
// Smart Plug types
|
|
export interface SmartPlug {
|
|
id: number;
|
|
name: string;
|
|
ip_address: string;
|
|
printer_id: number | null;
|
|
enabled: boolean;
|
|
auto_on: boolean;
|
|
auto_off: boolean;
|
|
off_delay_mode: 'time' | 'temperature';
|
|
off_delay_minutes: number;
|
|
off_temp_threshold: number;
|
|
username: string | null;
|
|
password: string | null;
|
|
// Power alerts
|
|
power_alert_enabled: boolean;
|
|
power_alert_high: number | null;
|
|
power_alert_low: number | null;
|
|
power_alert_last_triggered: string | null;
|
|
// Schedule
|
|
schedule_enabled: boolean;
|
|
schedule_on_time: string | null;
|
|
schedule_off_time: string | null;
|
|
// Switchbar visibility
|
|
show_in_switchbar: boolean;
|
|
// Status
|
|
last_state: string | null;
|
|
last_checked: string | null;
|
|
auto_off_executed: boolean; // True when auto-off was triggered after print
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface SmartPlugCreate {
|
|
name: string;
|
|
ip_address: string;
|
|
printer_id?: number | null;
|
|
enabled?: boolean;
|
|
auto_on?: boolean;
|
|
auto_off?: boolean;
|
|
off_delay_mode?: 'time' | 'temperature';
|
|
off_delay_minutes?: number;
|
|
off_temp_threshold?: number;
|
|
username?: string | null;
|
|
password?: string | null;
|
|
// Power alerts
|
|
power_alert_enabled?: boolean;
|
|
power_alert_high?: number | null;
|
|
power_alert_low?: number | null;
|
|
// Schedule
|
|
schedule_enabled?: boolean;
|
|
schedule_on_time?: string | null;
|
|
schedule_off_time?: string | null;
|
|
// Switchbar visibility
|
|
show_in_switchbar?: boolean;
|
|
}
|
|
|
|
export interface SmartPlugUpdate {
|
|
name?: string;
|
|
ip_address?: string;
|
|
printer_id?: number | null;
|
|
enabled?: boolean;
|
|
auto_on?: boolean;
|
|
auto_off?: boolean;
|
|
off_delay_mode?: 'time' | 'temperature';
|
|
off_delay_minutes?: number;
|
|
off_temp_threshold?: number;
|
|
username?: string | null;
|
|
password?: string | null;
|
|
// Power alerts
|
|
power_alert_enabled?: boolean;
|
|
power_alert_high?: number | null;
|
|
power_alert_low?: number | null;
|
|
// Schedule
|
|
schedule_enabled?: boolean;
|
|
schedule_on_time?: string | null;
|
|
schedule_off_time?: string | null;
|
|
// Switchbar visibility
|
|
show_in_switchbar?: boolean;
|
|
}
|
|
|
|
export interface SmartPlugEnergy {
|
|
power: number | null; // Current watts
|
|
voltage: number | null; // Volts
|
|
current: number | null; // Amps
|
|
today: number | null; // kWh used today
|
|
yesterday: number | null; // kWh used yesterday
|
|
total: number | null; // Total kWh
|
|
factor: number | null; // Power factor (0-1)
|
|
apparent_power: number | null; // VA
|
|
reactive_power: number | null; // VAr
|
|
}
|
|
|
|
export interface SmartPlugStatus {
|
|
state: string | null;
|
|
reachable: boolean;
|
|
device_name: string | null;
|
|
energy: SmartPlugEnergy | null;
|
|
}
|
|
|
|
export interface SmartPlugTestResult {
|
|
success: boolean;
|
|
state: string | null;
|
|
device_name: string | null;
|
|
}
|
|
|
|
// Tasmota Discovery types
|
|
export interface TasmotaScanStatus {
|
|
running: boolean;
|
|
scanned: number;
|
|
total: number;
|
|
}
|
|
|
|
export interface DiscoveredTasmotaDevice {
|
|
ip_address: string;
|
|
name: string;
|
|
module: number | null;
|
|
state: string | null;
|
|
discovered_at: string | null;
|
|
}
|
|
|
|
// Print Queue types
|
|
export interface PrintQueueItem {
|
|
id: number;
|
|
printer_id: number;
|
|
archive_id: number;
|
|
position: number;
|
|
scheduled_time: string | null;
|
|
require_previous_success: boolean;
|
|
auto_off_after: boolean;
|
|
manual_start: boolean; // Requires manual trigger to start (staged)
|
|
status: 'pending' | 'printing' | 'completed' | 'failed' | 'skipped' | 'cancelled';
|
|
started_at: string | null;
|
|
completed_at: string | null;
|
|
error_message: string | null;
|
|
created_at: string;
|
|
archive_name?: string | null;
|
|
archive_thumbnail?: string | null;
|
|
printer_name?: string | null;
|
|
print_time_seconds?: number | null; // Estimated print time from archive
|
|
}
|
|
|
|
export interface PrintQueueItemCreate {
|
|
printer_id: number;
|
|
archive_id: number;
|
|
scheduled_time?: string | null;
|
|
require_previous_success?: boolean;
|
|
auto_off_after?: boolean;
|
|
manual_start?: boolean; // Requires manual trigger to start (staged)
|
|
}
|
|
|
|
export interface PrintQueueItemUpdate {
|
|
printer_id?: number;
|
|
position?: number;
|
|
scheduled_time?: string | null;
|
|
require_previous_success?: boolean;
|
|
auto_off_after?: boolean;
|
|
manual_start?: boolean;
|
|
}
|
|
|
|
// MQTT Logging types
|
|
export interface MQTTLogEntry {
|
|
timestamp: string;
|
|
topic: string;
|
|
direction: 'in' | 'out';
|
|
payload: Record<string, unknown>;
|
|
}
|
|
|
|
export interface MQTTLogsResponse {
|
|
logging_enabled: boolean;
|
|
logs: MQTTLogEntry[];
|
|
}
|
|
|
|
// K-Profile types
|
|
export interface KProfile {
|
|
slot_id: number;
|
|
extruder_id: number;
|
|
nozzle_id: string;
|
|
nozzle_diameter: string;
|
|
filament_id: string;
|
|
name: string;
|
|
k_value: string;
|
|
n_coef: string;
|
|
ams_id: number;
|
|
tray_id: number;
|
|
setting_id: string | null;
|
|
}
|
|
|
|
export interface KProfileCreate {
|
|
slot_id?: number; // Storage slot, 0 for new profiles
|
|
extruder_id?: number;
|
|
nozzle_id: string;
|
|
nozzle_diameter: string;
|
|
filament_id: string;
|
|
name: string;
|
|
k_value: string;
|
|
n_coef?: string;
|
|
ams_id?: number;
|
|
tray_id?: number;
|
|
setting_id?: string | null;
|
|
}
|
|
|
|
export interface KProfileDelete {
|
|
slot_id: number; // cali_idx - calibration index to delete
|
|
extruder_id: number;
|
|
nozzle_id: string; // e.g., "HH00-0.4"
|
|
nozzle_diameter: string; // e.g., "0.4"
|
|
filament_id: string; // Bambu filament identifier
|
|
setting_id?: string | null; // Setting ID (for X1C series)
|
|
}
|
|
|
|
export interface KProfilesResponse {
|
|
profiles: KProfile[];
|
|
nozzle_diameter: string;
|
|
}
|
|
|
|
export interface KProfileNote {
|
|
setting_id: string;
|
|
note: string;
|
|
}
|
|
|
|
export interface KProfileNotesResponse {
|
|
notes: Record<string, string>; // setting_id -> note
|
|
}
|
|
|
|
// Slot Preset Mapping
|
|
export interface SlotPresetMapping {
|
|
ams_id: number;
|
|
tray_id: number;
|
|
preset_id: string;
|
|
preset_name: string;
|
|
}
|
|
|
|
// Filament types
|
|
export interface Filament {
|
|
id: number;
|
|
name: string;
|
|
type: string; // PLA, PETG, ABS, etc.
|
|
brand: string | null;
|
|
color: string | null;
|
|
color_hex: string | null;
|
|
cost_per_kg: number;
|
|
spool_weight_g: number;
|
|
currency: string;
|
|
density: number | null;
|
|
print_temp_min: number | null;
|
|
print_temp_max: number | null;
|
|
bed_temp_min: number | null;
|
|
bed_temp_max: number | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
// Notification Provider types
|
|
export type ProviderType = 'callmebot' | 'ntfy' | 'pushover' | 'telegram' | 'email' | 'discord' | 'webhook';
|
|
|
|
export interface NotificationProvider {
|
|
id: number;
|
|
name: string;
|
|
provider_type: ProviderType;
|
|
enabled: boolean;
|
|
config: Record<string, unknown>;
|
|
// Print lifecycle events
|
|
on_print_start: boolean;
|
|
on_print_complete: boolean;
|
|
on_print_failed: boolean;
|
|
on_print_stopped: boolean;
|
|
on_print_progress: boolean;
|
|
// Printer status events
|
|
on_printer_offline: boolean;
|
|
on_printer_error: boolean;
|
|
on_filament_low: boolean;
|
|
on_maintenance_due: boolean;
|
|
// AMS environmental alarms (regular AMS)
|
|
on_ams_humidity_high: boolean;
|
|
on_ams_temperature_high: boolean;
|
|
// AMS-HT environmental alarms
|
|
on_ams_ht_humidity_high: boolean;
|
|
on_ams_ht_temperature_high: boolean;
|
|
// Quiet hours
|
|
quiet_hours_enabled: boolean;
|
|
quiet_hours_start: string | null;
|
|
quiet_hours_end: string | null;
|
|
// Daily digest
|
|
daily_digest_enabled: boolean;
|
|
daily_digest_time: string | null;
|
|
// Printer filter
|
|
printer_id: number | null;
|
|
// Status tracking
|
|
last_success: string | null;
|
|
last_error: string | null;
|
|
last_error_at: string | null;
|
|
// Timestamps
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface NotificationProviderCreate {
|
|
name: string;
|
|
provider_type: ProviderType;
|
|
enabled?: boolean;
|
|
config: Record<string, unknown>;
|
|
// Print lifecycle events
|
|
on_print_start?: boolean;
|
|
on_print_complete?: boolean;
|
|
on_print_failed?: boolean;
|
|
on_print_stopped?: boolean;
|
|
on_print_progress?: boolean;
|
|
// Printer status events
|
|
on_printer_offline?: boolean;
|
|
on_printer_error?: boolean;
|
|
on_filament_low?: boolean;
|
|
on_maintenance_due?: boolean;
|
|
// AMS environmental alarms (regular AMS)
|
|
on_ams_humidity_high?: boolean;
|
|
on_ams_temperature_high?: boolean;
|
|
// AMS-HT environmental alarms
|
|
on_ams_ht_humidity_high?: boolean;
|
|
on_ams_ht_temperature_high?: boolean;
|
|
// Quiet hours
|
|
quiet_hours_enabled?: boolean;
|
|
quiet_hours_start?: string | null;
|
|
quiet_hours_end?: string | null;
|
|
// Daily digest
|
|
daily_digest_enabled?: boolean;
|
|
daily_digest_time?: string | null;
|
|
// Printer filter
|
|
printer_id?: number | null;
|
|
}
|
|
|
|
export interface NotificationProviderUpdate {
|
|
name?: string;
|
|
provider_type?: ProviderType;
|
|
enabled?: boolean;
|
|
config?: Record<string, unknown>;
|
|
// Print lifecycle events
|
|
on_print_start?: boolean;
|
|
on_print_complete?: boolean;
|
|
on_print_failed?: boolean;
|
|
on_print_stopped?: boolean;
|
|
on_print_progress?: boolean;
|
|
// Printer status events
|
|
on_printer_offline?: boolean;
|
|
on_printer_error?: boolean;
|
|
on_filament_low?: boolean;
|
|
on_maintenance_due?: boolean;
|
|
// AMS environmental alarms (regular AMS)
|
|
on_ams_humidity_high?: boolean;
|
|
on_ams_temperature_high?: boolean;
|
|
// AMS-HT environmental alarms
|
|
on_ams_ht_humidity_high?: boolean;
|
|
on_ams_ht_temperature_high?: boolean;
|
|
// Quiet hours
|
|
quiet_hours_enabled?: boolean;
|
|
quiet_hours_start?: string | null;
|
|
quiet_hours_end?: string | null;
|
|
// Daily digest
|
|
daily_digest_enabled?: boolean;
|
|
daily_digest_time?: string | null;
|
|
// Printer filter
|
|
printer_id?: number | null;
|
|
}
|
|
|
|
export interface NotificationTestRequest {
|
|
provider_type: ProviderType;
|
|
config: Record<string, unknown>;
|
|
}
|
|
|
|
export interface NotificationTestResponse {
|
|
success: boolean;
|
|
message: string;
|
|
}
|
|
|
|
// Provider-specific config types for reference
|
|
export interface CallMeBotConfig {
|
|
phone: string;
|
|
apikey: string;
|
|
}
|
|
|
|
export interface NtfyConfig {
|
|
server?: string;
|
|
topic: string;
|
|
auth_token?: string | null;
|
|
}
|
|
|
|
export interface PushoverConfig {
|
|
user_key: string;
|
|
app_token: string;
|
|
priority?: number;
|
|
}
|
|
|
|
export interface TelegramConfig {
|
|
bot_token: string;
|
|
chat_id: string;
|
|
}
|
|
|
|
export interface EmailConfig {
|
|
smtp_server: string;
|
|
smtp_port?: number;
|
|
username: string;
|
|
password: string;
|
|
from_email: string;
|
|
to_email: string;
|
|
use_tls?: boolean;
|
|
}
|
|
|
|
// Notification Template types
|
|
export interface NotificationTemplate {
|
|
id: number;
|
|
event_type: string;
|
|
name: string;
|
|
title_template: string;
|
|
body_template: string;
|
|
is_default: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface NotificationTemplateUpdate {
|
|
title_template?: string;
|
|
body_template?: string;
|
|
}
|
|
|
|
export interface EventVariablesResponse {
|
|
event_type: string;
|
|
event_name: string;
|
|
variables: string[];
|
|
}
|
|
|
|
export interface TemplatePreviewRequest {
|
|
event_type: string;
|
|
title_template: string;
|
|
body_template: string;
|
|
}
|
|
|
|
export interface TemplatePreviewResponse {
|
|
title: string;
|
|
body: string;
|
|
}
|
|
|
|
// Notification Log types
|
|
export interface NotificationLogEntry {
|
|
id: number;
|
|
provider_id: number;
|
|
provider_name: string | null;
|
|
provider_type: string | null;
|
|
event_type: string;
|
|
title: string;
|
|
message: string;
|
|
success: boolean;
|
|
error_message: string | null;
|
|
printer_id: number | null;
|
|
printer_name: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface NotificationLogStats {
|
|
total: number;
|
|
success_count: number;
|
|
failure_count: number;
|
|
by_event_type: Record<string, number>;
|
|
by_provider: Record<string, number>;
|
|
}
|
|
|
|
// Spoolman types
|
|
export interface SpoolmanStatus {
|
|
enabled: boolean;
|
|
connected: boolean;
|
|
url: string | null;
|
|
}
|
|
|
|
export interface SkippedSpool {
|
|
location: string;
|
|
reason: string;
|
|
filament_type: string | null;
|
|
color: string | null;
|
|
}
|
|
|
|
export interface SpoolmanSyncResult {
|
|
success: boolean;
|
|
synced_count: number;
|
|
skipped_count: number;
|
|
skipped: SkippedSpool[];
|
|
errors: string[];
|
|
}
|
|
|
|
export interface UnlinkedSpool {
|
|
id: number;
|
|
filament_name: string | null;
|
|
filament_material: string | null;
|
|
filament_color_hex: string | null;
|
|
remaining_weight: number | null;
|
|
location: string | null;
|
|
}
|
|
|
|
// Update types
|
|
export interface VersionInfo {
|
|
version: string;
|
|
repo: string;
|
|
}
|
|
|
|
export interface UpdateCheckResult {
|
|
update_available: boolean;
|
|
current_version: string;
|
|
latest_version: string | null;
|
|
release_name?: string;
|
|
release_notes?: string;
|
|
release_url?: string;
|
|
published_at?: string;
|
|
error?: string;
|
|
message?: string;
|
|
is_docker?: boolean;
|
|
update_method?: 'docker' | 'git';
|
|
}
|
|
|
|
export interface UpdateStatus {
|
|
status: 'idle' | 'checking' | 'downloading' | 'installing' | 'complete' | 'error';
|
|
progress: number;
|
|
message: string;
|
|
error: string | null;
|
|
}
|
|
|
|
// Maintenance types
|
|
export interface MaintenanceType {
|
|
id: number;
|
|
name: string;
|
|
description: string | null;
|
|
default_interval_hours: number;
|
|
interval_type: 'hours' | 'days'; // "hours" = print hours, "days" = calendar days
|
|
icon: string | null;
|
|
wiki_url: string | null; // Documentation link
|
|
is_system: boolean;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface MaintenanceTypeCreate {
|
|
name: string;
|
|
description?: string | null;
|
|
default_interval_hours?: number;
|
|
interval_type?: 'hours' | 'days';
|
|
icon?: string | null;
|
|
wiki_url?: string | null;
|
|
}
|
|
|
|
export interface MaintenanceStatus {
|
|
id: number;
|
|
printer_id: number;
|
|
printer_name: string;
|
|
printer_model: string | null;
|
|
maintenance_type_id: number;
|
|
maintenance_type_name: string;
|
|
maintenance_type_icon: string | null;
|
|
maintenance_type_wiki_url: string | null; // Custom wiki URL from type
|
|
enabled: boolean;
|
|
interval_hours: number; // For hours type: print hours; for days type: number of days
|
|
interval_type: 'hours' | 'days';
|
|
current_hours: number;
|
|
hours_since_maintenance: number;
|
|
hours_until_due: number;
|
|
days_since_maintenance: number | null; // For days type
|
|
days_until_due: number | null; // For days type
|
|
is_due: boolean;
|
|
is_warning: boolean;
|
|
last_performed_at: string | null;
|
|
}
|
|
|
|
export interface PrinterMaintenanceOverview {
|
|
printer_id: number;
|
|
printer_name: string;
|
|
printer_model: string | null;
|
|
total_print_hours: number;
|
|
maintenance_items: MaintenanceStatus[];
|
|
due_count: number;
|
|
warning_count: number;
|
|
}
|
|
|
|
export interface MaintenanceHistory {
|
|
id: number;
|
|
printer_maintenance_id: number;
|
|
performed_at: string;
|
|
hours_at_maintenance: number;
|
|
notes: string | null;
|
|
}
|
|
|
|
export interface MaintenanceSummary {
|
|
total_due: number;
|
|
total_warning: number;
|
|
printers_with_issues: Array<{
|
|
printer_id: number;
|
|
printer_name: string;
|
|
due_count: number;
|
|
warning_count: number;
|
|
}>;
|
|
}
|
|
|
|
// External Links (sidebar)
|
|
export interface ExternalLink {
|
|
id: number;
|
|
name: string;
|
|
url: string;
|
|
icon: string;
|
|
custom_icon: string | null;
|
|
sort_order: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface ExternalLinkCreate {
|
|
name: string;
|
|
url: string;
|
|
icon: string;
|
|
}
|
|
|
|
export interface ExternalLinkUpdate {
|
|
name?: string;
|
|
url?: string;
|
|
icon?: string;
|
|
}
|
|
|
|
// API functions
|
|
export const api = {
|
|
// Printers
|
|
getPrinters: () => request<Printer[]>('/printers/'),
|
|
getPrinter: (id: number) => request<Printer>(`/printers/${id}`),
|
|
createPrinter: (data: PrinterCreate) =>
|
|
request<Printer>('/printers/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updatePrinter: (id: number, data: Partial<PrinterCreate>) =>
|
|
request<Printer>(`/printers/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deletePrinter: (id: number, deleteArchives: boolean = true) =>
|
|
request<{ status: string; archives_deleted: boolean }>(
|
|
`/printers/${id}?delete_archives=${deleteArchives}`,
|
|
{ method: 'DELETE' }
|
|
),
|
|
getPrinterStatus: (id: number) =>
|
|
request<PrinterStatus>(`/printers/${id}/status`),
|
|
refreshPrinterStatus: (id: number) =>
|
|
request<{ status: string }>(`/printers/${id}/refresh-status`, {
|
|
method: 'POST',
|
|
}),
|
|
connectPrinter: (id: number) =>
|
|
request<{ connected: boolean }>(`/printers/${id}/connect`, {
|
|
method: 'POST',
|
|
}),
|
|
disconnectPrinter: (id: number) =>
|
|
request<{ connected: boolean }>(`/printers/${id}/disconnect`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
// Print Control
|
|
stopPrint: (printerId: number) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/print/stop`, {
|
|
method: 'POST',
|
|
}),
|
|
pausePrint: (printerId: number) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/print/pause`, {
|
|
method: 'POST',
|
|
}),
|
|
resumePrint: (printerId: number) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/print/resume`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
// Skip Objects
|
|
getPrintableObjects: (printerId: number) =>
|
|
request<{
|
|
objects: Array<{ id: number; name: string; x: number | null; y: number | null; skipped: boolean }>;
|
|
total: number;
|
|
skipped_count: number;
|
|
is_printing: boolean;
|
|
bbox_all: [number, number, number, number] | null;
|
|
}>(`/printers/${printerId}/print/objects`),
|
|
|
|
skipObjects: (printerId: number, objectIds: number[]) =>
|
|
request<{ success: boolean; message: string; skipped_objects: number[] }>(
|
|
`/printers/${printerId}/print/skip-objects`,
|
|
{
|
|
method: 'POST',
|
|
body: JSON.stringify(objectIds),
|
|
}
|
|
),
|
|
|
|
// AMS Control
|
|
refreshAmsSlot: (printerId: number, amsId: number, slotId: number) =>
|
|
request<{ success: boolean; message: string }>(
|
|
`/printers/${printerId}/ams/${amsId}/slot/${slotId}/refresh`,
|
|
{ method: 'POST' }
|
|
),
|
|
|
|
// MQTT Debug Logging
|
|
enableMQTTLogging: (printerId: number) =>
|
|
request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, {
|
|
method: 'POST',
|
|
}),
|
|
disableMQTTLogging: (printerId: number) =>
|
|
request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/disable`, {
|
|
method: 'POST',
|
|
}),
|
|
getMQTTLogs: (printerId: number) =>
|
|
request<MQTTLogsResponse>(`/printers/${printerId}/logging`),
|
|
clearMQTTLogs: (printerId: number) =>
|
|
request<{ status: string }>(`/printers/${printerId}/logging`, {
|
|
method: 'DELETE',
|
|
}),
|
|
|
|
// Printer File Manager
|
|
getPrinterFiles: (printerId: number, path = '/') =>
|
|
request<{
|
|
path: string;
|
|
files: Array<{
|
|
name: string;
|
|
is_directory: boolean;
|
|
size: number;
|
|
path: string;
|
|
}>;
|
|
}>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`),
|
|
getPrinterFileDownloadUrl: (printerId: number, path: string) =>
|
|
`${API_BASE}/printers/${printerId}/files/download?path=${encodeURIComponent(path)}`,
|
|
deletePrinterFile: (printerId: number, path: string) =>
|
|
request<{ status: string; path: string }>(`/printers/${printerId}/files?path=${encodeURIComponent(path)}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
getPrinterStorage: (printerId: number) =>
|
|
request<{ used_bytes: number | null; free_bytes: number | null }>(`/printers/${printerId}/storage`),
|
|
|
|
// Archives
|
|
getArchives: (printerId?: number, projectId?: number, limit = 50, offset = 0) => {
|
|
const params = new URLSearchParams();
|
|
if (printerId) params.set('printer_id', String(printerId));
|
|
if (projectId) params.set('project_id', String(projectId));
|
|
params.set('limit', String(limit));
|
|
params.set('offset', String(offset));
|
|
return request<Archive[]>(`/archives/?${params}`);
|
|
},
|
|
getArchive: (id: number) => request<Archive>(`/archives/${id}`),
|
|
searchArchives: (query: string, options?: {
|
|
printerId?: number;
|
|
projectId?: number;
|
|
status?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}) => {
|
|
const params = new URLSearchParams();
|
|
params.set('q', query);
|
|
if (options?.printerId) params.set('printer_id', String(options.printerId));
|
|
if (options?.projectId) params.set('project_id', String(options.projectId));
|
|
if (options?.status) params.set('status', options.status);
|
|
if (options?.limit) params.set('limit', String(options.limit));
|
|
if (options?.offset) params.set('offset', String(options.offset));
|
|
return request<Archive[]>(`/archives/search?${params}`);
|
|
},
|
|
rebuildSearchIndex: () => request<{ message: string }>('/archives/search/rebuild-index', { method: 'POST' }),
|
|
updateArchive: (id: number, data: {
|
|
printer_id?: number | null;
|
|
project_id?: number | null;
|
|
print_name?: string;
|
|
is_favorite?: boolean;
|
|
tags?: string;
|
|
notes?: string;
|
|
cost?: number;
|
|
failure_reason?: string | null;
|
|
status?: string;
|
|
quantity?: number;
|
|
}) =>
|
|
request<Archive>(`/archives/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
toggleFavorite: (id: number) =>
|
|
request<Archive>(`/archives/${id}/favorite`, { method: 'POST' }),
|
|
deleteArchive: (id: number) =>
|
|
request<void>(`/archives/${id}`, { method: 'DELETE' }),
|
|
getArchiveStats: () => request<ArchiveStats>('/archives/stats'),
|
|
getFailureAnalysis: (options?: { days?: number; printerId?: number; projectId?: number }) => {
|
|
const params = new URLSearchParams();
|
|
if (options?.days) params.set('days', String(options.days));
|
|
if (options?.printerId) params.set('printer_id', String(options.printerId));
|
|
if (options?.projectId) params.set('project_id', String(options.projectId));
|
|
return request<FailureAnalysis>(`/archives/analysis/failures?${params}`);
|
|
},
|
|
compareArchives: (archiveIds: number[]) =>
|
|
request<ArchiveComparison>(`/archives/compare?archive_ids=${archiveIds.join(',')}`),
|
|
findSimilarArchives: (archiveId: number, limit = 10) =>
|
|
request<SimilarArchive[]>(`/archives/${archiveId}/similar?limit=${limit}`),
|
|
exportArchives: async (options?: {
|
|
format?: 'csv' | 'xlsx';
|
|
fields?: string[];
|
|
printerId?: number;
|
|
projectId?: number;
|
|
status?: string;
|
|
dateFrom?: string;
|
|
dateTo?: string;
|
|
search?: string;
|
|
}): Promise<{ blob: Blob; filename: string }> => {
|
|
const params = new URLSearchParams();
|
|
if (options?.format) params.set('format', options.format);
|
|
if (options?.fields) params.set('fields', options.fields.join(','));
|
|
if (options?.printerId) params.set('printer_id', String(options.printerId));
|
|
if (options?.projectId) params.set('project_id', String(options.projectId));
|
|
if (options?.status) params.set('status', options.status);
|
|
if (options?.dateFrom) params.set('date_from', options.dateFrom);
|
|
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}`);
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = options?.format === 'xlsx' ? 'archives_export.xlsx' : 'archives_export.csv';
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
|
if (match) filename = match[1];
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
return { blob, filename };
|
|
},
|
|
exportStats: async (options?: {
|
|
format?: 'csv' | 'xlsx';
|
|
days?: number;
|
|
printerId?: number;
|
|
projectId?: number;
|
|
}): Promise<{ blob: Blob; filename: string }> => {
|
|
const params = new URLSearchParams();
|
|
if (options?.format) params.set('format', options.format);
|
|
if (options?.days) params.set('days', String(options.days));
|
|
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}`);
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = options?.format === 'xlsx' ? 'stats_export.xlsx' : 'stats_export.csv';
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
|
if (match) filename = match[1];
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
return { blob, filename };
|
|
},
|
|
getArchiveDuplicates: (id: number) =>
|
|
request<{ duplicates: ArchiveDuplicate[]; count: number }>(`/archives/${id}/duplicates`),
|
|
backfillContentHashes: () =>
|
|
request<{ updated: number; errors: Array<{ id: number; error: string }> }>('/archives/backfill-hashes', {
|
|
method: 'POST',
|
|
}),
|
|
getArchiveThumbnail: (id: number) => `${API_BASE}/archives/${id}/thumbnail?v=${Date.now()}`,
|
|
getArchiveDownload: (id: number) => `${API_BASE}/archives/${id}/download`,
|
|
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()}`,
|
|
scanArchiveTimelapse: (id: number) =>
|
|
request<{
|
|
status: string;
|
|
message: string;
|
|
filename?: string;
|
|
available_files?: Array<{ name: string; path: string; size: number; mtime: string | null }>;
|
|
}>(`/archives/${id}/timelapse/scan`, {
|
|
method: 'POST',
|
|
}),
|
|
selectArchiveTimelapse: (id: number, filename: string) =>
|
|
request<{ status: string; message: string; filename: string }>(
|
|
`/archives/${id}/timelapse/select?filename=${encodeURIComponent(filename)}`,
|
|
{ method: 'POST' }
|
|
),
|
|
uploadArchiveTimelapse: async (archiveId: number, file: File): Promise<{ status: string; filename: string }> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/upload`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
// Timelapse Editor
|
|
getTimelapseInfo: (archiveId: number) =>
|
|
request<{
|
|
duration: number;
|
|
width: number;
|
|
height: number;
|
|
fps: number;
|
|
codec: string;
|
|
file_size: number;
|
|
has_audio: boolean;
|
|
}>(`/archives/${archiveId}/timelapse/info`),
|
|
getTimelapseThumbnails: (archiveId: number, count: number = 10) =>
|
|
request<{
|
|
thumbnails: string[];
|
|
timestamps: number[];
|
|
}>(`/archives/${archiveId}/timelapse/thumbnails?count=${count}`),
|
|
processTimelapse: async (
|
|
archiveId: number,
|
|
params: {
|
|
trimStart?: number;
|
|
trimEnd?: number;
|
|
speed?: number;
|
|
saveMode: 'replace' | 'new';
|
|
outputFilename?: string;
|
|
},
|
|
audioFile?: File
|
|
): Promise<{ status: string; output_path: string | null; message: string }> => {
|
|
const formData = new FormData();
|
|
formData.append('trim_start', String(params.trimStart ?? 0));
|
|
if (params.trimEnd !== undefined) {
|
|
formData.append('trim_end', String(params.trimEnd));
|
|
}
|
|
formData.append('speed', String(params.speed ?? 1));
|
|
formData.append('save_mode', params.saveMode);
|
|
if (params.outputFilename) {
|
|
formData.append('output_filename', params.outputFilename);
|
|
}
|
|
if (audioFile) {
|
|
formData.append('audio', audioFile);
|
|
}
|
|
const response = await fetch(`${API_BASE}/archives/${archiveId}/timelapse/process`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
// Photos
|
|
getArchivePhotoUrl: (archiveId: number, filename: string) =>
|
|
`${API_BASE}/archives/${archiveId}/photos/${encodeURIComponent(filename)}`,
|
|
uploadArchivePhoto: async (archiveId: number, file: File): Promise<{ status: string; filename: string; photos: string[] }> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await fetch(`${API_BASE}/archives/${archiveId}/photos`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
deleteArchivePhoto: (archiveId: number, filename: string) =>
|
|
request<{ status: string; photos: string[] | null }>(`/archives/${archiveId}/photos/${encodeURIComponent(filename)}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
// Source 3MF (original slicer project file)
|
|
getSource3mfDownloadUrl: (archiveId: number) =>
|
|
`${API_BASE}/archives/${archiveId}/source`,
|
|
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 }> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await fetch(`${API_BASE}/archives/${archiveId}/source`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
deleteSource3mf: (archiveId: number) =>
|
|
request<{ status: string }>(`/archives/${archiveId}/source`, {
|
|
method: 'DELETE',
|
|
}),
|
|
|
|
// QR Code
|
|
getArchiveQRCodeUrl: (archiveId: number, size = 200) =>
|
|
`${API_BASE}/archives/${archiveId}/qrcode?size=${size}`,
|
|
getArchiveCapabilities: (id: number) =>
|
|
request<{
|
|
has_model: boolean;
|
|
has_gcode: boolean;
|
|
has_source: boolean;
|
|
build_volume: { x: number; y: number; z: number };
|
|
filament_colors: string[];
|
|
}>(`/archives/${id}/capabilities`),
|
|
// Project Page
|
|
getArchiveProjectPage: (id: number) =>
|
|
request<{
|
|
title: string | null;
|
|
description: string | null;
|
|
designer: string | null;
|
|
designer_user_id: string | null;
|
|
license: string | null;
|
|
copyright: string | null;
|
|
creation_date: string | null;
|
|
modification_date: string | null;
|
|
origin: string | null;
|
|
profile_title: string | null;
|
|
profile_description: string | null;
|
|
profile_cover: string | null;
|
|
profile_user_id: string | null;
|
|
profile_user_name: string | null;
|
|
design_model_id: string | null;
|
|
design_profile_id: string | null;
|
|
design_region: string | null;
|
|
model_pictures: Array<{ name: string; path: string; url: string }>;
|
|
profile_pictures: Array<{ name: string; path: string; url: string }>;
|
|
thumbnails: Array<{ name: string; path: string; url: string }>;
|
|
}>(`/archives/${id}/project-page`),
|
|
updateArchiveProjectPage: (id: number, data: {
|
|
title?: string;
|
|
description?: string;
|
|
designer?: string;
|
|
license?: string;
|
|
copyright?: string;
|
|
profile_title?: string;
|
|
profile_description?: string;
|
|
}) =>
|
|
request(`/archives/${id}/project-page`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
getArchiveProjectImageUrl: (archiveId: number, imagePath: string) =>
|
|
`${API_BASE}/archives/${archiveId}/project-image/${encodeURIComponent(imagePath)}`,
|
|
getArchiveForSlicer: (id: number, filename: string) =>
|
|
`${API_BASE}/archives/${id}/file/${encodeURIComponent(filename.endsWith('.3mf') ? filename : filename + '.3mf')}`,
|
|
getArchiveFilamentRequirements: (archiveId: number) =>
|
|
request<{
|
|
archive_id: number;
|
|
filename: string;
|
|
filaments: Array<{
|
|
slot_id: number;
|
|
type: string;
|
|
color: string;
|
|
used_grams: number;
|
|
used_meters: number;
|
|
}>;
|
|
}>(`/archives/${archiveId}/filament-requirements`),
|
|
reprintArchive: (
|
|
archiveId: number,
|
|
printerId: number,
|
|
options?: {
|
|
ams_mapping?: number[];
|
|
timelapse?: boolean;
|
|
bed_levelling?: boolean;
|
|
flow_cali?: boolean;
|
|
vibration_cali?: boolean;
|
|
layer_inspect?: boolean;
|
|
use_ams?: boolean;
|
|
}
|
|
) =>
|
|
request<{ status: string; printer_id: number; archive_id: number; filename: string }>(
|
|
`/archives/${archiveId}/reprint?printer_id=${printerId}`,
|
|
{
|
|
method: 'POST',
|
|
headers: options ? { 'Content-Type': 'application/json' } : undefined,
|
|
body: options ? JSON.stringify(options) : undefined,
|
|
}
|
|
),
|
|
uploadArchive: async (file: File, printerId?: number): Promise<Archive> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const url = printerId
|
|
? `${API_BASE}/archives/upload?printer_id=${printerId}`
|
|
: `${API_BASE}/archives/upload`;
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
uploadArchivesBulk: async (files: File[], printerId?: number): Promise<BulkUploadResult> => {
|
|
const formData = new FormData();
|
|
files.forEach((file) => formData.append('files', file));
|
|
const url = printerId
|
|
? `${API_BASE}/archives/upload-bulk?printer_id=${printerId}`
|
|
: `${API_BASE}/archives/upload-bulk`;
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
|
|
// Settings
|
|
getSettings: () => request<AppSettings>('/settings/'),
|
|
updateSettings: (data: AppSettingsUpdate) =>
|
|
request<AppSettings>('/settings/', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
resetSettings: () =>
|
|
request<AppSettings>('/settings/reset', { method: 'POST' }),
|
|
exportBackup: async (categories?: Record<string, boolean>): Promise<{ blob: Blob; filename: string }> => {
|
|
const params = new URLSearchParams();
|
|
if (categories) {
|
|
if (categories.settings !== undefined) params.set('include_settings', String(categories.settings));
|
|
if (categories.notifications !== undefined) params.set('include_notifications', String(categories.notifications));
|
|
if (categories.templates !== undefined) params.set('include_templates', String(categories.templates));
|
|
if (categories.smart_plugs !== undefined) params.set('include_smart_plugs', String(categories.smart_plugs));
|
|
if (categories.external_links !== undefined) params.set('include_external_links', String(categories.external_links));
|
|
if (categories.printers !== undefined) params.set('include_printers', String(categories.printers));
|
|
if (categories.filaments !== undefined) params.set('include_filaments', String(categories.filaments));
|
|
if (categories.maintenance !== undefined) params.set('include_maintenance', String(categories.maintenance));
|
|
if (categories.archives !== undefined) params.set('include_archives', String(categories.archives));
|
|
if (categories.projects !== undefined) params.set('include_projects', String(categories.projects));
|
|
if (categories.access_codes !== undefined) params.set('include_access_codes', String(categories.access_codes));
|
|
}
|
|
const url = `${API_BASE}/settings/backup${params.toString() ? '?' + params.toString() : ''}`;
|
|
const response = await fetch(url);
|
|
|
|
// Get filename from Content-Disposition header
|
|
const contentDisposition = response.headers.get('Content-Disposition');
|
|
let filename = 'bambuddy-backup.json';
|
|
if (contentDisposition) {
|
|
const match = contentDisposition.match(/filename=([^;]+)/);
|
|
if (match) filename = match[1].trim();
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
return { blob, filename };
|
|
},
|
|
importBackup: async (file: File, overwrite = false) => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const url = `${API_BASE}/settings/restore${overwrite ? '?overwrite=true' : ''}`;
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
return response.json() as Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
restored?: Record<string, number>;
|
|
skipped?: Record<string, number>;
|
|
skipped_details?: Record<string, string[]>;
|
|
files_restored?: number;
|
|
total_skipped?: number;
|
|
}>;
|
|
},
|
|
checkFfmpeg: () =>
|
|
request<{ installed: boolean; path: string | null }>('/settings/check-ffmpeg'),
|
|
|
|
// Cloud
|
|
getCloudStatus: () => request<CloudAuthStatus>('/cloud/status'),
|
|
cloudLogin: (email: string, password: string, region = 'global') =>
|
|
request<CloudLoginResponse>('/cloud/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password, region }),
|
|
}),
|
|
cloudVerify: (email: string, code: string) =>
|
|
request<CloudLoginResponse>('/cloud/verify', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, code }),
|
|
}),
|
|
cloudSetToken: (access_token: string) =>
|
|
request<CloudAuthStatus>('/cloud/token', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ access_token }),
|
|
}),
|
|
cloudLogout: () =>
|
|
request<{ success: boolean }>('/cloud/logout', { method: 'POST' }),
|
|
getCloudSettings: (version = '02.04.00.70') =>
|
|
request<SlicerSettingsResponse>(`/cloud/settings?version=${version}`),
|
|
getCloudSettingDetail: (settingId: string) =>
|
|
request<SlicerSettingDetail>(`/cloud/settings/${settingId}`),
|
|
createCloudSetting: (data: SlicerSettingCreate) =>
|
|
request<SlicerSettingDetail>('/cloud/settings', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateCloudSetting: (settingId: string, data: SlicerSettingUpdate) =>
|
|
request<SlicerSettingDetail>(`/cloud/settings/${settingId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteCloudSetting: (settingId: string) =>
|
|
request<SlicerSettingDeleteResponse>(`/cloud/settings/${settingId}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
getCloudDevices: () => request<CloudDevice[]>('/cloud/devices'),
|
|
getCloudFields: (presetType: 'filament' | 'print' | 'process' | 'printer') =>
|
|
request<FieldDefinitionsResponse>(`/cloud/fields/${presetType}`),
|
|
getAllCloudFields: () =>
|
|
request<Record<string, FieldDefinitionsResponse>>('/cloud/fields'),
|
|
getFilamentInfo: (settingIds: string[]) =>
|
|
request<Record<string, { name: string; k: number | null }>>('/cloud/filament-info', {
|
|
method: 'POST',
|
|
body: JSON.stringify(settingIds),
|
|
}),
|
|
|
|
// Smart Plugs
|
|
getSmartPlugs: () => request<SmartPlug[]>('/smart-plugs/'),
|
|
getSmartPlug: (id: number) => request<SmartPlug>(`/smart-plugs/${id}`),
|
|
getSmartPlugByPrinter: (printerId: number) => request<SmartPlug | null>(`/smart-plugs/by-printer/${printerId}`),
|
|
createSmartPlug: (data: SmartPlugCreate) =>
|
|
request<SmartPlug>('/smart-plugs/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateSmartPlug: (id: number, data: SmartPlugUpdate) =>
|
|
request<SmartPlug>(`/smart-plugs/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteSmartPlug: (id: number) =>
|
|
request<void>(`/smart-plugs/${id}`, { method: 'DELETE' }),
|
|
controlSmartPlug: (id: number, action: 'on' | 'off' | 'toggle') =>
|
|
request<{ success: boolean; action: string }>(`/smart-plugs/${id}/control`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ action }),
|
|
}),
|
|
getSmartPlugStatus: (id: number) =>
|
|
request<SmartPlugStatus>(`/smart-plugs/${id}/status`),
|
|
testSmartPlugConnection: (ip_address: string, username?: string | null, password?: string | null) =>
|
|
request<SmartPlugTestResult>('/smart-plugs/test-connection', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ ip_address, username, password }),
|
|
}),
|
|
|
|
// Tasmota Discovery (auto-detects network)
|
|
startTasmotaScan: () =>
|
|
fetch(`${API_BASE}/smart-plugs/discover/scan`, { method: 'POST' })
|
|
.then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })),
|
|
getTasmotaScanStatus: () =>
|
|
request<TasmotaScanStatus>('/smart-plugs/discover/status'),
|
|
stopTasmotaScan: () =>
|
|
fetch(`${API_BASE}/smart-plugs/discover/stop`, { method: 'POST' })
|
|
.then(res => res.ok ? res.json() : res.json().then(e => { throw new Error(e.detail || `HTTP ${res.status}`); })),
|
|
getDiscoveredTasmotaDevices: () =>
|
|
request<DiscoveredTasmotaDevice[]>('/smart-plugs/discover/devices'),
|
|
|
|
// Print Queue
|
|
getQueue: (printerId?: number, status?: string) => {
|
|
const params = new URLSearchParams();
|
|
if (printerId) params.set('printer_id', String(printerId));
|
|
if (status) params.set('status', status);
|
|
return request<PrintQueueItem[]>(`/queue/?${params}`);
|
|
},
|
|
getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),
|
|
addToQueue: (data: PrintQueueItemCreate) =>
|
|
request<PrintQueueItem>('/queue/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateQueueItem: (id: number, data: PrintQueueItemUpdate) =>
|
|
request<PrintQueueItem>(`/queue/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
removeFromQueue: (id: number) =>
|
|
request<{ message: string }>(`/queue/${id}`, { method: 'DELETE' }),
|
|
reorderQueue: (items: { id: number; position: number }[]) =>
|
|
request<{ message: string }>('/queue/reorder', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ items }),
|
|
}),
|
|
cancelQueueItem: (id: number) =>
|
|
request<{ message: string }>(`/queue/${id}/cancel`, { method: 'POST' }),
|
|
stopQueueItem: (id: number) =>
|
|
request<{ message: string }>(`/queue/${id}/stop`, { method: 'POST' }),
|
|
startQueueItem: (id: number) =>
|
|
request<PrintQueueItem>(`/queue/${id}/start`, { method: 'POST' }),
|
|
|
|
// K-Profiles
|
|
getKProfiles: (printerId: number, nozzleDiameter = '0.4') =>
|
|
request<KProfilesResponse>(`/printers/${printerId}/kprofiles/?nozzle_diameter=${nozzleDiameter}`),
|
|
setKProfile: (printerId: number, profile: KProfileCreate) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(profile),
|
|
}),
|
|
deleteKProfile: (printerId: number, profile: KProfileDelete) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/`, {
|
|
method: 'DELETE',
|
|
body: JSON.stringify(profile),
|
|
}),
|
|
setKProfilesBatch: (printerId: number, profiles: KProfileCreate[]) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/batch`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(profiles),
|
|
}),
|
|
|
|
// K-Profile Notes (stored locally, not on printer)
|
|
getKProfileNotes: (printerId: number) =>
|
|
request<KProfileNotesResponse>(`/printers/${printerId}/kprofiles/notes`),
|
|
setKProfileNote: (printerId: number, settingId: string, note: string) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ setting_id: settingId, note }),
|
|
}),
|
|
deleteKProfileNote: (printerId: number, settingId: string) =>
|
|
request<{ success: boolean; message: string }>(`/printers/${printerId}/kprofiles/notes/${encodeURIComponent(settingId)}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
|
|
// Slot Preset Mappings
|
|
getSlotPresets: (printerId: number) =>
|
|
request<Record<number, SlotPresetMapping>>(`/printers/${printerId}/slot-presets`),
|
|
getSlotPreset: (printerId: number, amsId: number, trayId: number) =>
|
|
request<SlotPresetMapping | null>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`),
|
|
saveSlotPreset: (printerId: number, amsId: number, trayId: number, presetId: string, presetName: string) =>
|
|
request<SlotPresetMapping>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}?preset_id=${encodeURIComponent(presetId)}&preset_name=${encodeURIComponent(presetName)}`, {
|
|
method: 'PUT',
|
|
}),
|
|
deleteSlotPreset: (printerId: number, amsId: number, trayId: number) =>
|
|
request<{ success: boolean }>(`/printers/${printerId}/slot-presets/${amsId}/${trayId}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
|
|
// Filaments
|
|
listFilaments: () => request<Filament[]>('/filaments/'),
|
|
getFilament: (id: number) => request<Filament>(`/filaments/${id}`),
|
|
getFilamentsByType: (type: string) => request<Filament[]>(`/filaments/by-type/${type}`),
|
|
|
|
// Notification Providers
|
|
getNotificationProviders: () => request<NotificationProvider[]>('/notifications/'),
|
|
getNotificationProvider: (id: number) => request<NotificationProvider>(`/notifications/${id}`),
|
|
createNotificationProvider: (data: NotificationProviderCreate) =>
|
|
request<NotificationProvider>('/notifications/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateNotificationProvider: (id: number, data: NotificationProviderUpdate) =>
|
|
request<NotificationProvider>(`/notifications/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteNotificationProvider: (id: number) =>
|
|
request<{ message: string }>(`/notifications/${id}`, { method: 'DELETE' }),
|
|
testNotificationProvider: (id: number) =>
|
|
request<NotificationTestResponse>(`/notifications/${id}/test`, { method: 'POST' }),
|
|
testNotificationConfig: (data: NotificationTestRequest) =>
|
|
request<NotificationTestResponse>('/notifications/test-config', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
testAllNotificationProviders: () =>
|
|
request<{
|
|
tested: number;
|
|
success: number;
|
|
failed: number;
|
|
results: Array<{
|
|
provider_id: number;
|
|
provider_name: string;
|
|
provider_type: string;
|
|
success: boolean;
|
|
message: string;
|
|
}>;
|
|
}>('/notifications/test-all', { method: 'POST' }),
|
|
|
|
// Notification Templates
|
|
getNotificationTemplates: () => request<NotificationTemplate[]>('/notification-templates'),
|
|
getNotificationTemplate: (id: number) => request<NotificationTemplate>(`/notification-templates/${id}`),
|
|
updateNotificationTemplate: (id: number, data: NotificationTemplateUpdate) =>
|
|
request<NotificationTemplate>(`/notification-templates/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
resetNotificationTemplate: (id: number) =>
|
|
request<NotificationTemplate>(`/notification-templates/${id}/reset`, {
|
|
method: 'POST',
|
|
}),
|
|
getTemplateVariables: () => request<EventVariablesResponse[]>('/notification-templates/variables'),
|
|
previewTemplate: (data: TemplatePreviewRequest) =>
|
|
request<TemplatePreviewResponse>('/notification-templates/preview', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
|
|
// Notification Logs
|
|
getNotificationLogs: (params?: {
|
|
limit?: number;
|
|
offset?: number;
|
|
provider_id?: number;
|
|
event_type?: string;
|
|
success?: boolean;
|
|
days?: number;
|
|
}) => {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.limit) searchParams.set('limit', String(params.limit));
|
|
if (params?.offset) searchParams.set('offset', String(params.offset));
|
|
if (params?.provider_id) searchParams.set('provider_id', String(params.provider_id));
|
|
if (params?.event_type) searchParams.set('event_type', params.event_type);
|
|
if (params?.success !== undefined) searchParams.set('success', String(params.success));
|
|
if (params?.days) searchParams.set('days', String(params.days));
|
|
return request<NotificationLogEntry[]>(`/notifications/logs?${searchParams}`);
|
|
},
|
|
getNotificationLogStats: (days = 7) =>
|
|
request<NotificationLogStats>(`/notifications/logs/stats?days=${days}`),
|
|
clearNotificationLogs: (olderThanDays = 30) =>
|
|
request<{ deleted: number; message: string }>(
|
|
`/notifications/logs?older_than_days=${olderThanDays}`,
|
|
{ method: 'DELETE' }
|
|
),
|
|
|
|
// Spoolman Integration
|
|
getSpoolmanStatus: () => request<SpoolmanStatus>('/spoolman/status'),
|
|
connectSpoolman: () =>
|
|
request<{ success: boolean; message: string }>('/spoolman/connect', {
|
|
method: 'POST',
|
|
}),
|
|
disconnectSpoolman: () =>
|
|
request<{ success: boolean; message: string }>('/spoolman/disconnect', {
|
|
method: 'POST',
|
|
}),
|
|
syncPrinterAms: (printerId: number) =>
|
|
request<SpoolmanSyncResult>(`/spoolman/sync/${printerId}`, {
|
|
method: 'POST',
|
|
}),
|
|
syncAllPrintersAms: () =>
|
|
request<SpoolmanSyncResult>('/spoolman/sync-all', {
|
|
method: 'POST',
|
|
}),
|
|
getSpoolmanSpools: () =>
|
|
request<{ spools: unknown[] }>('/spoolman/spools'),
|
|
getSpoolmanFilaments: () =>
|
|
request<{ filaments: unknown[] }>('/spoolman/filaments'),
|
|
getUnlinkedSpools: () =>
|
|
request<UnlinkedSpool[]>('/spoolman/spools/unlinked'),
|
|
linkSpool: (spoolId: number, trayUuid: string) =>
|
|
request<{ success: boolean; message: string }>(`/spoolman/spools/${spoolId}/link`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ tray_uuid: trayUuid }),
|
|
}),
|
|
|
|
// Updates
|
|
getVersion: () => request<VersionInfo>('/updates/version'),
|
|
checkForUpdates: () => request<UpdateCheckResult>('/updates/check'),
|
|
applyUpdate: () =>
|
|
request<{ success: boolean; message: string; status?: UpdateStatus; is_docker?: boolean }>('/updates/apply', {
|
|
method: 'POST',
|
|
}),
|
|
getUpdateStatus: () => request<UpdateStatus>('/updates/status'),
|
|
|
|
// Maintenance
|
|
getMaintenanceTypes: () => request<MaintenanceType[]>('/maintenance/types'),
|
|
createMaintenanceType: (data: MaintenanceTypeCreate) =>
|
|
request<MaintenanceType>('/maintenance/types', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateMaintenanceType: (id: number, data: Partial<MaintenanceTypeCreate>) =>
|
|
request<MaintenanceType>(`/maintenance/types/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteMaintenanceType: (id: number) =>
|
|
request<{ status: string }>(`/maintenance/types/${id}`, { method: 'DELETE' }),
|
|
getMaintenanceOverview: () => request<PrinterMaintenanceOverview[]>('/maintenance/overview'),
|
|
getPrinterMaintenance: (printerId: number) =>
|
|
request<PrinterMaintenanceOverview>(`/maintenance/printers/${printerId}`),
|
|
updateMaintenanceItem: (itemId: number, data: { custom_interval_hours?: number | null; custom_interval_type?: 'hours' | 'days' | null; enabled?: boolean }) =>
|
|
request<MaintenanceStatus>(`/maintenance/items/${itemId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
performMaintenance: (itemId: number, notes?: string) =>
|
|
request<MaintenanceStatus>(`/maintenance/items/${itemId}/perform`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ notes }),
|
|
}),
|
|
getMaintenanceHistory: (itemId: number) =>
|
|
request<MaintenanceHistory[]>(`/maintenance/items/${itemId}/history`),
|
|
getMaintenanceSummary: () => request<MaintenanceSummary>('/maintenance/summary'),
|
|
setPrinterHours: (printerId: number, totalHours: number) =>
|
|
request<{ printer_id: number; total_hours: number; archive_hours: number; offset_hours: number }>(
|
|
`/maintenance/printers/${printerId}/hours?total_hours=${totalHours}`,
|
|
{ method: 'PATCH' }
|
|
),
|
|
assignMaintenanceType: (printerId: number, typeId: number) =>
|
|
request<MaintenanceStatus>(`/maintenance/printers/${printerId}/assign/${typeId}`, {
|
|
method: 'POST',
|
|
}),
|
|
removeMaintenanceItem: (itemId: number) =>
|
|
request<{ status: string }>(`/maintenance/items/${itemId}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
|
|
// Camera
|
|
getCameraStreamUrl: (printerId: number, fps = 10) =>
|
|
`${API_BASE}/printers/${printerId}/camera/stream?fps=${fps}`,
|
|
getCameraSnapshotUrl: (printerId: number) =>
|
|
`${API_BASE}/printers/${printerId}/camera/snapshot`,
|
|
testCameraConnection: (printerId: number) =>
|
|
request<{ success: boolean; message?: string; error?: string }>(`/printers/${printerId}/camera/test`),
|
|
|
|
// External Links
|
|
getExternalLinks: () => request<ExternalLink[]>('/external-links/'),
|
|
getExternalLink: (id: number) => request<ExternalLink>(`/external-links/${id}`),
|
|
createExternalLink: (data: ExternalLinkCreate) =>
|
|
request<ExternalLink>('/external-links/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateExternalLink: (id: number, data: ExternalLinkUpdate) =>
|
|
request<ExternalLink>(`/external-links/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteExternalLink: (id: number) =>
|
|
request<{ message: string }>(`/external-links/${id}`, { method: 'DELETE' }),
|
|
reorderExternalLinks: (ids: number[]) =>
|
|
request<ExternalLink[]>('/external-links/reorder', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ ids }),
|
|
}),
|
|
uploadExternalLinkIcon: async (id: number, file: File): Promise<ExternalLink> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await fetch(`${API_BASE}/external-links/${id}/icon`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
deleteExternalLinkIcon: (id: number) =>
|
|
request<ExternalLink>(`/external-links/${id}/icon`, { method: 'DELETE' }),
|
|
getExternalLinkIconUrl: (id: number) => `${API_BASE}/external-links/${id}/icon`,
|
|
|
|
// Projects
|
|
getProjects: (status?: string) => {
|
|
const params = new URLSearchParams();
|
|
if (status) params.set('status', status);
|
|
return request<ProjectListItem[]>(`/projects/?${params}`);
|
|
},
|
|
getProject: (id: number) => request<Project>(`/projects/${id}`),
|
|
createProject: (data: ProjectCreate) =>
|
|
request<Project>('/projects/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateProject: (id: number, data: ProjectUpdate) =>
|
|
request<Project>(`/projects/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteProject: (id: number) =>
|
|
request<{ message: string }>(`/projects/${id}`, { method: 'DELETE' }),
|
|
getProjectArchives: (id: number, limit = 100, offset = 0) =>
|
|
request<Archive[]>(`/projects/${id}/archives?limit=${limit}&offset=${offset}`),
|
|
addArchivesToProject: (projectId: number, archiveIds: number[]) =>
|
|
request<{ message: string }>(`/projects/${projectId}/add-archives`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ archive_ids: archiveIds }),
|
|
}),
|
|
removeArchivesFromProject: (projectId: number, archiveIds: number[]) =>
|
|
request<{ message: string }>(`/projects/${projectId}/remove-archives`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ archive_ids: archiveIds }),
|
|
}),
|
|
addQueueItemsToProject: (projectId: number, queueItemIds: number[]) =>
|
|
request<{ message: string }>(`/projects/${projectId}/add-queue`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ queue_item_ids: queueItemIds }),
|
|
}),
|
|
|
|
// Project Attachments
|
|
uploadProjectAttachment: async (projectId: number, file: File): Promise<{
|
|
status: string;
|
|
filename: string;
|
|
original_name: string;
|
|
attachments: ProjectAttachment[];
|
|
}> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const response = await fetch(`${API_BASE}/projects/${projectId}/attachments`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
return response.json();
|
|
},
|
|
getProjectAttachmentUrl: (projectId: number, filename: string) =>
|
|
`${API_BASE}/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
|
|
deleteProjectAttachment: (projectId: number, filename: string) =>
|
|
request<{ status: string; message: string; attachments: ProjectAttachment[] | null }>(
|
|
`/projects/${projectId}/attachments/${encodeURIComponent(filename)}`,
|
|
{ method: 'DELETE' }
|
|
),
|
|
|
|
// BOM (Bill of Materials)
|
|
getProjectBOM: (projectId: number) =>
|
|
request<BOMItem[]>(`/projects/${projectId}/bom`),
|
|
createBOMItem: (projectId: number, data: BOMItemCreate) =>
|
|
request<BOMItem>(`/projects/${projectId}/bom`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateBOMItem: (projectId: number, itemId: number, data: BOMItemUpdate) =>
|
|
request<BOMItem>(`/projects/${projectId}/bom/${itemId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteBOMItem: (projectId: number, itemId: number) =>
|
|
request<{ status: string; message: string }>(`/projects/${projectId}/bom/${itemId}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
|
|
// Templates
|
|
getTemplates: () => request<ProjectListItem[]>('/projects/templates/'),
|
|
createTemplateFromProject: (projectId: number) =>
|
|
request<Project>(`/projects/${projectId}/create-template`, { method: 'POST' }),
|
|
createProjectFromTemplate: (templateId: number, name?: string) =>
|
|
request<Project>(`/projects/from-template/${templateId}${name ? `?name=${encodeURIComponent(name)}` : ''}`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
// Timeline
|
|
getProjectTimeline: (projectId: number, limit = 50) =>
|
|
request<TimelineEvent[]>(`/projects/${projectId}/timeline?limit=${limit}`),
|
|
|
|
// API Keys
|
|
getAPIKeys: () => request<APIKey[]>('/api-keys/'),
|
|
createAPIKey: (data: APIKeyCreate) =>
|
|
request<APIKeyCreateResponse>('/api-keys/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
updateAPIKey: (id: number, data: APIKeyUpdate) =>
|
|
request<APIKey>(`/api-keys/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
}),
|
|
deleteAPIKey: (id: number) =>
|
|
request<{ message: string }>(`/api-keys/${id}`, { method: 'DELETE' }),
|
|
|
|
// AMS History
|
|
getAMSHistory: (printerId: number, amsId: number, hours = 24) =>
|
|
request<AMSHistoryResponse>(`/ams-history/${printerId}/${amsId}?hours=${hours}`),
|
|
|
|
// System Info
|
|
getSystemInfo: () => request<SystemInfo>('/system/info'),
|
|
};
|
|
|
|
// AMS History types
|
|
export interface AMSHistoryPoint {
|
|
recorded_at: string;
|
|
humidity: number | null;
|
|
humidity_raw: number | null;
|
|
temperature: number | null;
|
|
}
|
|
|
|
export interface AMSHistoryResponse {
|
|
printer_id: number;
|
|
ams_id: number;
|
|
data: AMSHistoryPoint[];
|
|
min_humidity: number | null;
|
|
max_humidity: number | null;
|
|
avg_humidity: number | null;
|
|
min_temperature: number | null;
|
|
max_temperature: number | null;
|
|
avg_temperature: number | null;
|
|
}
|
|
|
|
// System Info types
|
|
export interface SystemInfo {
|
|
app: {
|
|
version: string;
|
|
base_dir: string;
|
|
archive_dir: string;
|
|
};
|
|
database: {
|
|
archives: number;
|
|
archives_completed: number;
|
|
archives_failed: number;
|
|
archives_printing: number;
|
|
printers: number;
|
|
filaments: number;
|
|
projects: number;
|
|
smart_plugs: number;
|
|
total_print_time_seconds: number;
|
|
total_print_time_formatted: string;
|
|
total_filament_grams: number;
|
|
total_filament_kg: number;
|
|
};
|
|
printers: {
|
|
total: number;
|
|
connected: number;
|
|
connected_list: Array<{
|
|
id: number;
|
|
name: string;
|
|
state: string;
|
|
model: string;
|
|
}>;
|
|
};
|
|
storage: {
|
|
archive_size_bytes: number;
|
|
archive_size_formatted: string;
|
|
database_size_bytes: number;
|
|
database_size_formatted: string;
|
|
disk_total_bytes: number;
|
|
disk_total_formatted: string;
|
|
disk_used_bytes: number;
|
|
disk_used_formatted: string;
|
|
disk_free_bytes: number;
|
|
disk_free_formatted: string;
|
|
disk_percent_used: number;
|
|
};
|
|
system: {
|
|
platform: string;
|
|
platform_release: string;
|
|
platform_version: string;
|
|
architecture: string;
|
|
hostname: string;
|
|
python_version: string;
|
|
uptime_seconds: number;
|
|
uptime_formatted: string;
|
|
boot_time: string;
|
|
};
|
|
memory: {
|
|
total_bytes: number;
|
|
total_formatted: string;
|
|
available_bytes: number;
|
|
available_formatted: string;
|
|
used_bytes: number;
|
|
used_formatted: string;
|
|
percent_used: number;
|
|
};
|
|
cpu: {
|
|
count: number;
|
|
count_logical: number;
|
|
percent: number;
|
|
};
|
|
}
|
|
|
|
// Discovery types
|
|
export interface DiscoveredPrinter {
|
|
serial: string;
|
|
name: string;
|
|
ip_address: string;
|
|
model: string | null;
|
|
discovered_at: string | null;
|
|
}
|
|
|
|
export interface DiscoveryStatus {
|
|
running: boolean;
|
|
}
|
|
|
|
export interface DiscoveryInfo {
|
|
is_docker: boolean;
|
|
ssdp_running: boolean;
|
|
scan_running: boolean;
|
|
}
|
|
|
|
export interface SubnetScanStatus {
|
|
running: boolean;
|
|
scanned: number;
|
|
total: number;
|
|
}
|
|
|
|
// Discovery API
|
|
export const discoveryApi = {
|
|
getInfo: () => request<DiscoveryInfo>('/discovery/info'),
|
|
|
|
getStatus: () => request<DiscoveryStatus>('/discovery/status'),
|
|
|
|
startDiscovery: (duration: number = 10) =>
|
|
request<DiscoveryStatus>(`/discovery/start?duration=${duration}`, { method: 'POST' }),
|
|
|
|
stopDiscovery: () =>
|
|
request<DiscoveryStatus>('/discovery/stop', { method: 'POST' }),
|
|
|
|
getDiscoveredPrinters: () =>
|
|
request<DiscoveredPrinter[]>('/discovery/printers'),
|
|
|
|
// Subnet scanning (for Docker environments)
|
|
startSubnetScan: (subnet: string, timeout: number = 1.0) =>
|
|
request<SubnetScanStatus>('/discovery/scan', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ subnet, timeout }),
|
|
}),
|
|
|
|
getScanStatus: () => request<SubnetScanStatus>('/discovery/scan/status'),
|
|
|
|
stopSubnetScan: () =>
|
|
request<SubnetScanStatus>('/discovery/scan/stop', { method: 'POST' }),
|
|
};
|
|
|
|
// Virtual Printer types
|
|
export interface VirtualPrinterStatus {
|
|
enabled: boolean;
|
|
running: boolean;
|
|
mode: 'immediate' | 'queue';
|
|
name: string;
|
|
serial: string;
|
|
model: string;
|
|
model_name: string;
|
|
pending_files: number;
|
|
}
|
|
|
|
export interface VirtualPrinterSettings {
|
|
enabled: boolean;
|
|
access_code_set: boolean;
|
|
mode: 'immediate' | 'queue';
|
|
model: string;
|
|
status: VirtualPrinterStatus;
|
|
}
|
|
|
|
export interface VirtualPrinterModels {
|
|
models: Record<string, string>; // SSDP code -> display name
|
|
default: string;
|
|
}
|
|
|
|
export interface PendingUpload {
|
|
id: number;
|
|
filename: string;
|
|
file_size: number;
|
|
source_ip: string | null;
|
|
status: string;
|
|
tags: string | null;
|
|
notes: string | null;
|
|
project_id: number | null;
|
|
uploaded_at: string;
|
|
}
|
|
|
|
// Virtual Printer API
|
|
export const virtualPrinterApi = {
|
|
getSettings: () => request<VirtualPrinterSettings>('/settings/virtual-printer'),
|
|
|
|
getModels: () => request<VirtualPrinterModels>('/settings/virtual-printer/models'),
|
|
|
|
updateSettings: (data: {
|
|
enabled?: boolean;
|
|
access_code?: string;
|
|
mode?: 'immediate' | 'queue';
|
|
model?: string;
|
|
}) => {
|
|
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);
|
|
|
|
return request<VirtualPrinterSettings>(`/settings/virtual-printer?${params.toString()}`, {
|
|
method: 'PUT',
|
|
});
|
|
},
|
|
};
|
|
|
|
// Pending Uploads API
|
|
export const pendingUploadsApi = {
|
|
list: () => request<PendingUpload[]>('/pending-uploads/'),
|
|
|
|
getCount: () => request<{ count: number }>('/pending-uploads/count'),
|
|
|
|
get: (id: number) => request<PendingUpload>(`/pending-uploads/${id}`),
|
|
|
|
archive: (id: number, data?: { tags?: string; notes?: string; project_id?: number }) =>
|
|
request<{ id: number; print_name: string; filename: string }>(`/pending-uploads/${id}/archive`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data || {}),
|
|
}),
|
|
|
|
discard: (id: number) =>
|
|
request<{ success: boolean }>(`/pending-uploads/${id}`, { method: 'DELETE' }),
|
|
|
|
archiveAll: () =>
|
|
request<{ archived: number; failed: number }>('/pending-uploads/archive-all', { method: 'POST' }),
|
|
|
|
discardAll: () =>
|
|
request<{ discarded: number }>('/pending-uploads/discard-all', { method: 'DELETE' }),
|
|
};
|
|
|
|
// Firmware API Types
|
|
export interface FirmwareUpdateInfo {
|
|
printer_id: number;
|
|
printer_name: string;
|
|
model: string | null;
|
|
current_version: string | null;
|
|
latest_version: string | null;
|
|
update_available: boolean;
|
|
download_url: string | null;
|
|
release_notes: string | null;
|
|
}
|
|
|
|
export interface FirmwareUploadPrepare {
|
|
can_proceed: boolean;
|
|
sd_card_present: boolean;
|
|
sd_card_free_space: number;
|
|
firmware_size: number;
|
|
space_sufficient: boolean;
|
|
update_available: boolean;
|
|
current_version: string | null;
|
|
latest_version: string | null;
|
|
firmware_filename: string | null;
|
|
errors: string[];
|
|
}
|
|
|
|
export interface FirmwareUploadStatus {
|
|
status: 'idle' | 'preparing' | 'downloading' | 'uploading' | 'complete' | 'error';
|
|
progress: number;
|
|
message: string;
|
|
error: string | null;
|
|
firmware_filename: string | null;
|
|
firmware_version: string | null;
|
|
}
|
|
|
|
// Firmware API
|
|
export const firmwareApi = {
|
|
checkUpdates: () =>
|
|
request<{ updates: FirmwareUpdateInfo[]; updates_available: number }>('/firmware/updates'),
|
|
|
|
checkPrinterUpdate: (printerId: number) =>
|
|
request<FirmwareUpdateInfo>(`/firmware/updates/${printerId}`),
|
|
|
|
prepareUpload: (printerId: number) =>
|
|
request<FirmwareUploadPrepare>(`/firmware/updates/${printerId}/prepare`),
|
|
|
|
startUpload: (printerId: number) =>
|
|
request<{ started: boolean; message: string }>(`/firmware/updates/${printerId}/upload`, {
|
|
method: 'POST',
|
|
}),
|
|
|
|
getUploadStatus: (printerId: number) =>
|
|
request<FirmwareUploadStatus>(`/firmware/updates/${printerId}/upload/status`),
|
|
};
|
|
|
|
// Support types
|
|
export interface DebugLoggingState {
|
|
enabled: boolean;
|
|
enabled_at: string | null;
|
|
duration_seconds: number | null;
|
|
}
|
|
|
|
// Support API
|
|
export const supportApi = {
|
|
getDebugLoggingState: () =>
|
|
request<DebugLoggingState>('/support/debug-logging'),
|
|
|
|
setDebugLogging: (enabled: boolean) =>
|
|
request<DebugLoggingState>('/support/debug-logging', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ enabled }),
|
|
}),
|
|
|
|
downloadSupportBundle: async () => {
|
|
const response = await fetch(`${API_BASE}/support/bundle`);
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({}));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
// Get filename from Content-Disposition header or use default
|
|
const disposition = response.headers.get('Content-Disposition');
|
|
const filenameMatch = disposition?.match(/filename=(.+)/);
|
|
const filename = filenameMatch ? filenameMatch[1] : 'bambuddy-support.zip';
|
|
|
|
// Download the blob
|
|
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);
|
|
},
|
|
};
|