fix(queue): keep the filament override when a model job moves to one printer (issue #3133)

Switching an "Any P2S" job to a specific P2S cleared its filament
    override: "Specific Printer" empties the target model and the reset
    effect counted that as a model change. Printer mode also matched trays
    against the 3MF's colours, never sent the override, and left the old one
    on the row.

    The reset now compares against the last model actually targeted, so the
    switch keeps the override while a real model or plate change still clears
    it. Printer-mode tray matching (single, per-plate, multi-printer and the
    selector's per-printer editor) runs against the requirements with the
    overrides applied, mirroring the scheduler's _apply_filament_overrides; an
    entry naming the slot's own filament is not a swap and keeps its
    tray_info_idx. Printer-mode submits carry the user's overrides, and the
    create endpoint stores them for a printer-targeted job, so a dispatch-time
    recompute of an unresolved mapping looks for the same filament.

    Saving re-attaches the tray_info_idx an unchanged entry already had, so a
    virtual printer's force-colour PLA-variant pin (#2650) survives an edit in
    either assignment mode. The printer card's compatibility filter skips
    printer-targeted jobs: it mirrors the model scheduler, and hiding a job on
    filament would hide it from the printer it is going to run on.
This commit is contained in:
maziggy
2026-09-24 14:51:12 +02:00
95 changed files with 420 additions and 5496 deletions
+1
View File
@@ -9,6 +9,7 @@ All notable changes to Bambuddy will be documented in this file.
- **Every FTP session Bambuddy opens now records how it closed (#3009, reported by @grengojbo)** — the report traced a print completion that opened two FTP connections to the printer, deleted one file and then, as far as the log showed, did nothing else until the printer was powered off 21 minutes later, and concluded the connections were being left open. They were not: the post-print SD-card cleanup opens one connection per candidate filename and closes each in a `finally`, which a run against a real FTPS server confirms at the server end for both the delete and the 550 not-here case. The trouble is that nothing in the log could have said so. Neither the clean close nor the hard socket drop logged anything at any level, so a session closed properly and a socket genuinely abandoned produced the same output — none — and the only way to tell them apart was to read the source. Both now log one DEBUG line naming the printer, whether QUIT was acknowledged or the socket had to be dropped without it, why, and how long the session was held. Every connect in a debug log is now paired with a close, so the next person suspecting a leaked FTP connection can settle it from a support bundle rather than by inference. Nothing about the connection handling itself changed, and at default log level nothing new is printed. This does not explain the SD-card read/write error in that report or in #645; it only removes one theory from the list by making it checkable.
### Fixed
- **Moving a queued job from "Any P2S" to one P2S threw away its filament colour (#3133, reported by @bgrr74)** — a job queued for any printer of a model can print in a different filament from the one it was sliced with: brown in the 3MF, Bone White on the spool. Editing that job and switching it to a specific printer of the same model reset the choice, and the job went back to brown. The print dialog cleared overrides whenever the target model changed, and "Specific Printer" empties the target model, so the switch counted as a change. Clearing would not have been the whole problem anyway: in specific-printer mode the dialog matched trays against the 3MF's colours, never sent the override, and left the old one on the queue item, where it contradicted the trays the job would actually use. Now the override is kept through the switch, and the chosen printer's trays are matched against the requested filament, so Bone White picks the Bone White spool, and a printer without one shows the usual colour-mismatch warning for Bone White instead of quietly settling on brown. The job keeps the override on the queue item, so if the dialog could not resolve every tray, the scheduler's match at dispatch looks for the same colour. Moving the job to a different printer model, or picking a different plate, still clears the override, as before. A printer's queue card and badge no longer hide a job assigned to that printer because its override colour is not loaded. That check is for picking among printers of a model, which a job for one printer has already done. The same goes for a virtual printer's force-colour jobs sent to a specific printer, which the card used to hide the same way even though the scheduler dispatched them. Saving a virtual-printer job in the print dialog now keeps the PLA-variant pin (Basic, Matte or Silk) its force-colour overrides carry. Editing one queued for any printer of a model used to strip it.
- **Asking for 25 copies across two printer models queued exactly one (#3101, reported by @Leander-Vh)** — the reporter sliced one project for two printer types, selected both files in the File Manager, set the count and pressed Print. One item printed, the queue was empty behind it, and the Batches tab held nothing. Two features had met badly. Cross-model alternatives (#671) put several sliced files behind one queue slot, and a multi-plate file (#342) moves the run count out of the modal's single Quantity field onto a stepper beside each plate. The cross-model submit reads the field, not the steppers — and on a multi-plate file that field is not rendered at all, so it stayed at its initial 1 with nothing on screen able to change it. The modal read "19 runs in total" directly above a button that queued one. Cross-model no longer borrows the per-plate steppers: such a job picks its plate per candidate, down in the alternatives list, so there is only ever one number to give and the ordinary Quantity field is back. The plate selector above it is gone in that mode for the same reason — its choice never reached the request. It only decided which plate the filament panel described, which is why picking plate 3 for a candidate while plate 1 stayed ticked above it produced overrides computed from a plate the job would not print; that panel now follows the first candidate's own plate. The backend needed nothing for the count itself — it has always created one item per copy and given each its own candidate rows, so the same request now produces 25 jobs, each free to land on whichever of the two models frees up first. It did need a name: a cross-model job carries neither an archive nor a library file, because the candidates are the files, so both branches that name a batch missed and the order the reporter went looking for would have appeared in the Batches tab as "Batch". It is now named after its first candidate, the same file the print dialog names the job after.
- **A print from Bambu Studio archived under the name `plate_1` instead of its own (#3126, reported by @alex-2000-ac-ghetto)** — the reporter's X2D archived Bambu Studio prints with no thumbnail, no filament total and no layer count, while the same project sent from OrcaSlicer archived in full. Two separate things were happening. The first is the known internal-storage limitation: Bambu Studio filed the sliced file on the printer's eMMC (`"url": "brtc://emmc/..."`), which FTPS on port 990 does not serve, and the X2D does this like the H2 series and the P2S. Bambuddy's bounded probe then found a file of exactly that name on the card and downloaded it — but it was an earlier slice of the same project, plate 4, while the running print was plate 1. The guard added in #1204 caught the contradiction and refused it, which is right: archiving plate 4's thumbnail, filament and cost against this print is the swap #2957 removed. What it did next was not. Having rejected the file, it asked `swap_plate_suffix` for a name with the correct plate number and blanked the print's name entirely whenever that came back `None` — but `None` covers a name that carries no `- Plate N` suffix at all, and such a name holds no stale plate number to be wrong about. So a single-plate project lost the name Bambuddy was holding and the row fell through to the gcode_file path, titled `plate_1`. #1204's own premise is consecutive plates *of the same model*, so the project part of a lagging name is right either way; it is now kept as the archive's title, and only a suffix that really does point at the wrong plate is rewritten. Kept as the title and nothing else, though: a name the guard has just watched fetch another plate's 3MF still gets disowned everywhere a file is looked up by name. That includes the `_active_prints` keys, and it matters — the cover endpoint downloads the running print's 3MF under that same name for a thumbnail and offers the bytes to the fallback-recovery path, which checks that a candidate is a readable 3MF but never which plate it holds. Registering the row under the rejected name would have let it be filled in with the exact file this branch had just discarded. The archive is still thumbnail-less — nothing can read that eMMC file — but it carries the print's name again. Separately, the Archives banner and the connection diagnostic named only "H2-series and P2S" as the models that do this, in all 14 languages; an X2D owner reading either was told the explanation did not apply to them. All three now name the X2D too.
- **Linking a tag another spool already carries gave an answer nothing could act on, and a duplicate broke the request outright (#3110, reported by @Niko11111)** — the built-in inventory refused with "Tag UID already linked to another active spool", naming nobody, although the route had just loaded the conflicting spool row and was holding it when it wrote that sentence. Spoolman mode did name the spool, but only inside a different English sentence. Same operation, two answers, neither machine-readable, so a client had to parse prose to learn which spool to look at — and could only do it in one of the two inventory modes. Both now raise one structured 409, `{"code": "tag_already_linked", "message": ..., "spool_id": N, "field": "tag_uid" | "tray_uuid"}`, the shape Bambuddy already answers `insufficient_filament` and `printer_connection_failed` with. The SpoolBuddy kiosk reads it: a refused link showed a flat "Failed to assign spool" and now names the spool holding the tag, so the operator can walk to that spool instead of rescanning one that cannot link. The other half was worse than untidy. Nothing stops two active spools from carrying one tag — neither column has a unique index, `PATCH /inventory/spools/{id}` writes both with no conflict check, and `POST /inventory/spools/bulk` copies a single payload, tag included, into every row it creates — and the conflict lookup read its result with `scalar_one_or_none()`, which raises when two rows come back. That exception escaped the route into the authentication middleware’s fail-closed handler, so the caller was told "Authentication service temporarily unavailable" for a request that had nothing to do with authentication. The lookup is now ordered and takes the first row, the way the by-tag lookup earlier in the same file always has, and a duplicate yields a 409 naming the lowest id.
+15 -10
View File
@@ -906,29 +906,34 @@ async def add_to_queue(
# Extract filament types for model-based assignment (used by scheduler for validation)
required_filament_types = None
file_path = None
# Get file path from archive or library file
if archive:
file_path = settings.base_dir / archive.file_path
elif library_file:
lib_path = Path(library_file.file_path)
file_path = lib_path if lib_path.is_absolute() else settings.base_dir / library_file.file_path
if target_model_norm:
# Get file path from archive or library file
if archive:
file_path = settings.base_dir / archive.file_path
elif library_file:
lib_path = Path(library_file.file_path)
file_path = lib_path if lib_path.is_absolute() else settings.base_dir / library_file.file_path
if file_path and file_path.exists():
filament_types = _extract_filament_types_from_3mf(file_path, data.plate_id)
if filament_types:
required_filament_types = json.dumps(filament_types)
logger.info("Extracted filament types for model-based queue: %s", filament_types)
# If filament overrides are provided, update required_filament_types to match override types
# If filament overrides are provided, update required_filament_types to match override types.
# A specific-printer job keeps its overrides too (#3133): an override chosen for
# "Any P2S" survives the switch to one P2S in the print dialog, and when the
# dialog could not resolve every tray the scheduler recomputes the mapping at
# dispatch — against the 3MF's filament, unless the row still says otherwise.
# The type list below stays model-only; it gates which printer of a model is
# eligible, which a printer-targeted job has already settled.
filament_overrides_json = None
if data.filament_overrides and target_model_norm:
if data.filament_overrides and (target_model_norm or data.printer_id is not None):
plate_overrides = overrides_for_plate(data.filament_overrides, file_path, data.plate_id)
if plate_overrides:
filament_overrides_json = json.dumps(plate_overrides)
# Update required_filament_types from overrides so scheduler validates against overridden types
override_types = sorted({o["type"] for o in plate_overrides if "type" in o})
if override_types:
if override_types and target_model_norm:
# Merge with existing types (overrides may only cover some slots)
existing_types = set(json.loads(required_filament_types)) if required_filament_types else set()
# Replace types for overridden slots, keep others
@@ -636,6 +636,36 @@ class TestPrintQueueAPI:
result = response.json()
assert result["ams_mapping"] == [5, -1, 2, -1]
@pytest.mark.asyncio
@pytest.mark.integration
async def test_add_to_queue_keeps_overrides_on_a_specific_printer_job(
self, async_client: AsyncClient, printer_factory, archive_factory, db_session
):
"""An override picked for "Any P2S" survives the dialog's switch to one
P2S (#3133). The row must keep it: when the dialog could not resolve
every tray, the scheduler recomputes the mapping at dispatch, and without
the override it would match the 3MF's colour again. It used to be
dropped whenever the item had no target model.
"""
printer = await printer_factory()
archive = await archive_factory()
data = {
"printer_id": printer.id,
"archive_id": archive.id,
"filament_overrides": [{"slot_id": 1, "type": "PLA", "color": "#F5F5DC"}],
}
response = await async_client.post("/api/v1/queue/", json=data)
assert response.status_code == 200
result = response.json()
assert result["filament_overrides"] == [{"slot_id": 1, "type": "PLA", "color": "#F5F5DC"}]
# The type list gates which printer of a model may take the job; a job
# for one printer has no such choice left to make.
from backend.app.models.print_queue import PrintQueueItem
row = await db_session.get(PrintQueueItem, result["id"])
assert row.required_filament_types is None
@pytest.mark.asyncio
@pytest.mark.integration
async def test_queue_response_flags_saved_mapping_only_for_its_own_printer(
@@ -2272,3 +2272,238 @@ describe('PrintModal — per-plate quantity (#342)', () => {
expect(queued.every((q) => q.quantity === undefined)).toBe(true);
});
});
describe('PrintModal — override survives "Any model" -> "Specific Printer" (#3133)', () => {
const mockOnClose = vi.fn();
const PRINTERS = [
{ id: 1, name: 'Printer 01', model: 'P2S', ip_address: '192.168.1.101', enabled: true, is_active: true },
{ id: 2, name: 'Printer 02', model: 'X1C', ip_address: '192.168.1.102', enabled: true, is_active: true },
];
const BROWN = '#8B4513';
const BONE_WHITE = '#F5F5DC';
// The 3MF was sliced in brown; the user asked for Bone White.
const SLOT_1_BROWN = { slot_id: 1, type: 'PLA', color: BROWN, tray_info_idx: 'GFA00', used_grams: 50 };
type Patched = {
printer_id?: number | null;
target_model?: string | null;
ams_mapping?: number[] | null;
filament_overrides?: Array<{ slot_id: number; type: string; color: string }> | null;
};
let patched: Patched[];
let posted: Patched[];
const statusWith = (trays: Array<{ id: number; tray_type: string; tray_color: string }>) =>
HttpResponse.json({ connected: true, state: 'IDLE', ams: [{ id: 0, tray: trays }], vt_tray: [] });
beforeEach(() => {
vi.clearAllMocks();
patched = [];
posted = [];
server.use(
http.get('/api/v1/printers/', () => HttpResponse.json(PRINTERS)),
http.get('/api/v1/archives/:id/plates', () => HttpResponse.json({ is_multi_plate: false, plates: [] })),
http.get('/api/v1/archives/:id/filament-requirements', () => HttpResponse.json({ filaments: [SLOT_1_BROWN] })),
http.get('/api/v1/printers/available-filaments', () =>
HttpResponse.json([
{ type: 'PLA', color: BROWN, tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: null },
{ type: 'PLA', color: BONE_WHITE, tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: null },
]),
),
// Printer 01 has both colours loaded; brown is first, so a match against
// the 3MF picks tray 0 and a match against the override picks tray 1.
http.get('/api/v1/printers/:id/status', () =>
statusWith([
{ id: 0, tray_type: 'PLA', tray_color: '8B4513FF' },
{ id: 1, tray_type: 'PLA', tray_color: 'F5F5DCFF' },
]),
),
http.get('/api/v1/printers/:id/assignments', () => HttpResponse.json([])),
http.patch('/api/v1/queue/:id', async ({ request }) => {
patched.push((await request.json()) as Patched);
return HttpResponse.json({ id: 1, status: 'pending' });
}),
http.post('/api/v1/queue/', async ({ request }) => {
posted.push((await request.json()) as Patched);
return HttpResponse.json({ id: 1, status: 'pending' });
}),
);
});
const anyP2SItem = () =>
createMockQueueItem({
printer_id: null,
target_model: 'P2S',
filament_overrides: [{ slot_id: 1, type: 'PLA', color: BONE_WHITE, force_color_match: false }],
} as Partial<PrintQueueItem>);
const moveToPrinter01 = async (user: ReturnType<typeof userEvent.setup>) => {
await user.click(await screen.findByRole('button', { name: /specific printer/i }));
await user.click(await screen.findByText('Printer 01'));
};
const submit = async (user: ReturnType<typeof userEvent.setup>) => {
await user.click(document.querySelector('button[type="submit"]') as HTMLElement);
};
it('matches the chosen printer against the override and keeps it on the item', async () => {
const user = userEvent.setup();
render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={anyP2SItem()} onClose={mockOnClose} />);
await moveToPrinter01(user);
// Wait for the mapping to settle on the printer's trays before saving.
await waitFor(() => expect(screen.getByText(/filament mapping/i)).toBeInTheDocument());
await submit(user);
await waitFor(() => expect(patched).toHaveLength(1));
expect(patched[0].printer_id).toBe(1);
expect(patched[0].target_model).toBeNull();
// Tray 1 is the Bone White spool. Matching the 3MF would have taken tray 0.
expect(patched[0].ams_mapping).toEqual([1]);
expect(patched[0].filament_overrides).toEqual([
expect.objectContaining({ slot_id: 1, type: 'PLA', color: BONE_WHITE }),
]);
});
it('keeps the requested colour when the printer has no such spool', async () => {
server.use(
http.get('/api/v1/printers/:id/status', () =>
statusWith([
{ id: 0, tray_type: 'PLA', tray_color: '8B4513FF' },
{ id: 1, tray_type: 'PLA', tray_color: '000000FF' },
]),
),
);
const user = userEvent.setup();
render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={anyP2SItem()} onClose={mockOnClose} />);
await moveToPrinter01(user);
await waitFor(() => expect(screen.getByText(/filament mapping/i)).toBeInTheDocument());
await submit(user);
await waitFor(() => expect(patched).toHaveLength(1));
// Still asked for, so the scheduler's recompute at dispatch looks for it too
// instead of settling on the brown the 3MF was sliced with.
expect(patched[0].filament_overrides).toEqual([
expect.objectContaining({ slot_id: 1, color: BONE_WHITE }),
]);
});
it('still drops the override when the job moves to a different model', async () => {
const user = userEvent.setup();
render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={anyP2SItem()} onClose={mockOnClose} />);
const modelSelect = await waitFor(() => {
const select = screen
.getAllByRole('combobox')
.find((el) => (el as HTMLSelectElement).options[0]?.text === 'Select a model...');
if (!select) throw new Error('target model select not rendered');
return select as HTMLSelectElement;
});
// The override was picked from P2S's loaded filaments; X1C's are another list.
await user.selectOptions(modelSelect, 'X1C');
await submit(user);
await waitFor(() => expect(patched).toHaveLength(1));
expect(patched[0].target_model).toBe('X1C');
expect(patched[0].filament_overrides ?? null).toBeNull();
});
it("keeps a virtual printer's variant pin when its specific-printer job is saved", async () => {
// A VP with force colour on writes the 3MF's own filament back as an
// override, tray_info_idx included, to tell PLA variants apart (#2650). Two
// brown trays differ only by variant; the job was sliced for Matte (GFA01).
server.use(
http.get('/api/v1/archives/:id/filament-requirements', () =>
HttpResponse.json({ filaments: [{ ...SLOT_1_BROWN, tray_info_idx: 'GFA01' }] }),
),
http.get('/api/v1/printers/:id/status', () =>
HttpResponse.json({
connected: true,
state: 'IDLE',
ams: [{ id: 0, tray: [
{ id: 0, tray_type: 'PLA', tray_color: '8B4513FF', tray_info_idx: 'GFA00' },
{ id: 1, tray_type: 'PLA', tray_color: '8B4513FF', tray_info_idx: 'GFA01' },
] }],
vt_tray: [],
}),
),
);
const vpItem = createMockQueueItem({
printer_id: 1,
target_model: null,
filament_overrides: [
{ slot_id: 1, type: 'PLA', color: BROWN, tray_info_idx: 'GFA01', force_color_match: true },
],
} as Partial<PrintQueueItem>);
const user = userEvent.setup();
render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={vpItem} onClose={mockOnClose} />);
await waitFor(() => expect(screen.getByText(/filament mapping/i)).toBeInTheDocument());
await submit(user);
await waitFor(() => expect(patched).toHaveLength(1));
// Not treated as a swap: the matcher still pins the Matte tray...
expect(patched[0].ams_mapping).toEqual([1]);
// ...and the row keeps the pin rather than losing it on save.
expect(patched[0].filament_overrides).toEqual([
expect.objectContaining({ slot_id: 1, color: BROWN, tray_info_idx: 'GFA01', force_color_match: true }),
]);
});
it("keeps a virtual printer's variant pin when its any-model job is saved", async () => {
const vpItem = createMockQueueItem({
printer_id: null,
target_model: 'P2S',
filament_overrides: [
{ slot_id: 1, type: 'PLA', color: BROWN, tray_info_idx: 'GFA01', force_color_match: true },
],
} as Partial<PrintQueueItem>);
const user = userEvent.setup();
render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={vpItem} onClose={mockOnClose} />);
await waitFor(() => expect(screen.getByText('Filament Override')).toBeInTheDocument());
await submit(user);
await waitFor(() => expect(patched).toHaveLength(1));
expect(patched[0].target_model).toBe('P2S');
expect(patched[0].filament_overrides).toEqual([
expect.objectContaining({ slot_id: 1, tray_info_idx: 'GFA01', force_color_match: true }),
]);
});
it('carries an override picked in model mode into a specific-printer job when creating', async () => {
const user = userEvent.setup();
render(<PrintModal mode="create" archiveId={1} archiveName="Job" onClose={mockOnClose} />);
await user.click(await screen.findByRole('button', { name: /any model/i }));
const modelSelect = await waitFor(() => {
const select = screen
.getAllByRole('combobox')
.find((el) => (el as HTMLSelectElement).options[0]?.text === 'Select a model...');
if (!select) throw new Error('target model select not rendered');
return select as HTMLSelectElement;
});
await user.selectOptions(modelSelect, 'P2S');
const overrideSelect = await waitFor(() => {
const select = screen
.getAllByRole('combobox')
.find((el) => [...(el as HTMLSelectElement).options].some((o) => o.value === `PLA|${BONE_WHITE}`));
if (!select) throw new Error('override select not rendered');
return select as HTMLSelectElement;
});
await user.selectOptions(overrideSelect, `PLA|${BONE_WHITE}`);
await moveToPrinter01(user);
await waitFor(() => expect(screen.getByText(/filament mapping/i)).toBeInTheDocument());
await submit(user);
await waitFor(() => expect(posted).toHaveLength(1));
expect(posted[0].printer_id).toBe(1);
expect(posted[0].ams_mapping).toEqual([1]);
expect(posted[0].filament_overrides).toEqual([
expect.objectContaining({ slot_id: 1, type: 'PLA', color: BONE_WHITE }),
]);
});
});
@@ -176,3 +176,22 @@ describe('filterCompatibleQueueItems — force-color PLA variant (#2650)', () =>
expect(filterCompatibleQueueItems([noIdxJob], loadedTypes, loaded, variants)).toHaveLength(1);
});
});
describe('filterCompatibleQueueItems — printer-targeted jobs (#3133)', () => {
// A job moved from "Any P2S" to one P2S keeps its override. The printer is
// already chosen, so a colour it has not loaded must not hide the job from
// that printer's card — the scheduler maps the trays at dispatch.
const boneWhite = [{ slot_id: 1, type: 'PLA', color: '#F5F5DC', force_color_match: false }];
const loadedTypes = new Set(['PLA']);
const loadedBrown = new Set(['PLA:8b4513']);
it('keeps a specific-printer job whose override colour is not loaded', () => {
const item = { id: 1, printer_id: 3, filament_overrides: boneWhite } as unknown as PrintQueueItem;
expect(filterCompatibleQueueItems([item], loadedTypes, loadedBrown)).toHaveLength(1);
});
it('still filters the same job while it targets any printer of a model', () => {
const item = { id: 1, printer_id: null, target_model: 'P2S', filament_overrides: boneWhite } as unknown as PrintQueueItem;
expect(filterCompatibleQueueItems([item], loadedTypes, loadedBrown)).toHaveLength(0);
});
});
+3 -3
View File
@@ -2682,7 +2682,7 @@ export interface PrintQueueItemCreate {
printer_id?: number | null; // null = unassigned
target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
target_location?: string | null; // Target location filter (only used with target_model)
filament_overrides?: Array<{ slot_id: number; type: string; color: string; color_name?: string; force_color_match?: boolean }> | null;
filament_overrides?: Array<{ slot_id: number; type: string; color: string; color_name?: string; tray_info_idx?: string; force_color_match?: boolean }> | null;
archive_id?: number | null;
library_file_id?: number | null;
scheduled_time?: string | null;
@@ -2737,7 +2737,7 @@ export interface QueueVariantCreate {
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;
filament_overrides?: Array<{ slot_id: number; type: string; color: string; color_name?: string; tray_info_idx?: string; force_color_match?: boolean }> | null;
}
export interface PrintBatchCreate {
@@ -2776,7 +2776,7 @@ export interface PrintQueueItemUpdate {
printer_id?: number | null; // null = unassign
target_model?: string | null; // Target printer model (mutually exclusive with printer_id)
target_location?: string | null; // Target location filter (only used with target_model)
filament_overrides?: Array<{ slot_id: number; type: string; color: string; color_name?: string; force_color_match?: boolean }> | null;
filament_overrides?: Array<{ slot_id: number; type: string; color: string; color_name?: string; tray_info_idx?: string; force_color_match?: boolean }> | null;
position?: number;
scheduled_time?: string | null;
require_previous_success?: boolean;
+110 -20
View File
@@ -41,6 +41,39 @@ import type {
} from './types';
import { DEFAULT_PRINT_OPTIONS, DEFAULT_SCHEDULE_OPTIONS } from './types';
/** Same filament: type ignoring case, colour as RRGGBB ignoring `#`, case and alpha. */
function isSameFilament(a: { type: string; color: string }, b: { type: string; color: string }): boolean {
const hex = (c: string) => (c || '').replace('#', '').toLowerCase().slice(0, 6);
return (a.type || '').toUpperCase() === (b.type || '').toUpperCase() && hex(a.color) === hex(b.color);
}
/**
* The filament list as the tray matcher should see it: each overridden slot
* asks for the override's type and colour, not the 3MF's (#3133). Mirrors the
* scheduler's `_apply_filament_overrides` for a manual override — the 3MF's
* `tray_info_idx` names the replaced spool's SKU, so it is dropped and matching
* falls back to type + colour. An entry naming the slot's own filament is no
* swap — a virtual printer's force-colour entries are exactly that — so the
* slot keeps its idx and with it the PLA-variant pin (#2650). Slots with no
* override pass through unchanged.
*/
function withFilamentOverrides(
reqs: FilamentReqsData | undefined,
overrides: Record<number, { type: string; color: string }>,
): FilamentReqsData | undefined {
const isSwap = (f: FilamentReqsData['filaments'][number]) => {
const override = overrides[f.slot_id];
return !!override && !isSameFilament(override, f);
};
if (!reqs?.filaments || !reqs.filaments.some(isSwap)) return reqs;
return {
...reqs,
filaments: reqs.filaments.map((f) =>
isSwap(f) ? { ...f, type: overrides[f.slot_id].type, color: overrides[f.slot_id].color, tray_info_idx: '' } : f,
),
};
}
/**
* Unified PrintModal component that handles queue item creation and editing.
* - 'create': Create a print queue item from an archive or library file
@@ -460,6 +493,16 @@ export function PrintModal({
// Combine filament requirements from either source
const effectiveFilamentReqs = isLibraryFile ? libraryFilamentReqs : archiveFilamentReqs;
// What the tray matching works from. An override chosen in model mode stays
// in force when the job is moved to a specific printer (#3133), so that
// printer's trays are matched against the requested filament rather than the
// one the 3MF was sliced with. The override panel keeps the original list —
// it shows "sliced brown, print Bone White".
const mappingFilamentReqs = useMemo(
() => withFilamentOverrides(effectiveFilamentReqs, filamentOverrides),
[effectiveFilamentReqs, filamentOverrides],
);
// Fetch available filaments for model-based assignment (for filament override UI)
const { data: availableFilaments } = useQuery({
queryKey: ['available-filaments', targetModel, targetLocation],
@@ -540,7 +583,7 @@ export function PrintModal({
// Get AMS mapping from hook (only when single printer selected)
const { amsMapping } = useFilamentMapping(
effectiveFilamentReqs,
mappingFilamentReqs,
printerStatus,
manualMappings,
singlePrinterPreferLowest,
@@ -597,6 +640,16 @@ export function PrintModal({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedPlateIds, perPlateReqQueries.map((q) => q.dataUpdatedAt).join('|')]);
// Per-plate twin of `mappingFilamentReqs`: slot ids are global to the file, so
// one override applies to every plate that prints that slot (#3133).
const mappingPerPlateReqs = useMemo(() => {
const byPlate = new Map<number, FilamentReqsData>();
for (const [plateId, reqs] of perPlateReqs) {
byPlate.set(plateId, withFilamentOverrides(reqs, filamentOverrides) ?? reqs);
}
return byPlate;
}, [perPlateReqs, filamentOverrides]);
// Manual slot overrides are per plate: slot 3 of plate 1 and slot 3 of plate 2
// are different prints and may want different trays.
const [manualMappingsByPlate, setManualMappingsByPlate] = useState<Record<number, Record<number, number>>>({});
@@ -629,7 +682,7 @@ export function PrintModal({
const inventoryByTrayId = inventoryByTrayIdPerPrinter.get(effectivePrinterId);
for (const plateId of selectedPlateIds) {
const reqs = perPlateReqs.get(plateId);
const reqs = mappingPerPlateReqs.get(plateId);
if (!reqs) continue;
const comparison = buildFilamentComparison(
reqs,
@@ -648,7 +701,7 @@ export function PrintModal({
printerStatus,
inventoryByTrayIdPerPrinter,
selectedPlateIds,
perPlateReqs,
mappingPerPlateReqs,
manualMappingsByPlate,
singlePrinterPreferLowest,
selectedPrinters.length,
@@ -658,7 +711,7 @@ export function PrintModal({
const multiPrinterMapping = useMultiPrinterFilamentMapping(
selectedPrinters,
printers,
effectiveFilamentReqs,
mappingFilamentReqs,
manualMappings,
perPrinterConfigs,
setPerPrinterConfigs,
@@ -716,18 +769,27 @@ export function PrintModal({
}
}, [mode, selectedPrinters, selectedPlate, initialPrinterIds, initialPlateId]);
// Clear filament overrides when target model or plate changes (but not on initial mount for edit mode)
// Clear filament overrides when target model or plate changes (but not on initial mount for edit mode).
// `prevTargetModel` is the last model actually targeted, so it skips nulls:
// "Any P2S" -> "Specific Printer" empties targetModel without naming another
// model, and the override is the job's filament, not a tray on some printer —
// it survives the switch and is matched against the chosen printer (#3133).
// Going P2S -> (none) -> X1C still compares P2S with X1C and clears.
const [prevTargetModel, setPrevTargetModel] = useState(targetModel);
const [prevPlateForOverrides, setPrevPlateForOverrides] = useState(selectedPlate);
useEffect(() => {
if (targetModel !== prevTargetModel || selectedPlate !== prevPlateForOverrides) {
setPrevTargetModel(targetModel);
setPrevPlateForOverrides(selectedPlate);
// Don't clear on initial render in edit mode (values are initialized from queueItem)
if (mode !== 'edit-queue-item' || prevTargetModel !== null) {
setFilamentOverrides({});
setForceColorMatch({});
}
const modelChanged = targetModel !== null && targetModel !== prevTargetModel;
const plateChanged = selectedPlate !== prevPlateForOverrides;
if (!modelChanged && !plateChanged) return;
if (modelChanged) setPrevTargetModel(targetModel);
if (plateChanged) setPrevPlateForOverrides(selectedPlate);
// A first model after none is a choice, not a change: nothing was picked
// against another model's filaments. That also covers the initial render in
// edit mode, where the values are initialized from queueItem.
if (modelChanged && !plateChanged && prevTargetModel === null) return;
if (mode !== 'edit-queue-item' || prevTargetModel !== null) {
setFilamentOverrides({});
setForceColorMatch({});
}
}, [targetModel, selectedPlate, prevTargetModel, prevPlateForOverrides, mode]);
@@ -1016,8 +1078,22 @@ export function PrintModal({
// Convert filament overrides from Record to array format for API.
// Include all slots that either have a user override or have force_color_match enabled
// (which is the default for model-based assignment).
// The dialog keeps an override as type + colour only, so an entry that comes
// back unchanged gets the variant id the item already carried re-attached. A
// virtual printer writes force-colour entries with the 3MF's tray_info_idx to
// tell Basic, Matte and Silk PLA apart (#2650); saving the item — which a
// specific-printer edit now does for the overrides too (#3133) — must not
// quietly drop that pin. A changed entry is a swap and has no idx to keep.
const storedOverrideBySlot = new Map(
(mode === 'edit-queue-item' ? queueItem?.filament_overrides ?? [] : []).map((o) => [o.slot_id, o]),
);
const storedVariantFor = (slotId: number, type: string, color: string) => {
const stored = storedOverrideBySlot.get(slotId);
return stored?.tray_info_idx && isSameFilament(stored, { type, color }) ? { tray_info_idx: stored.tray_info_idx } : {};
};
const buildFilamentOverridesArray = (reqs: FilamentReqsData | undefined) => {
const entries: Array<{ slot_id: number; type: string; color: string; color_name: string; force_color_match: boolean }> = [];
const entries: Array<{ slot_id: number; type: string; color: string; color_name: string; tray_info_idx?: string; force_color_match: boolean }> = [];
// Process all slots from filament requirements (to capture force_color_match defaults)
if (reqs?.filaments) {
@@ -1029,7 +1105,7 @@ export function PrintModal({
// Include slot if user changed the filament OR force_color_match is enabled
if (userOverride || isForceColor) {
entries.push({ slot_id: req.slot_id, type: effectiveType, color: effectiveColor, color_name: getColorName(effectiveColor), force_color_match: isForceColor });
entries.push({ slot_id: req.slot_id, type: effectiveType, color: effectiveColor, color_name: getColorName(effectiveColor), ...storedVariantFor(req.slot_id, effectiveType, effectiveColor), force_color_match: isForceColor });
}
}
} else {
@@ -1037,7 +1113,7 @@ export function PrintModal({
for (const [slotId, { type, color }] of Object.entries(filamentOverrides)) {
const id = parseInt(slotId, 10);
const isForceColor = forceColorMatch[id] ?? false;
entries.push({ slot_id: id, type, color, color_name: getColorName(color), force_color_match: isForceColor });
entries.push({ slot_id: id, type, color, color_name: getColorName(color), ...storedVariantFor(id, type, color), force_color_match: isForceColor });
}
}
@@ -1057,6 +1133,16 @@ export function PrintModal({
? buildFilamentOverridesArray(perPlateReqs.get(plateId))
: filamentOverridesArray;
// A specific-printer job carries only the slots the user actually changed
// (#3133): the tray mapping was matched against them, and they are what the
// scheduler needs if it has to recompute that mapping at dispatch. The
// force-colour flags on their own stay behind — printer mode never sent
// them, and changing that is not what this is for.
const printerOverridesForPlate = (plateId: number | null) => {
const entries = overridesForPlate(plateId)?.filter((o) => filamentOverrides[o.slot_id]);
return entries && entries.length > 0 ? entries : undefined;
};
// Cross-model alternatives (#671): ONE item carrying a candidate per file,
// in the order the user arranged. This returns before the plate/printer
// fan-out below because it deliberately fans out to nothing — the whole
@@ -1175,7 +1261,7 @@ export function PrintModal({
printer_id: assignmentMode === 'printer' ? printerId : null,
target_model: assignmentMode === 'model' ? targetModel : null,
target_location: assignmentMode === 'model' ? targetLocation : null,
filament_overrides: assignmentMode === 'model' ? overridesForPlate(plateId) : undefined,
filament_overrides: assignmentMode === 'model' ? overridesForPlate(plateId) : printerOverridesForPlate(plateId),
// Use library_file_id for library files, archive_id for archives
archive_id: isLibraryFile ? undefined : archiveId,
library_file_id: isLibraryFile ? libraryFileId : undefined,
@@ -1280,6 +1366,10 @@ export function PrintModal({
printer_id: printerId,
target_model: null,
target_location: null,
// null, not undefined: omitting the field left a model job's
// overrides on the row after it moved to a printer, whatever the
// user did with them here (#3133).
filament_overrides: printerOverridesForPlate(plateId) ?? null,
require_previous_success: scheduleOptions.requirePreviousSuccess,
auto_off_after: scheduleOptions.autoOffAfter,
gcode_injection: scheduleOptions.gcodeInjection,
@@ -1667,7 +1757,7 @@ export function PrintModal({
// fan-out across printers ships no mapping at all (the scheduler maps
// each plate against the printer it picks), so the editor would be
// collecting tray choices it then throws away. Withhold its input.
filamentReqs={isMultiPlateSelection ? undefined : effectiveFilamentReqs}
filamentReqs={isMultiPlateSelection ? undefined : mappingFilamentReqs}
onAutoConfigurePrinter={multiPrinterMapping.autoConfigurePrinter}
onUpdatePrinterConfig={multiPrinterMapping.updatePrinterConfig}
assignmentMode={assignmentMode}
@@ -1767,7 +1857,7 @@ export function PrintModal({
{showFilamentMapping && !archiveDataMissing && selectedPrinters.length === 1 && (
<FilamentMapping
printerId={effectivePrinterId!}
filamentReqs={effectiveFilamentReqs}
filamentReqs={mappingFilamentReqs}
manualMappings={manualMappings}
onManualMappingChange={setManualMappings}
onEstimatedCostChange={setEstimatedCost}
@@ -1790,7 +1880,7 @@ export function PrintModal({
own print with its own slots, so it gets its own AMS mapping. */}
{showPerPlateFilamentMapping && !archiveDataMissing && selectedPlateIds.map((plateId) => {
const plate = plates.find((p) => p.index === plateId);
const plateReqs = perPlateReqs.get(plateId);
const plateReqs = mappingPerPlateReqs.get(plateId);
if (!plateReqs) return null;
return (
<FilamentMapping
+7
View File
@@ -95,6 +95,13 @@ export function filterCompatibleQueueItems(
loadedVariants?: Set<string>
): PrintQueueItem[] {
return items.filter(item => {
// A job bound to one printer has no printer left to choose, and the backend
// never gates it on filament: the scheduler maps its trays at dispatch. Such
// a job can carry overrides — one moved from "Any P2S" to a specific P2S
// keeps its colour (#3133) — and filtering on them would hide a job that is
// going to run from the card of the printer it is going to run on.
if (item.printer_id != null) return true;
// Type check: all required filament types must be loaded
if (item.required_filament_types && item.required_filament_types.length > 0 && loadedFilamentTypes !== undefined) {
if (!item.required_filament_types.every((t: string) => loadedFilamentTypes.has(t.toUpperCase()))) {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 50">
<!-- Left wire: horizontal from left edge, then down to extruder left inlet -->
<line x1="0" y1="0" x2="10" y2="0" stroke="#909090" stroke-width="2" />
<line x1="10" y1="0" x2="10" y2="50" stroke="#909090" stroke-width="2" />
<!-- Right wire: horizontal from right edge, then down to extruder right inlet -->
<line x1="40" y1="0" x2="30" y2="0" stroke="#909090" stroke-width="2" />
<line x1="30" y1="0" x2="30" y2="50" stroke="#909090" stroke-width="2" />
</svg>

Before

Width:  |  Height:  |  Size: 537 B

-17
View File
@@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 50">
<!-- Vertical lines from slots down to horizontal bar -->
<line x1="28" y1="0" x2="28" y2="14" stroke="#909090" stroke-width="2" />
<line x1="82" y1="0" x2="82" y2="14" stroke="#909090" stroke-width="2" />
<line x1="138" y1="0" x2="138" y2="14" stroke="#909090" stroke-width="2" />
<line x1="192" y1="0" x2="192" y2="14" stroke="#909090" stroke-width="2" />
<!-- Horizontal bar across all slots -->
<line x1="28" y1="14" x2="192" y2="14" stroke="#909090" stroke-width="2" />
<!-- Center hub box -->
<rect x="96" y="8" width="28" height="12" rx="2" fill="#c0c0c0" stroke="#909090" stroke-width="1" />
<!-- Wire from hub: down, then right to edge (at same level as hub horizontal bar) -->
<line x1="110" y1="20" x2="110" y2="35" stroke="#909090" stroke-width="2" />
<line x1="110" y1="35" x2="220" y2="35" stroke="#909090" stroke-width="2" />
</svg>

Before

Width:  |  Height:  |  Size: 937 B

-17
View File
@@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 50">
<!-- Vertical lines from slots down to horizontal bar -->
<line x1="28" y1="0" x2="28" y2="14" stroke="#909090" stroke-width="2" />
<line x1="82" y1="0" x2="82" y2="14" stroke="#909090" stroke-width="2" />
<line x1="138" y1="0" x2="138" y2="14" stroke="#909090" stroke-width="2" />
<line x1="192" y1="0" x2="192" y2="14" stroke="#909090" stroke-width="2" />
<!-- Horizontal bar across all slots -->
<line x1="28" y1="14" x2="192" y2="14" stroke="#909090" stroke-width="2" />
<!-- Center hub box -->
<rect x="96" y="8" width="28" height="12" rx="2" fill="#c0c0c0" stroke="#909090" stroke-width="1" />
<!-- Wire from hub: down, then left to edge (at same level as hub horizontal bar) -->
<line x1="110" y1="20" x2="110" y2="35" stroke="#909090" stroke-width="2" />
<line x1="0" y1="35" x2="110" y2="35" stroke="#909090" stroke-width="2" />
</svg>

Before

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m15 12h-6c-1.103 0-2 .897-2 2v4c0 1.103.897 2 2 2h6c1.103 0 2-.897 2-2v-4c0-1.103-.897-2-2-2zm1 6c0 .552-.448 1-1 1h-6c-.551 0-1-.448-1-1v-4c0-.552.449-1 1-1h6c.552 0 1 .448 1 1zm-2.5-2.5c0 .276-.224.5-.5.5h-2c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h2c.276 0 .5.224.5.5zm6-13.5h-1.5v-1.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v1.5h-10v-1.5c0-.276-.224-.5-.5-.5s-.5.224-.5.5v1.5h-1.5c-2.481 0-4.5 2.019-4.5 4.5v13c0 2.481 2.019 4.5 4.5 4.5h15c2.481 0 4.5-2.019 4.5-4.5v-13c0-2.481-2.019-4.5-4.5-4.5zm-15 1h15c1.93 0 3.5 1.57 3.5 3.5v1.5h-22v-1.5c0-1.93 1.57-3.5 3.5-3.5zm15 20h-15c-1.93 0-3.5-1.57-3.5-3.5v-10.5h22v10.5c0 1.93-1.57 3.5-3.5 3.5z"/></svg>

Before

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

-51
View File
@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 511.999 511.999" style="enable-background:new 0 0 511.999 511.999;" xml:space="preserve">
<g>
<g>
<path d="M508.745,246.041c-4.574-6.257-113.557-153.206-252.748-153.206S7.818,239.784,3.249,246.035
c-4.332,5.936-4.332,13.987,0,19.923c4.569,6.257,113.557,153.206,252.748,153.206s248.174-146.95,252.748-153.201
C513.083,260.028,513.083,251.971,508.745,246.041z M255.997,385.406c-102.529,0-191.33-97.533-217.617-129.418
c26.253-31.913,114.868-129.395,217.617-129.395c102.524,0,191.319,97.516,217.617,129.418
C447.361,287.923,358.746,385.406,255.997,385.406z"/>
</g>
</g>
<g>
<g>
<path d="M255.997,154.725c-55.842,0-101.275,45.433-101.275,101.275s45.433,101.275,101.275,101.275
s101.275-45.433,101.275-101.275S311.839,154.725,255.997,154.725z M255.997,323.516c-37.23,0-67.516-30.287-67.516-67.516
s30.287-67.516,67.516-67.516s67.516,30.287,67.516,67.516S293.227,323.516,255.997,323.516z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><g fill="rgb(0,0,0)"><path d="m6.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m12.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m18.3 3.9c.54-.4 1.2-.9 1.2-1.9 0-.28-.22-.5-.5-.5s-.5.22-.5.5c0 .48-.29.71-.8 1.1-.53.4-1.2.9-1.2 1.9s.67 1.5 1.2 1.9c.51.38.8.62.8 1.1s-.29.72-.8 1.1c-.54.4-1.2.9-1.2 1.9 0 .28.22.5.5.5s.5-.22.5-.5c0-.48.29-.72.8-1.1.54-.4 1.2-.9 1.2-1.9s-.67-1.5-1.2-1.9c-.51-.38-.8-.62-.8-1.1s.29-.72.8-1.1z"/><path d="m22 13.5c-1.07 0-1.61.65-2.05 1.18-.44.52-.71.82-1.29.82s-.85-.3-1.29-.82c-.44-.53-.98-1.18-2.05-1.18s-1.61.65-2.05 1.18c-.44.52-.71.82-1.28.82s-.85-.3-1.28-.82c-.44-.53-.98-1.18-2.05-1.18s-1.61.65-2.05 1.18c-.44.52-.71.82-1.28.82s-.84-.3-1.28-.82c-.44-.53-.98-1.18-2.05-1.18-.28 0-.5.22-.5.5s.22.5.5.5c.57 0 .84.3 1.28.82.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.28-.82s.85.3 1.28.82c.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.28-.82s.85.3 1.29.82c.44.53.98 1.18 2.05 1.18s1.61-.65 2.05-1.18c.44-.52.71-.82 1.29-.82.28 0 .5-.22.5-.5s-.22-.5-.5-.5z"/><path d="m21 18.5h-18c-.83 0-1.5.67-1.5 1.5v1c0 .83.67 1.5 1.5 1.5h18c.83 0 1.5-.67 1.5-1.5v-1c0-.83-.67-1.5-1.5-1.5zm.5 2.5c0 .28-.22.5-.5.5h-18c-.28 0-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5h18c.28 0 .5.22.5.5z"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-44
View File
@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 476.912 476.912" style="enable-background:new 0 0 476.912 476.912;" xml:space="preserve">
<g>
<g>
<path d="M461.776,209.408L249.568,4.52c-6.182-6.026-16.042-6.026-22.224,0L15.144,209.4c-3.124,3.015-4.888,7.17-4.888,11.512
c0,8.837,7.164,16,16,16h28.2v224c0,8.837,7.163,16,16,16h112c8.837,0,16-7.163,16-16v-128h80v128c0,8.837,7.163,16,16,16h112
c8.837,0,16-7.163,16-16v-224h28.2c4.338,0,8.489-1.761,11.504-4.88C468.301,225.678,468.129,215.549,461.776,209.408z
M422.456,220.912c-8.837,0-16,7.163-16,16v224h-112v-128c0-8.837-7.163-16-16-16h-80c-8.837,0-16,7.163-16,16v128h-112v-224
c0-8.837-7.163-16-16-16h-28.2l212.2-204.88l212.28,204.88H422.456z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-6
View File
@@ -1,6 +0,0 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.4759 7.06822H13.7562L8.74263 11.2268C8.514 11.416 8.10431 11.416 7.87554 11.227C7.87545 11.2269 7.87536 11.2268 7.87528 11.2268L2.86167 7.06822H4.142C4.54877 7.06822 4.93613 6.94338 5.23267 6.71974C5.52893 6.49633 5.76004 6.14967 5.76004 5.72506V1.84316C5.76004 1.80403 5.78049 1.72911 5.88953 1.64688C5.99827 1.56488 6.16993 1.5 6.37809 1.5H10.2398C10.448 1.5 10.6196 1.56488 10.7284 1.64688C10.8374 1.72911 10.8579 1.80403 10.8579 1.84316V5.72506C10.8579 6.14967 11.089 6.49633 11.3852 6.71974C11.6818 6.94338 12.0691 7.06822 12.4759 7.06822ZM2.36979 7.09452C2.36773 7.09555 2.36658 7.096 2.36652 7.09597C2.36645 7.09594 2.36748 7.09542 2.36979 7.09452ZM14.2475 7.09456C14.2498 7.09545 14.2508 7.09596 14.2507 7.096C14.2507 7.09603 14.2495 7.09558 14.2475 7.09456Z" stroke="#6B6B6B"/>
<path d="M3.80389 10.668C3.58699 10.7742 3.42822 10.9007 3.42822 11.0895C3.42822 11.673 4.95994 11.673 4.95994 12.2548C4.95994 12.8383 3.42822 12.8383 3.42822 13.42C3.42822 14.0035 4.95994 14.0035 4.95994 14.587C4.95994 15.1704 3.42822 15.1704 3.42822 15.7539" stroke="#6B6B6B" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M8.63467 14.1348C8.88288 14.2477 9.07518 14.381 9.07518 14.5867C9.07518 15.1702 7.54346 15.1702 7.54346 15.7536" stroke="#6B6B6B" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M11.893 11.4316C12.3223 11.7065 13.1899 11.8161 13.1899 12.2546C13.1899 12.838 11.6582 12.838 11.6582 13.4198C11.6582 14.0033 13.1899 14.0033 13.1899 14.5867C13.1899 15.1702 11.6582 15.1702 11.6582 15.7537" stroke="#6B6B6B" stroke-miterlimit="10" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg width="36" height="54" viewBox="0 0 36 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.8131 0.00537678C18.4463 -0.150913 20.3648 3.14642 20.8264 3.84781C25.4187 10.816 35.3089 26.9368 35.9383 34.8694C37.4182 53.5822 11.882 61.3357 2.53721 45.3789C-1.73471 38.0791 0.016016 32.2049 3.178 25.0232C6.99221 16.3662 12.6411 7.90372 17.8131 0.00537678ZM18.3738 7.24807L17.5881 7.48441C14.4452 12.9431 10.917 18.2341 8.19369 23.9368C4.6808 31.29 1.18317 38.5479 7.69403 45.5657C17.3058 55.9228 34.9847 46.8808 31.4604 32.8681C29.2558 24.0969 22.4207 15.2913 18.3776 7.24807H18.3738Z" fill="#D0D0D0"/>
<path d="M8 46C12 48 24 48 28 46C26 50 22 52 18 52C14 52 10 50 8 46Z" fill="#1F8FEB"/>
</svg>

Before

Width:  |  Height:  |  Size: 710 B

-4
View File
@@ -1,4 +0,0 @@
<svg width="36" height="54" viewBox="0 0 36 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.9625 4.48059L4.77216 26.3154L2.08228 40.2175L10.0224 50.8414H23.1594L33.3246 42.1693V30.2455L17.9625 4.48059Z" fill="#1F8FEB"/>
<path d="M17.7948 0.00537678C18.4273 -0.150913 20.3438 3.14642 20.8048 3.84781C25.3921 10.816 35.2715 26.9368 35.9001 34.8694C37.3784 53.5822 11.8702 61.3357 2.53562 45.3789C-1.73163 38.0829 0.0133678 32.2087 3.1757 25.027C6.98574 16.3662 12.6284 7.90372 17.7948 0.00537678ZM18.3549 7.24807L17.57 7.48441C14.4306 12.9431 10.9063 18.2341 8.1859 23.9368C4.67686 31.29 1.18305 38.5479 7.68679 45.5657C17.2881 55.9228 34.9476 46.8808 31.4271 32.8681C29.2249 24.0969 22.3974 15.2913 18.3587 7.24807H18.3549Z" fill="#D0D0D0"/>
</svg>

Before

Width:  |  Height:  |  Size: 765 B

-4
View File
@@ -1,4 +0,0 @@
<svg width="35" height="53" viewBox="0 0 35 53" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.3165 0.00379674C17.932 -0.149588 19.7971 3.08645 20.2458 3.77481C24.7103 10.6135 34.3251 26.4346 34.937 34.2198C36.3757 52.5848 11.5505 60.1942 2.46584 44.534C-1.68714 37.3735 0.0148377 31.6085 3.08879 24.5603C6.79681 16.0605 12.2884 7.75907 17.3165 0.00379674ZM17.8615 7.11561L17.0977 7.34755C14.0423 12.7048 10.6124 17.8974 7.96483 23.4941C4.54975 30.7107 1.14949 37.8337 7.47908 44.721C16.8233 54.8856 34.01 46.0117 30.5838 32.2595C28.4405 23.6512 21.7957 15.0093 17.8652 7.11561H17.8615Z" fill="#D0D0D0"/>
<path d="M5.03547 30.112C9.64453 30.4936 11.632 35.7985 16.4154 35.791C19.6339 35.7873 20.2161 33.2283 22.3853 31.6197C31.6776 24.7286 33.5835 37.4894 27.9881 44.4254C18.1878 56.5653 -1.16063 44.6013 5.03917 30.1158L5.03547 30.112Z" fill="#1F8FEB"/>
</svg>

Before

Width:  |  Height:  |  Size: 876 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 98 KiB

-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
<path d="m6.443,4.08L4.304.567,5.157.048l2.14,3.513-.854.52Zm13.557,7.92c0,2.323-1.01,4.528-2.771,6.051-.781.674-1.229,1.641-1.229,2.653v3.296h-8v-3.295c0-1.007-.456-1.982-1.252-2.675-2.062-1.796-3.058-4.497-2.661-7.227.512-3.521,3.457-6.36,7.003-6.753,2.307-.256,4.527.45,6.245,1.987,1.693,1.517,2.665,3.689,2.665,5.962Zm-5,8.704c0-.239.04-.471.077-.704h-6.156c.038.233.078.467.078.705v2.295h6v-2.296Zm4-8.704c0-1.988-.85-3.89-2.332-5.217-1.502-1.344-3.438-1.964-5.469-1.738-3.1.343-5.675,2.825-6.122,5.903-.348,2.391.522,4.757,2.327,6.328.553.481.963,1.076,1.234,1.724h2.861v-5.551c-1.14-.232-2-1.242-2-2.449h1c0,.827.673,1.5,1.5,1.5s1.5-.673,1.5-1.5h1c0,1.208-.86,2.217-2,2.449v5.551h2.856c.268-.645.672-1.234,1.218-1.706,1.542-1.332,2.426-3.262,2.426-5.294Zm.696-11.433l-.854-.52-2.14,3.513.854.52,2.14-3.513Zm3.86,4.342l-3.536,1.597.412.912,3.536-1.597-.412-.912ZM.031,5.821l3.536,1.597.412-.912L.443,4.909l-.412.912Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-12
View File
@@ -1,12 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<!-- Generator: imaengine 6.0 -->
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" viewBox="0,0,512,512" style="enable-background:new 0 0 512 512;" version="1.1">
<defs/>
<g id="layer0">
<path d="M-0.00100857,91.001L-0.000996768,361.001C-0.000995864,381.679 16.821,398.501 37.499,398.501L154.393,398.501L212.196,456.305C213.603,457.711 215.51,458.501 217.5,458.501L277.5,458.501C281.643,458.501 285,455.144 285,451.001L285,428.501L334.394,428.501L362.197,456.305C363.604,457.711 365.511,458.501 367.501,458.501L474.501,458.501C495.179,458.501 512.001,441.679 512.001,421.001L512.001,91.001C512.001,70.323 495.179,53.501 474.501,53.501L37.501,53.501C16.821,53.501 -0.00100947,70.323 -0.00100857,91.001L-0.00100857,91.001ZM496.999,91.001L496.999,421.001C496.999,433.407 486.905,443.501 474.499,443.501L436.999,443.501L436.999,121.001C436.999,116.858 433.642,113.501 429.499,113.501C425.356,113.501 421.999,116.858 421.999,121.001L421.999,443.501L370.605,443.501L342.802,415.697C341.395,414.291 339.488,413.501 337.498,413.501L277.498,413.501C273.355,413.501 269.998,416.858 269.998,421.001L269.998,443.501L220.604,443.501L162.801,385.697C161.394,384.291 159.487,383.501 157.497,383.501L37.497,383.501C25.091,383.501 14.997,373.407 14.997,361.001L14.997,91.001C14.997,78.595 25.091,68.501 37.497,68.501L421.999,68.501L421.999,91.001C421.999,95.144 425.356,98.501 429.499,98.501C433.642,98.501 436.999,95.144 436.999,91.001L436.999,68.501L474.499,68.501C486.905,68.501 496.999,78.595 496.999,91.001L496.999,91.001Z" fill="#000000"/>
<path d="M29.999,316.001L29.999,361.001C29.999,365.144 33.356,368.501 37.499,368.501L157.499,368.501C161.642,368.501 164.999,365.144 164.999,361.001L164.999,316.001C164.999,311.858 161.642,308.501 157.499,308.501L37.499,308.501C33.356,308.501 29.999,311.858 29.999,316.001L29.999,316.001ZM149.999,323.501L149.999,353.501L44.999,353.501L44.999,323.501L149.999,323.501Z" fill="#000000"/>
<path d="M29.999,241.001L29.999,286.001C29.999,290.144 33.356,293.501 37.499,293.501L157.499,293.501C161.642,293.501 164.999,290.144 164.999,286.001L164.999,241.001C164.999,236.858 161.642,233.501 157.499,233.501L37.499,233.501C33.356,233.501 29.999,236.858 29.999,241.001L29.999,241.001ZM149.999,248.501L149.999,278.501L44.999,278.501L44.999,248.501L149.999,248.501Z" fill="#000000"/>
<path d="M29.999,166.001L29.999,211.001C29.999,215.144 33.356,218.501 37.499,218.501L157.499,218.501C161.642,218.501 164.999,215.144 164.999,211.001L164.999,166.001C164.999,161.858 161.642,158.501 157.499,158.501L37.499,158.501C33.356,158.501 29.999,161.858 29.999,166.001L29.999,166.001ZM149.999,173.501L149.999,203.501L44.999,203.501L44.999,173.501L149.999,173.501Z" fill="#000000"/>
<path d="M157.499,83.501L37.499,83.501C33.356,83.501 29.999,86.858 29.999,91.001L29.999,136.001C29.999,140.144 33.356,143.501 37.499,143.501L157.499,143.501C161.642,143.501 164.999,140.144 164.999,136.001L164.999,91.001C164.999,86.858 161.642,83.501 157.499,83.501L157.499,83.501ZM149.999,98.501L149.999,128.501L44.999,128.501L44.999,98.501L149.999,98.501Z" fill="#000000"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m22 11a1 1 0 0 0 -1 1 9 9 0 1 1 -9-9 8.9 8.9 0 0 1 4.42 1.166l-1.127 1.127a1 1 0 0 0 .707 1.707h4a1 1 0 0 0 1-1v-4a1 1 0 0 0 -1.707-.707l-1.411 1.407a10.9 10.9 0 0 0 -5.882-1.7 11 11 0 1 0 11 11 1 1 0 0 0 -1-1z"/></svg>

Before

Width:  |  Height:  |  Size: 289 B

-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m272.066 512h-32.133c-25.989 0-47.134-21.144-47.134-47.133v-10.871c-11.049-3.53-21.784-7.986-32.097-13.323l-7.704 7.704c-18.659 18.682-48.548 18.134-66.665-.007l-22.711-22.71c-18.149-18.129-18.671-48.008.006-66.665l7.698-7.698c-5.337-10.313-9.792-21.046-13.323-32.097h-10.87c-25.988 0-47.133-21.144-47.133-47.133v-32.134c0-25.989 21.145-47.133 47.134-47.133h10.87c3.531-11.05 7.986-21.784 13.323-32.097l-7.704-7.703c-18.666-18.646-18.151-48.528.006-66.665l22.713-22.712c18.159-18.184 48.041-18.638 66.664.006l7.697 7.697c10.313-5.336 21.048-9.792 32.097-13.323v-10.87c0-25.989 21.144-47.133 47.134-47.133h32.133c25.989 0 47.133 21.144 47.133 47.133v10.871c11.049 3.53 21.784 7.986 32.097 13.323l7.704-7.704c18.659-18.682 48.548-18.134 66.665.007l22.711 22.71c18.149 18.129 18.671 48.008-.006 66.665l-7.698 7.698c5.337 10.313 9.792 21.046 13.323 32.097h10.87c25.989 0 47.134 21.144 47.134 47.133v32.134c0 25.989-21.145 47.133-47.134 47.133h-10.87c-3.531 11.05-7.986 21.784-13.323 32.097l7.704 7.704c18.666 18.646 18.151 48.528-.006 66.665l-22.713 22.712c-18.159 18.184-48.041 18.638-66.664-.006l-7.697-7.697c-10.313 5.336-21.048 9.792-32.097 13.323v10.871c0 25.987-21.144 47.131-47.134 47.131zm-106.349-102.83c14.327 8.473 29.747 14.874 45.831 19.025 6.624 1.709 11.252 7.683 11.252 14.524v22.148c0 9.447 7.687 17.133 17.134 17.133h32.133c9.447 0 17.134-7.686 17.134-17.133v-22.148c0-6.841 4.628-12.815 11.252-14.524 16.084-4.151 31.504-10.552 45.831-19.025 5.895-3.486 13.4-2.538 18.243 2.305l15.688 15.689c6.764 6.772 17.626 6.615 24.224.007l22.727-22.726c6.582-6.574 6.802-17.438.006-24.225l-15.695-15.695c-4.842-4.842-5.79-12.348-2.305-18.242 8.473-14.326 14.873-29.746 19.024-45.831 1.71-6.624 7.684-11.251 14.524-11.251h22.147c9.447 0 17.134-7.686 17.134-17.133v-32.134c0-9.447-7.687-17.133-17.134-17.133h-22.147c-6.841 0-12.814-4.628-14.524-11.251-4.151-16.085-10.552-31.505-19.024-45.831-3.485-5.894-2.537-13.4 2.305-18.242l15.689-15.689c6.782-6.774 6.605-17.634.006-24.225l-22.725-22.725c-6.587-6.596-17.451-6.789-24.225-.006l-15.694 15.695c-4.842 4.843-12.35 5.791-18.243 2.305-14.327-8.473-29.747-14.874-45.831-19.025-6.624-1.709-11.252-7.683-11.252-14.524v-22.15c0-9.447-7.687-17.133-17.134-17.133h-32.133c-9.447 0-17.134 7.686-17.134 17.133v22.148c0 6.841-4.628 12.815-11.252 14.524-16.084 4.151-31.504 10.552-45.831 19.025-5.896 3.485-13.401 2.537-18.243-2.305l-15.688-15.689c-6.764-6.772-17.627-6.615-24.224-.007l-22.727 22.726c-6.582 6.574-6.802 17.437-.006 24.225l15.695 15.695c4.842 4.842 5.79 12.348 2.305 18.242-8.473 14.326-14.873 29.746-19.024 45.831-1.71 6.624-7.684 11.251-14.524 11.251h-22.148c-9.447.001-17.134 7.687-17.134 17.134v32.134c0 9.447 7.687 17.133 17.134 17.133h22.147c6.841 0 12.814 4.628 14.524 11.251 4.151 16.085 10.552 31.505 19.024 45.831 3.485 5.894 2.537 13.4-2.305 18.242l-15.689 15.689c-6.782 6.774-6.605 17.634-.006 24.225l22.725 22.725c6.587 6.596 17.451 6.789 24.225.006l15.694-15.695c3.568-3.567 10.991-6.594 18.244-2.304z"/><path d="m256 367.4c-61.427 0-111.4-49.974-111.4-111.4s49.973-111.4 111.4-111.4 111.4 49.974 111.4 111.4-49.973 111.4-111.4 111.4zm0-192.8c-44.885 0-81.4 36.516-81.4 81.4s36.516 81.4 81.4 81.4 81.4-36.516 81.4-81.4-36.515-81.4-81.4-81.4z"/></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" height="512" viewBox="0 0 32 32" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m30 17-3 3-3-3h2c0-6.0654-4.9355-11-11-11s-11 4.9346-11 11h-2c0-7.168 5.832-13 13-13s13 5.832 13 13zm0 5h-6v2h6zm-10 0h-2v2h2zm-16 0h-2v2h2zm4 0h-2v2h2zm4 0h-2v2h2zm4 0h-2v2h2z"/></svg>

Before

Width:  |  Height:  |  Size: 313 B

-53
View File
@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 412.8 412.8" style="enable-background:new 0 0 412.8 412.8;" xml:space="preserve">
<g>
<g>
<path d="M378.4,225.6L304,251.2L274,234v-27.6v-27.2l30-17.2l74.4,25.6c5.2,2,11.2-1.2,12.8-6.4c2-5.2-1.2-11.2-6.4-12.8
l-57.6-19.6l54-31.2c4.8-2.8,6.4-9.2,3.6-14c-2.8-4.8-9.2-6.4-14-3.6l-54,31.2l11.6-59.6c1.2-5.6-2.4-10.8-8-12
c-5.6-1.2-10.8,2.4-12,8l-15.2,77.2l-30,17.2l-22.8-13.2l-0.4-0.4l-23.2-13.6v-34.4L276,48.8c4.4-3.6,4.8-10,0.8-14.4
c-3.6-4.4-10-4.8-14.4-0.8l-45.6,40V10.4c0-5.6-4.4-10.4-10.4-10.4C200.8,0,196,4.4,196,10.4v62.4l-45.6-39.6
C146,29.6,139.6,30,136,34c-3.6,4.4-3.2,10.8,0.8,14.4L196,100v34.4L172.8,148l-23.2,13.6l-30-17.2l-15.2-77.2
c-1.2-5.6-6.4-9.2-12-8c-5.6,1.2-9.2,6.4-8,12L96,130.8L42,99.6c-4.8-2.8-11.2-1.2-14,3.6s-1.2,11.2,3.6,14l54,31.2L28,168
c-5.2,2-8.4,7.6-6.4,12.8s7.6,8.4,12.8,6.4l74.4-25.6l30,17.2v27.6v27.2h0.4l-30,17.2l-74.4-25.6c-5.2-2-11.2,1.2-12.8,6.4
c-2,5.2,1.2,11.2,6.4,12.8L86,264l-54,31.2c-4.8,2.8-6.4,9.2-3.6,14c2.8,4.8,9.2,6.4,14,3.6l54-31.2l-11.6,59.6
c-1.2,5.6,2.4,10.8,8,12c5.6,1.2,10.8-2.4,12-8L120,268l30-17.2l23.6,13.6l23.2,13.6v34.4L137.6,364c-4.4,3.6-4.8,10-0.8,14.4
c3.6,4.4,10,4.8,14.4,0.8l45.6-40v63.2c0,5.6,4.4,10.4,10.4,10.4c5.6,0,10.4-4.4,10.4-10.4V340l45.6,40c4.4,3.6,10.8,3.2,14.4-0.8
c3.6-4.4,3.2-10.8-0.8-14.4l-60-52v-34.4l23.2-13.6l23.2-13.6l30,17.2l15.2,77.2c1.2,5.6,6.4,9.2,12,8c5.6-1.2,9.2-6.4,8-12
L316.8,282l54,31.2c4.8,2.8,11.2,1.2,14-3.6c2.8-4.8,1.2-11.2-3.6-14l-54-31.2l57.6-19.6c5.2-2,8.4-7.6,6.4-12.8
C389.2,226.8,383.6,223.6,378.4,225.6z M252.4,206.4v27.2l-23.2,13.6l-22.8,13.2l-23.6-13.6l-23.2-13.6v-26.8v-27.2l23.2-13.6
L206,152l23.2,13.6l0.4,0.4l22.8,13.2V206.4z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

-6
View File
@@ -1,6 +0,0 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.4005 12.7763C17.649 12.8443 17.907 12.6982 17.9606 12.4463C18.1963 11.3405 18.1993 10.1959 17.9674 9.08583C17.7037 7.8233 17.1438 6.64152 16.3337 5.63787C15.5236 4.63422 14.4866 3.83744 13.3082 3.3132C12.1298 2.78897 10.8436 2.55228 9.55579 2.62265C8.26794 2.69302 7.01524 3.06843 5.90095 3.71795C4.78665 4.36746 3.84266 5.27248 3.14678 6.35842C2.45089 7.44436 2.02303 8.68012 1.89846 9.96387C1.78893 11.0926 1.91663 12.23 2.27134 13.3036C2.35217 13.5482 2.62452 13.6653 2.8641 13.5706V13.5706C3.10368 13.4759 3.21959 13.2053 3.14058 12.9601C2.83915 12.0245 2.73177 11.0355 2.82702 10.054C2.93731 8.91737 3.31613 7.82324 3.93226 6.86177C4.54839 5.9003 5.38418 5.09901 6.37076 4.52394C7.35733 3.94887 8.46645 3.61649 9.60669 3.55418C10.7469 3.49188 11.8857 3.70144 12.929 4.16559C13.9724 4.62974 14.8905 5.33519 15.6077 6.2238C16.3249 7.11242 16.8207 8.15875 17.0542 9.27658C17.2558 10.2419 17.2569 11.2367 17.0592 12.1995C17.0073 12.4519 17.152 12.7083 17.4005 12.7763V12.7763Z" fill="#323A3D"/>
<path d="M15.4157 8.78647C15.593 8.71313 15.6782 8.50941 15.5948 8.33658C15.1923 7.50231 14.6033 6.76987 13.8715 6.19699C13.0484 5.5526 12.0726 5.13199 11.0389 4.97602C10.0053 4.82004 8.94883 4.93399 7.9722 5.30681C7.10396 5.63825 6.32502 6.16427 5.6943 6.84264C5.56365 6.98317 5.58494 7.20296 5.73272 7.32535V7.32535C5.88051 7.44775 6.09873 7.42629 6.23043 7.28674C6.78398 6.70015 7.46373 6.2447 8.22002 5.956C9.08471 5.62591 10.0201 5.52502 10.9353 5.66312C11.8504 5.80122 12.7144 6.17362 13.4432 6.74415C14.0806 7.24316 14.5957 7.8789 14.9515 8.60271C15.0362 8.77491 15.2383 8.85981 15.4157 8.78647V8.78647Z" fill="#323A3D"/>
<path d="M13.7871 9.75159L10.6357 14.0831" stroke="#323A3D" stroke-width="1.04628" stroke-linecap="round"/>
<circle cx="10.1926" cy="14.5735" r="1.32935" fill="#323A3D"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

-4
View File
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24">
<path d="m12.5,15.051V5h-1v10.051c-1.14.232-2,1.242-2,2.449,0,1.379,1.121,2.5,2.5,2.5s2.5-1.121,2.5-2.5c0-1.208-.86-2.217-2-2.449Zm-.5,3.949c-.827,0-1.5-.673-1.5-1.5s.673-1.5,1.5-1.5,1.5.673,1.5,1.5-.673,1.5-1.5,1.5Zm4.5-6.181V4.5c0-2.481-2.019-4.5-4.5-4.5s-4.5,2.019-4.5,4.5v8.319c-1.627,1.561-2.32,3.805-1.859,6.049.508,2.472,2.506,4.476,4.972,4.987.459.096.92.143,1.376.143,1.495,0,2.942-.503,4.111-1.454,1.525-1.241,2.4-3.08,2.4-5.044,0-1.763-.727-3.456-2-4.681Zm-1.031,8.949c-1.292,1.05-2.989,1.454-4.653,1.108-2.081-.432-3.767-2.124-4.194-4.21-.405-1.968.235-3.933,1.713-5.258l.166-.148V4.5c0-1.93,1.57-3.5,3.5-3.5s3.5,1.57,3.5,3.5v8.761l.166.148c1.166,1.046,1.834,2.537,1.834,4.091,0,1.662-.74,3.218-2.031,4.269Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 865 B

-6
View File
@@ -1,6 +0,0 @@
<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6444 8.72819C12.3737 7.93152 12.8358 6.88333 12.8815 5.7196C12.9537 3.86391 11.9465 2.21641 10.4205 1.38122C10.2905 1.31022 10.1557 1.24523 10.0186 1.18506C9.74417 1.07796 9.44933 1.01417 9.13884 1.00214C7.64057 0.943171 6.37816 2.1105 6.3204 3.60878C6.2867 4.48608 6.67301 5.28155 7.29879 5.80264C7.34813 5.83754 7.39507 5.87485 7.442 5.91336C7.92097 6.31169 8.25913 6.87129 8.37587 7.5043C8.67793 7.40923 9.00045 7.3635 9.335 7.37673C10.317 7.41765 11.1642 7.95077 11.6444 8.72819Z" stroke="#00AE42" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M6.3842 10.1074C6.39743 9.77525 6.46723 9.45995 6.58276 9.16752C5.57669 8.98339 4.50443 9.12179 3.52844 9.63445C1.88455 10.4997 0.961517 12.1954 1.00123 13.9343C1.00484 14.0823 1.01567 14.2316 1.03252 14.3808C1.07705 14.6708 1.16971 14.9597 1.31412 15.234C2.01212 16.5602 3.65481 17.0705 4.98099 16.3713C5.75721 15.9621 6.25422 15.2304 6.39142 14.4265C6.39743 14.3664 6.40586 14.3062 6.41669 14.2472C6.53583 13.5564 6.91972 12.9174 7.52024 12.4926C6.79938 11.9523 6.34689 11.0774 6.3842 10.1074Z" stroke="#00AE42" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M16.7327 11.0132C15.9902 10.545 15.1081 10.4813 14.3427 10.7641C14.2874 10.7881 14.232 10.811 14.1754 10.8327C13.4895 11.0854 12.7097 11.0601 12.0213 10.7147C11.8083 11.9254 10.8347 12.8592 9.62646 13.0313C9.94778 14.0747 10.6277 15.0134 11.6242 15.6416C13.1959 16.632 15.1262 16.5839 16.6124 15.6801C16.7388 15.6031 16.8627 15.5188 16.9831 15.4298C17.2129 15.2457 17.4163 15.0218 17.5812 14.7595C18.3815 13.4898 18.0012 11.8135 16.7327 11.0132Z" stroke="#00AE42" stroke-miterlimit="10" stroke-linecap="round"/>
<path d="M9.20391 11.3717C9.76553 11.3717 10.2208 10.9164 10.2208 10.3548C10.2208 9.79317 9.76553 9.33789 9.20391 9.33789C8.64229 9.33789 8.18701 9.79317 8.18701 10.3548C8.18701 10.9164 8.64229 11.3717 9.20391 11.3717Z" stroke="#00AE42" stroke-miterlimit="10" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg height="472pt" viewBox="0 -87 472 472" width="472pt" xmlns="http://www.w3.org/2000/svg"><path d="m467.101562 26.527344c-3.039062-1.800782-6.796874-1.871094-9.898437-.179688l-108.296875 59.132813v-35.480469c-.03125-27.601562-22.398438-49.96875-50-50h-248.90625c-27.601562.03125-49.96875 22.398438-50 50v197.421875c.03125 27.601563 22.398438 49.96875 50 50h248.90625c27.601562-.03125 49.96875-22.398437 50-50v-34.835937l108.300781 59.132812c3.097657 1.691406 6.859375 1.625 9.894531-.175781 3.039063-1.804688 4.898438-5.074219 4.898438-8.601563v-227.816406c0-3.53125-1.863281-6.796875-4.898438-8.597656zm-138.203124 220.898437c-.015626 16.5625-13.4375 29.980469-30 30h-248.898438c-16.5625-.019531-29.980469-13.4375-30-30v-197.425781c.019531-16.558594 13.4375-29.980469 30-30h248.90625c16.558594.019531 29.980469 13.441406 30 30zm123.101562-1.335937-103.09375-56.289063v-81.535156l103.09375-56.285156zm0 0"/></svg>

Before

Width:  |  Height:  |  Size: 917 B

-2
View File
@@ -1,2 +0,0 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="512" height="512"><g id="Water"><path d="M24,46A16.0183,16.0183,0,0,1,8,30C8,16.0942,22.708,2.8125,23.3345,2.2539a.9983.9983,0,0,1,1.331,0C25.292,2.8125,40,16.0942,40,30A16.0183,16.0183,0,0,1,24,46ZM24,4.3721C21.1333,7.1372,10,18.6118,10,30a14,14,0,0,0,28,0C38,18.6118,26.8667,7.1372,24,4.3721Z"/><path d="M18.4976,40.5273a.9946.9946,0,0,1-.5-.1342A12.0449,12.0449,0,0,1,12,30a1,1,0,0,1,2,0,10.0373,10.0373,0,0,0,5,8.6616,1,1,0,0,1-.5019,1.8657Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 548 B

-73
View File
@@ -1,73 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M256,40c-5.52,0-10,4.48-10,10s4.48,10,10,10s10-4.48,10-10S261.52,40,256,40z"/>
</g>
</g>
<g>
<g>
<path d="M466,210C466,94.206,371.794,0,256,0S46,94.206,46,210c0,96.488,66.579,180.855,159.516,203.859
c-1.591,14.119-6.958,31.441-13.568,38.051l-0.131,0.131c-18.899,0.353-32.638,3.149-42.999,8.73
C133.677,468.949,126,482.82,126,502c0,5.522,4.478,10,10,10h240c5.522,0,10-4.478,10-10c0-19.187-7.68-33.058-22.824-41.229
c-10.344-5.58-24.082-8.378-42.992-8.731l-0.132-0.132c-6.61-6.609-11.977-23.931-13.568-38.05
C399.423,390.853,466,306.486,466,210z M316,472c33.23,0,45.303,7.689,48.794,20H147.226c2.172-7.762,6.862-11.345,11.087-13.626
C166.274,474.085,178.603,472,196,472H316z M215.517,452c5.068-10.601,8.238-23.466,9.638-34.27
C235.326,419.232,245.658,420,256,420c10.342,0,20.674-0.768,30.845-2.27c1.401,10.804,4.57,23.67,9.638,34.27H215.517z
M294.015,396.179c-0.019,0.004-0.037,0.007-0.056,0.011c-24.788,5.056-51.127,5.057-75.922-0.001
c-0.017-0.004-0.035-0.007-0.052-0.01C129.918,378.227,66,299.929,66,210c0-104.767,85.233-190,190-190s190,85.233,190,190
C446,299.929,382.082,378.227,294.015,396.179z"/>
</g>
</g>
<g>
<g>
<path d="M389.606,104.994c-23.072-29.303-55.544-50.505-91.434-59.701c-5.355-1.374-10.799,1.855-12.17,7.205
c-1.37,5.35,1.855,10.798,7.205,12.169c31.66,8.112,60.314,26.828,80.686,52.7c3.426,4.352,9.716,5.077,14.043,1.67
C392.275,115.621,393.023,109.333,389.606,104.994z"/>
</g>
</g>
<g>
<g>
<path d="M256,100c-60.654,0-110,49.346-110,110s49.346,110,110,110s110-49.346,110-110S316.654,100,256,100z M256,300
c-49.626,0-90-40.374-90-90c0-49.626,40.374-90,90-90c49.626,0,90,40.374,90,90C346,259.626,305.626,300,256,300z"/>
</g>
</g>
<g>
<g>
<path d="M256,140c-38.598,0-70,31.402-70,70c0,38.598,31.402,70,70,70c38.598,0,70-31.402,70-70C326,171.402,294.598,140,256,140z
M256,260c-27.57,0-50-22.43-50-50s22.43-50,50-50s50,22.43,50,50S283.57,260,256,260z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

-40
View File
@@ -1,40 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<!-- L-4: Restrict Referer header to origin-only on cross-origin navigation so
sensitive tokens in query parameters are not leaked to third-party servers. -->
<meta name="referrer" content="strict-origin-when-cross-origin" />
<title>Bambuddy</title>
<!-- PWA Meta Tags -->
<meta name="description" content="Monitor and manage your Bambu Lab 3D printers" />
<meta name="theme-color" content="#00ae42" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Bambuddy" />
<!-- Manifest -->
<link rel="manifest" href="/manifest.json" />
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-CxIlc-g5.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ChscM3lF.css">
</head>
<body>
<div id="root"></div>
<!-- Service Worker Registration (skip on SpoolBuddy kiosk).
Kept as an external file so the CSP `script-src 'self'` covers it
without needing 'unsafe-inline' or per-build hashes. -->
<script src="/sw-register.js"></script>
</body>
</html>
-95
View File
@@ -1,95 +0,0 @@
{
"id": "/",
"name": "Bambuddy",
"short_name": "Bambuddy",
"description": "Monitor and manage your Bambu Lab 3D printers",
"start_url": "/",
"display": "standalone",
"background_color": "#1a1a1a",
"theme_color": "#00ae42",
"orientation": "any",
"scope": "/",
"icons": [
{
"src": "/img/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/img/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/img/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/img/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/img/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots": [
{
"src": "/img/screenshot-mobile.png",
"sizes": "1080x1920",
"type": "image/png",
"form_factor": "narrow",
"label": "Bambuddy on mobile"
},
{
"src": "/img/screenshot-desktop.png",
"sizes": "1920x1080",
"type": "image/png",
"form_factor": "wide",
"label": "Bambuddy on desktop"
}
],
"categories": ["utilities", "productivity"],
"shortcuts": [
{
"name": "Printers",
"short_name": "Printers",
"description": "View your printers",
"url": "/",
"icons": [{ "src": "/img/android-chrome-192x192.png", "sizes": "192x192" }]
},
{
"name": "Archives",
"short_name": "Archives",
"description": "View print archives",
"url": "/archives",
"icons": [{ "src": "/img/android-chrome-192x192.png", "sizes": "192x192" }]
},
{
"name": "Queue",
"short_name": "Queue",
"description": "View print queue",
"url": "/queue",
"icons": [{ "src": "/img/android-chrome-192x192.png", "sizes": "192x192" }]
},
{
"name": "Projects",
"short_name": "Projects",
"description": "View print projects",
"url": "/projects",
"icons": [{ "src": "/img/android-chrome-192x192.png", "sizes": "192x192" }]
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

-37
View File
@@ -1,37 +0,0 @@
if ('serviceWorker' in navigator) {
if (location.pathname.startsWith('/spoolbuddy')) {
navigator.serviceWorker.getRegistrations().then((regs) => {
if (regs.length > 0) {
Promise.all([
...regs.map((r) => r.unregister()),
caches.keys().then((names) => Promise.all(names.map((n) => caches.delete(n)))),
]).then(() => location.reload());
}
});
} else {
// Capture controller state at script-load. Used to decide whether a
// subsequent `controllerchange` is a deploy-pickup (had a prior SW →
// reload so the new bundle takes over) or a first install (no prior SW →
// skip the reload; the in-flight React mount would otherwise race the
// forced navigation, leaving the page wedged on a spinner. The previous
// approach — `client.navigate(client.url)` from the SW's activate
// handler — exhibited that race in Chromium and a waitUntil hang in
// Firefox, both surfaced on every fresh demo subdomain).
const hadController = !!navigator.serviceWorker.controller;
let reloading = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (!hadController || reloading) return;
reloading = true;
location.reload();
});
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW registered:', registration.scope);
})
.catch((error) => {
console.log('SW registration failed:', error);
});
});
}
}
-229
View File
@@ -1,229 +0,0 @@
// Bambuddy Service Worker
const CACHE_NAME = 'bambuddy-v30';
const STATIC_CACHE = 'bambuddy-static-v29';
// Static assets to cache on install
const STATIC_ASSETS = [
'/',
'/manifest.json',
'/img/favicon.png',
'/img/favicon-16x16.png',
'/img/favicon-32x32.png',
'/img/android-chrome-192x192.png',
'/img/android-chrome-512x512.png',
'/img/apple-touch-icon.png',
'/img/bambuddy_logo_dark.png',
// Self-hosted Inter font (#1460) - cached so the UI renders offline.
'/fonts/inter-latin.woff2',
'/fonts/inter-latin-ext.woff2',
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_ASSETS);
})
);
// Activate immediately
self.skipWaiting();
});
// Activate event - clean up old caches and claim existing clients.
//
// The forced reload that picks up a new bundle on already-open clients (the
// kiosk deploy-pickup scenario) lives in sw-register.js via a
// `controllerchange` listener, gated on whether the page already had a SW
// controller at load time. That gate distinguishes first-install (where a
// reload would race the in-flight React mount — observed on every fresh
// *.demo.bambuddy.cool subdomain, and in Firefox the activate's waitUntil
// hung on `client.navigate` until the document load was aborted with a
// Corrupted-Content error) from upgrade-on-existing-client (where the reload
// is wanted).
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker...');
event.waitUntil(
(async () => {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME && name !== STATIC_CACHE)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
}),
);
await self.clients.claim();
})(),
);
});
// Fetch event - network-first for API, cache-first for static
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip cross-origin requests - let the browser handle them directly.
// Without this the catch-all HTML branch below would answer a failed
// cross-origin request with our cached index.html, so e.g. a blocked
// Google Fonts request came back as text/html (#1460).
if (url.origin !== self.location.origin) {
return;
}
// Skip WebSocket connections
if (url.protocol === 'ws:' || url.protocol === 'wss:') {
return;
}
// Skip camera stream/snapshot requests - Safari has issues with streaming through SW
if (url.pathname.includes('/camera/stream') || url.pathname.includes('/camera/snapshot')) {
return;
}
// API requests - network first, no cache (real-time data is critical)
if (url.pathname.startsWith('/api/')) {
event.respondWith(
fetch(request).catch(() => {
// Return offline response for API failures
return new Response(
JSON.stringify({ error: 'offline', message: 'You are currently offline' }),
{
status: 503,
headers: { 'Content-Type': 'application/json' },
}
);
})
);
return;
}
// Static assets - cache first, then network
if (
url.pathname.startsWith('/img/') ||
url.pathname.startsWith('/icons/') ||
url.pathname.startsWith('/fonts/') ||
url.pathname.endsWith('.png') ||
url.pathname.endsWith('.jpg') ||
url.pathname.endsWith('.svg') ||
url.pathname.endsWith('.ico') ||
url.pathname.endsWith('.woff2')
) {
event.respondWith(
caches.match(request).then((cached) => {
if (cached) {
return cached;
}
return fetch(request).then((response) => {
// Cache successful responses
if (response.ok) {
const clone = response.clone();
caches.open(STATIC_CACHE).then((cache) => {
cache.put(request, clone);
});
}
return response;
});
})
);
return;
}
// JS/CSS assets - network first (Vite content-hashes filenames, so
// cache-busting is built in; network-first ensures new builds load immediately)
if (
url.pathname.startsWith('/assets/') ||
url.pathname.endsWith('.js') ||
url.pathname.endsWith('.css')
) {
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(request);
})
);
return;
}
// HTML pages - network first, fall back to cache
event.respondWith(
fetch(request)
.then((response) => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, clone);
});
}
return response;
})
.catch(() => {
return caches.match(request).then((cached) => {
if (cached) {
return cached;
}
// Return cached index for SPA navigation
return caches.match('/');
});
})
);
});
// Handle push notifications (for future use)
self.addEventListener('push', (event) => {
if (!event.data) return;
const data = event.data.json();
const options = {
body: data.body || 'New notification from Bambuddy',
icon: '/img/android-chrome-192x192.png',
badge: '/img/favicon-32x32.png',
vibrate: [100, 50, 100],
data: {
url: data.url || '/',
},
};
event.waitUntil(
self.registration.showNotification(data.title || 'Bambuddy', options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
// Check if there's already a window open
for (const client of windowClients) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.navigate(url);
return client.focus();
}
}
// Open a new window if none exists
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB