mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 11:56:01 +02:00
Merge remote-tracking branch 'upstream/dev' into feature/billing
This commit is contained in:
@@ -341,6 +341,10 @@ export interface OverlayStatus {
|
||||
layer_num: number | null;
|
||||
total_layers: number | null;
|
||||
stg_cur_name: string | null;
|
||||
// Nozzle / bed / chamber readings for the overlay's temperature fields
|
||||
// (#1422). Only the keys a viewer is shown; chamber is absent on models
|
||||
// without a real sensor.
|
||||
temperatures: Record<string, number>;
|
||||
time_format: 'system' | '12h' | '24h';
|
||||
}
|
||||
|
||||
@@ -357,6 +361,10 @@ export interface Printer {
|
||||
model: string | null;
|
||||
location: string | null; // Group/location name
|
||||
nozzle_count: number; // 1 or 2, auto-detected from MQTT
|
||||
// Model is sold with both Standard and High Flow nozzles, so a K-profile's
|
||||
// flow type is a real choice. Derived from the model, not the nozzle count —
|
||||
// only the A-series has a single variant.
|
||||
supports_nozzle_flow_type: boolean;
|
||||
is_active: boolean;
|
||||
auto_archive: boolean;
|
||||
external_camera_url: string | null;
|
||||
@@ -1256,6 +1264,8 @@ export interface AppSettings {
|
||||
mqtt_use_tls: boolean;
|
||||
// External URL for notifications
|
||||
external_url: string;
|
||||
// Directory holding docker-compose.yml, shown in the update instructions (#2664)
|
||||
docker_compose_dir: string;
|
||||
// Home Assistant integration
|
||||
ha_enabled: boolean;
|
||||
ha_url: string;
|
||||
@@ -1605,6 +1615,12 @@ export interface SliceRequest {
|
||||
// instead of the picked profile triplet. The preset refs above are still
|
||||
// required by the backend validator but go unused on this path.
|
||||
use_embedded_settings?: boolean;
|
||||
// Layout passes the slicer runs before slicing (#2548), both off by
|
||||
// default because they move or rotate the objects the user laid out.
|
||||
// Unlike the fields above these are CLI actions rather than profile
|
||||
// values, so they apply on the embedded-settings path too.
|
||||
auto_orient?: boolean;
|
||||
auto_arrange?: boolean;
|
||||
}
|
||||
|
||||
// GET /api/v1/slicer/presets — unified listing across cloud / local / standard.
|
||||
@@ -2156,6 +2172,75 @@ export interface HATestConnectionResult {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// A Home Assistant entity bound to a printer for display on its card (#1148, #448).
|
||||
// Read-only: unlike a SmartPlug there is nothing here to switch.
|
||||
export interface PrinterHASensor {
|
||||
id: number;
|
||||
printer_id: number;
|
||||
name: string;
|
||||
entity_id: string;
|
||||
kind: 'binary' | 'numeric';
|
||||
device_class: string | null; // HA's own class: "door", "temperature", ...
|
||||
unit: string | null; // numeric sensors only
|
||||
// What counts as needing attention. Binary sensors use alert_state, numeric
|
||||
// ones the thresholds; all null means the sensor is display-only.
|
||||
alert_state: 'on' | 'off' | null;
|
||||
alert_above: number | null;
|
||||
alert_below: number | null;
|
||||
block_print: boolean; // hold the printer's queue while alerting
|
||||
notify_on_alert: boolean;
|
||||
show_on_printer_card: boolean;
|
||||
sort_order: number;
|
||||
last_state: string | null;
|
||||
last_changed: string | null;
|
||||
last_checked: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PrinterHASensorReading {
|
||||
id: number;
|
||||
name: string;
|
||||
entity_id: string;
|
||||
kind: 'binary' | 'numeric';
|
||||
device_class: string | null;
|
||||
unit: string | null;
|
||||
state: string | null; // null when unreadable or not yet polled
|
||||
value: number | null; // numeric sensors only
|
||||
alerting: boolean;
|
||||
block_print: boolean;
|
||||
reachable: boolean;
|
||||
last_changed: string | null;
|
||||
}
|
||||
|
||||
export interface PrinterHASensorCreate {
|
||||
printer_id: number;
|
||||
name: string;
|
||||
entity_id: string;
|
||||
kind: 'binary' | 'numeric';
|
||||
device_class?: string | null;
|
||||
unit?: string | null;
|
||||
alert_state?: 'on' | 'off' | null;
|
||||
alert_above?: number | null;
|
||||
alert_below?: number | null;
|
||||
block_print?: boolean;
|
||||
notify_on_alert?: boolean;
|
||||
show_on_printer_card?: boolean;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
export type PrinterHASensorUpdate = Partial<Omit<PrinterHASensorCreate, 'printer_id'>>;
|
||||
|
||||
// An entity offered by the binding picker.
|
||||
export interface HADisplayEntity {
|
||||
entity_id: string;
|
||||
friendly_name: string;
|
||||
state: string | null;
|
||||
domain: string; // "binary_sensor" | "sensor"
|
||||
device_class: string | null;
|
||||
unit_of_measurement: string | null;
|
||||
}
|
||||
|
||||
export interface SmartPlugEnergy {
|
||||
power: number | null; // Current watts
|
||||
voltage: number | null; // Volts
|
||||
@@ -2204,6 +2289,15 @@ export interface PrintQueueItem {
|
||||
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
|
||||
// Cross-model alternatives (#671), in priority order. Empty for ordinary
|
||||
// items. Present until dispatch resolves one, after which library_file_id and
|
||||
// target_model name the candidate that actually ran.
|
||||
variants?: Array<{
|
||||
library_file_id: number;
|
||||
filename: string;
|
||||
target_model: string;
|
||||
position: number;
|
||||
}>;
|
||||
// Either archive_id OR library_file_id must be set (archive created at print start)
|
||||
archive_id: number | null;
|
||||
library_file_id: number | null;
|
||||
@@ -2223,6 +2317,10 @@ export interface PrintQueueItem {
|
||||
// start route when skip_filament_check=true, or at queue creation if
|
||||
// PrintModal's deficit warning was acknowledged.
|
||||
skip_filament_check: boolean;
|
||||
// True when the source archive carries the slicer's own live-resolved
|
||||
// AMS-slot pick (extra_data.slicer_ams_mapping) — a reprint reuses that
|
||||
// exact physical spool instead of re-deriving one from type/color.
|
||||
archive_has_slicer_ams_mapping: boolean;
|
||||
ams_mapping: number[] | null; // AMS slot mapping for multi-color prints
|
||||
filament_overrides: Array<{ slot_id: number; type: string; color: string; color_name?: string; tray_info_idx?: string; force_color_match?: boolean }> | null; // Filament overrides for model-based assignment
|
||||
plate_id: number | null; // Plate ID for multi-plate 3MF files
|
||||
@@ -2268,6 +2366,32 @@ export interface PrintQueueItem {
|
||||
cleanup_library_after_dispatch?: boolean;
|
||||
}
|
||||
|
||||
export interface PrintBatchPlateTarget {
|
||||
plate_id: number | null;
|
||||
plate_name?: string | null;
|
||||
quantity_target: number;
|
||||
sort_order?: number;
|
||||
}
|
||||
|
||||
export interface PrintBatchPlateProgress {
|
||||
plate_id: number | null;
|
||||
plate_name: string | null;
|
||||
quantity_target: number;
|
||||
dispatched: number;
|
||||
remaining: number;
|
||||
pending_count: number;
|
||||
printing_count: number;
|
||||
completed_count: number;
|
||||
failed_count: number;
|
||||
cancelled_count: number;
|
||||
skipped_count: number;
|
||||
/** Measured from finished runs; null until one has produced a cost. */
|
||||
actual_cost: number | null;
|
||||
estimated_remaining_cost: number | null;
|
||||
filament_used_grams: number | null;
|
||||
print_time_seconds: number;
|
||||
}
|
||||
|
||||
export interface PrintBatch {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -2276,13 +2400,28 @@ export interface PrintBatch {
|
||||
quantity: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
completed_at: string | null;
|
||||
created_by_id: number | null;
|
||||
created_by_username: string | null;
|
||||
project_id: number | null;
|
||||
due_date: string | null;
|
||||
notes: string | null;
|
||||
pending_count: number;
|
||||
printing_count: number;
|
||||
completed_count: number;
|
||||
failed_count: number;
|
||||
cancelled_count: number;
|
||||
skipped_count: number;
|
||||
/** False for batches created before per-plate targets existed (#342):
|
||||
* they report progress but owe nothing and cannot be dispatched from. */
|
||||
has_targets: boolean;
|
||||
target_count: number;
|
||||
remaining_count: number;
|
||||
actual_cost: number | null;
|
||||
estimated_remaining_cost: number | null;
|
||||
filament_used_grams: number | null;
|
||||
print_time_seconds: number;
|
||||
plates: PrintBatchPlateProgress[];
|
||||
}
|
||||
|
||||
export interface PrintQueueItemCreate {
|
||||
@@ -2325,6 +2464,22 @@ export interface PrintQueueItemCreate {
|
||||
estimated_cost?: number | null;
|
||||
// Delete transient uploaded library file after scheduler creates the archive
|
||||
cleanup_library_after_dispatch?: boolean;
|
||||
// Cross-model alternatives (#671): several sliced files, one job, whichever
|
||||
// printer frees up first. Mutually exclusive with printer_id (a named printer
|
||||
// defeats the point) and with archive_id/library_file_id (these ARE the files).
|
||||
// Order is priority — index 0 wins when several printers are idle at once.
|
||||
variants?: QueueVariantCreate[];
|
||||
}
|
||||
|
||||
/** One candidate file for a cross-model queue item (#671). */
|
||||
export interface QueueVariantCreate {
|
||||
library_file_id: number;
|
||||
/** Read from the file's own sliced_for_model unless it declares none. */
|
||||
target_model?: string | null;
|
||||
plate_id?: number | null;
|
||||
ams_mapping?: number[] | null;
|
||||
nozzle_mapping?: number[] | null;
|
||||
filament_overrides?: Array<{ slot_id: number; type: string; color: string; color_name?: string; force_color_match?: boolean }> | null;
|
||||
}
|
||||
|
||||
export interface PrintBatchCreate {
|
||||
@@ -2335,6 +2490,28 @@ export interface PrintBatchCreate {
|
||||
* (manual "Group as batch"). When omitted/empty, an empty batch is
|
||||
* returned so the client can pass batch_id on subsequent addToQueue calls. */
|
||||
item_ids?: number[];
|
||||
/** Per-plate targets. Omitting them creates a plain grouping batch. */
|
||||
plates?: PrintBatchPlateTarget[];
|
||||
project_id?: number | null;
|
||||
due_date?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface PrintBatchUpdate {
|
||||
name?: string;
|
||||
status?: 'active' | 'cancelled';
|
||||
/** Replaces the full target set — a plate omitted here is removed. */
|
||||
plates?: PrintBatchPlateTarget[];
|
||||
project_id?: number | null;
|
||||
due_date?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
export interface PrintBatchDispatchRequest {
|
||||
plate_id?: number | null;
|
||||
only_plate?: boolean;
|
||||
/** Cap on items created across all plates; omit to queue everything owed. */
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface PrintQueueItemUpdate {
|
||||
@@ -2519,6 +2696,7 @@ export interface NotificationProvider {
|
||||
on_plate_clear_required: boolean;
|
||||
// Bed cooled
|
||||
on_bed_cooled: boolean;
|
||||
on_ha_sensor_alert: boolean;
|
||||
// First layer complete
|
||||
on_first_layer_complete: boolean;
|
||||
// Inventory stock alerts
|
||||
@@ -2579,6 +2757,7 @@ export interface NotificationProviderCreate {
|
||||
on_plate_clear_required?: boolean;
|
||||
// Bed cooled
|
||||
on_bed_cooled?: boolean;
|
||||
on_ha_sensor_alert?: boolean;
|
||||
// First layer complete
|
||||
on_first_layer_complete?: boolean;
|
||||
// Inventory stock alerts
|
||||
@@ -2632,6 +2811,7 @@ export interface NotificationProviderUpdate {
|
||||
on_plate_clear_required?: boolean;
|
||||
// Bed cooled
|
||||
on_bed_cooled?: boolean;
|
||||
on_ha_sensor_alert?: boolean;
|
||||
// First layer complete
|
||||
on_first_layer_complete?: boolean;
|
||||
// Inventory stock alerts
|
||||
@@ -2660,6 +2840,14 @@ export interface NotificationProviderUpdate {
|
||||
export type ScheduleType = 'hourly' | 'daily' | 'weekly';
|
||||
export type GitProviderType = 'github' | 'gitea' | 'forgejo' | 'gitlab';
|
||||
|
||||
/** How many cloud accounts a backup would collect presets from. Counts only —
|
||||
* with auth enabled the accounts belong to individual users, so the backup
|
||||
* administrator sees how many, never whose. */
|
||||
export interface CloudAccountCounts {
|
||||
bambu: number;
|
||||
orca: number;
|
||||
}
|
||||
|
||||
export interface GitHubBackupConfig {
|
||||
id: number;
|
||||
repository_url: string;
|
||||
@@ -3194,6 +3382,10 @@ export interface UpdateCheckResult {
|
||||
is_windows_installer?: boolean;
|
||||
update_method?: 'docker' | 'git' | 'ha_addon' | 'windows_installer';
|
||||
installer_download_url?: string | null;
|
||||
// Best-effort guess at the compose directory (#2664). Prefill only — the
|
||||
// value the user saved lives on AppSettings.docker_compose_dir, so clearing
|
||||
// that field falls back to this guess instead of resurrecting the old value.
|
||||
compose_dir_detected?: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateStatus {
|
||||
@@ -3653,6 +3845,11 @@ export interface OIDCProvider {
|
||||
// #1589: when true, the LoginPage redirects unauthenticated visitors
|
||||
// straight to this provider on mount. At most one provider may carry this.
|
||||
is_autologin: boolean;
|
||||
// #2593: defined by BAMBUDDY_OIDC_* and rewritten from the environment on
|
||||
// every boot. The API answers 409 to any write, so the settings UI must not
|
||||
// offer edit/delete controls that cannot succeed. Optional so a response
|
||||
// from an older backend still type-checks.
|
||||
is_env_managed?: boolean;
|
||||
}
|
||||
|
||||
export interface OIDCProviderCreate {
|
||||
@@ -4905,6 +5102,10 @@ export const api = {
|
||||
dateTo?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
// Sorting is server-side because paging is: ordering only the rows the
|
||||
// client holds would sort one page, not the log (#2636).
|
||||
sortBy?: string;
|
||||
sortDir?: 'asc' | 'desc';
|
||||
}) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.search) searchParams.set('search', params.search);
|
||||
@@ -4915,6 +5116,8 @@ export const api = {
|
||||
if (params?.dateTo) searchParams.set('date_to', params.dateTo);
|
||||
if (params?.limit) searchParams.set('limit', String(params.limit));
|
||||
if (params?.offset !== undefined) searchParams.set('offset', String(params.offset));
|
||||
if (params?.sortBy) searchParams.set('sort_by', params.sortBy);
|
||||
if (params?.sortDir) searchParams.set('sort_dir', params.sortDir);
|
||||
return request<PrintLogResponse>(`/print-log/?${searchParams}`);
|
||||
},
|
||||
getPrintLogThumbnail: (id: number) => withStreamToken(`${API_BASE}/print-log/${id}/thumbnail`),
|
||||
@@ -5185,6 +5388,22 @@ export const api = {
|
||||
getHASensorEntities: () =>
|
||||
request<HASensorEntity[]>('/smart-plugs/ha/sensors'),
|
||||
|
||||
// Home Assistant sensors bound to a printer (#1148, #448)
|
||||
getHASensors: (printerId?: number) =>
|
||||
request<PrinterHASensor[]>(`/ha-sensors/${printerId ? `?printer_id=${printerId}` : ''}`),
|
||||
getHASensorReadings: (printerId: number) =>
|
||||
request<PrinterHASensorReading[]>(`/ha-sensors/by-printer/${printerId}/readings`),
|
||||
getBindableHAEntities: (search?: string) => {
|
||||
const params = search ? `?search=${encodeURIComponent(search)}` : '';
|
||||
return request<HADisplayEntity[]>(`/ha-sensors/entities${params}`);
|
||||
},
|
||||
createHASensor: (data: PrinterHASensorCreate) =>
|
||||
request<PrinterHASensor>('/ha-sensors/', { method: 'POST', body: JSON.stringify(data) }),
|
||||
updateHASensor: (id: number, data: PrinterHASensorUpdate) =>
|
||||
request<PrinterHASensor>(`/ha-sensors/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
|
||||
deleteHASensor: (id: number) =>
|
||||
request<{ message: string }>(`/ha-sensors/${id}`, { method: 'DELETE' }),
|
||||
|
||||
// REST smart plug
|
||||
testRESTConnection: (url: string, method: string = 'GET', headers?: string | null) =>
|
||||
request<{ success: boolean; error: string | null }>('/smart-plugs/rest/test-connection', {
|
||||
@@ -5261,6 +5480,16 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
updateBatch: (id: number, data: PrintBatchUpdate) =>
|
||||
request<PrintBatch>(`/queue/batches/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
dispatchBatch: (id: number, data: PrintBatchDispatchRequest = {}) =>
|
||||
request<PrintBatch>(`/queue/batches/${id}/dispatch`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
ungroupBatch: (id: number) =>
|
||||
request<{ ungrouped_count: number; message: string }>(
|
||||
`/queue/batches/${id}/ungroup`,
|
||||
@@ -6462,6 +6691,50 @@ export const api = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ file_ids: fileIds, tag_ids: tagIds, action }),
|
||||
}),
|
||||
// ============ Variant groups (#671 / #2570) ============
|
||||
// "These files are the same job sliced for different printers." Consumed from
|
||||
// both ends: the queue picks a printer and needs the matching file, the File
|
||||
// Manager's print action has the printer and needs the same match.
|
||||
createVariantGroup: (
|
||||
members: { library_file_id: number; target_model?: string }[],
|
||||
name?: string,
|
||||
) =>
|
||||
request<VariantGroup>('/library/variant-groups', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ members, ...(name ? { name } : {}) }),
|
||||
}),
|
||||
getVariantGroup: (groupId: number) =>
|
||||
request<VariantGroup>(`/library/variant-groups/${groupId}`),
|
||||
/** Returns null when the file is not grouped, rather than throwing on the 404. */
|
||||
getVariantGroupForFile: async (fileId: number): Promise<VariantGroup | null> => {
|
||||
try {
|
||||
return await request<VariantGroup>(`/library/variant-groups/by-file/${fileId}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
updateVariantGroup: (groupId: number, body: { name?: string; member_file_ids?: number[] }) =>
|
||||
request<VariantGroup>(`/library/variant-groups/${groupId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
addVariantGroupMember: (
|
||||
groupId: number,
|
||||
libraryFileId: number,
|
||||
targetModel?: string,
|
||||
) =>
|
||||
request<VariantGroup>(`/library/variant-groups/${groupId}/members`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
library_file_id: libraryFileId,
|
||||
...(targetModel ? { target_model: targetModel } : {}),
|
||||
}),
|
||||
}),
|
||||
removeVariantGroupMember: (groupId: number, fileId: number) =>
|
||||
request<void>(`/library/variant-groups/${groupId}/members/${fileId}`, { method: 'DELETE' }),
|
||||
deleteVariantGroup: (groupId: number) =>
|
||||
request<void>(`/library/variant-groups/${groupId}`, { method: 'DELETE' }),
|
||||
|
||||
getLibraryFile: (id: number) => request<LibraryFile>(`/library/files/${id}`),
|
||||
uploadLibraryFile: async (
|
||||
file: File,
|
||||
@@ -6654,6 +6927,9 @@ export const api = {
|
||||
getGitHubBackupConfig: () =>
|
||||
request<GitHubBackupConfig | null>('/github-backup/config'),
|
||||
|
||||
getGitHubBackupCloudAccounts: () =>
|
||||
request<CloudAccountCounts>('/github-backup/cloud-accounts'),
|
||||
|
||||
saveGitHubBackupConfig: (config: GitHubBackupConfigCreate) =>
|
||||
request<GitHubBackupConfig>('/github-backup/config', {
|
||||
method: 'POST',
|
||||
@@ -7158,6 +7434,26 @@ export interface LibraryFileListItem {
|
||||
// legacy code path (or mock) that constructs a LibraryFileListItem without
|
||||
// it doesn't crash the renderer. Read sites use `file.tags ?? []`.
|
||||
tags?: LibraryTagSummary[];
|
||||
// Variant grouping (#671 / #2570). `variant_count` is the size of the whole
|
||||
// group, which may include files in other folders — never the number of
|
||||
// matching rows on screen. 0 when the file is not grouped.
|
||||
variant_group_id?: number | null;
|
||||
variant_count?: number;
|
||||
}
|
||||
|
||||
// Variant groups (#671 / #2570): the same job sliced for different printers.
|
||||
export interface VariantGroupMember {
|
||||
library_file_id: number;
|
||||
filename: string;
|
||||
target_model: string;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export interface VariantGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
/** In priority order — index 0 wins when several printers are free at once. */
|
||||
members: VariantGroupMember[];
|
||||
}
|
||||
|
||||
// Library tag catalog (#1268)
|
||||
@@ -7477,6 +7773,7 @@ export interface VirtualPrinterConfig {
|
||||
target_printer_id: number | null;
|
||||
auto_dispatch: boolean;
|
||||
queue_force_color_match: boolean;
|
||||
save_ams_mapping: boolean;
|
||||
gcode_injection: boolean;
|
||||
tailscale_disabled: boolean;
|
||||
bind_ip: string | null;
|
||||
@@ -7504,6 +7801,7 @@ export const multiVirtualPrinterApi = {
|
||||
target_printer_id?: number;
|
||||
auto_dispatch?: boolean;
|
||||
queue_force_color_match?: boolean;
|
||||
save_ams_mapping?: boolean;
|
||||
gcode_injection?: boolean;
|
||||
bind_ip?: string;
|
||||
remote_interface_ip?: string;
|
||||
@@ -7522,6 +7820,7 @@ export const multiVirtualPrinterApi = {
|
||||
target_printer_id?: number;
|
||||
auto_dispatch?: boolean;
|
||||
queue_force_color_match?: boolean;
|
||||
save_ams_mapping?: boolean;
|
||||
gcode_injection?: boolean;
|
||||
tailscale_disabled?: boolean;
|
||||
bind_ip?: string;
|
||||
|
||||
Reference in New Issue
Block a user