Browse Source

Show the job the scheduler starts next on the printer card (#3200)

maziggy 2 days ago
parent
commit
6968433d70

+ 1 - 1
CHANGELOG.md

@@ -65,7 +65,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The `{finish_photo_url}` link in a notification opens when authentication is on** — With authentication on, the link pointed at the archive's photo page, which needs a login that a link tapped in Telegram, CallMeBot or a Home Assistant notification can't carry, so it only ever showed an error. With authentication on the link now points at a copy of the photo under a long random name, which opens that one photo and nothing else and stops working after 3 days. With authentication off the link is unchanged. Photos attached to the message itself were not affected.
 - **The Reorder Alert and Stock Break Alert notifications are now actually sent (#2955, reported and contributed by @ojimpo in #3196)** — Both events could be switched on for a notification provider, but nothing in the backend worked out when a SKU was low: the Forecast panel on the Inventory page did that in the browser, so no alert could ever fire. Bambuddy now runs the same forecast once an hour, from your own inventory or from Spoolman, and sends an alert when a SKU reaches its reorder point or will run out before a reorder could arrive. Each SKU alerts once when it enters a condition and again only after it has cleared; one that gets worse, from reorder to stock break, alerts again. A SKU with its alerts snoozed is skipped, and a provider with both events on gets the stock break message only. What has been sent is stored, so a restart or an update does not send the alerts again. The default templates now name the subtype and colour, so two colours of one filament no longer send the same message; a template you have edited is left as it is.
 - **Reading a 3MF's details no longer loads all of its geometry into memory** — To find the title, designer and MakerWorld link, Bambuddy read the whole `3D/3dmodel.model` entry into memory. A plain 3MF keeps its meshes in that same entry, so a large one cost hundreds of MB just to read a few metadata lines; a combined plate at the size limits pushed memory from 1.0 to 1.7 GB. The mesh data is now streamed past, and saving a 3MF to the library parses it without holding up other requests.
-- **The queue starts jobs in the order you put them, whether they're pinned to a printer or queued for "Any <model>" (#3200, reported by @bgrr74)** — With a job pinned to a printer dragged above two "Any P2S" jobs, the printer finished and started one of the "Any" jobs from further down, while the pinned job sat at the top showing "Busy". The scheduler sorted the queue by target first and position second, so position only counted among jobs with the same target. Which job won a printer both wanted then came down to the database: on SQLite "Any" jobs always won, and on PostgreSQL pinned jobs did. The scheduler now follows the order the queue page shows, and so does Shortest Job First, whose starvation guard now also protects a pinned job that an "Any" job jumped, and the other way round. New jobs, batch copies, virtual printer and webhook uploads go to the end of the whole queue, not the end of their printer's part of it, and "add to top" puts them at the top of the whole queue. Likely also the cause of #1808.
+- **The queue starts jobs in the order you put them, whether they're pinned to a printer or queued for "Any <model>" (#3200, reported by @bgrr74)** — With a job pinned to a printer dragged above two "Any P2S" jobs, the printer finished and started one of the "Any" jobs from further down, while the pinned job sat at the top showing "Busy". The scheduler sorted the queue by target first and position second, so position only counted among jobs with the same target. Which job won a printer both wanted then came down to the database: on SQLite "Any" jobs always won, and on PostgreSQL pinned jobs did. The scheduler now follows the order the queue page shows, and so does Shortest Job First, whose starvation guard now also protects a pinned job that an "Any" job jumped, and the other way round. New jobs, batch copies, virtual printer and webhook uploads go to the end of the whole queue, not the end of their printer's part of it, and "add to top" puts them at the top of the whole queue. The **Next in queue** line on a printer card used the same ordering and kept naming a lower "Any" job; it now names the job in queue order too (it does not follow Shortest Job First). Likely also the cause of #1808.
 - **Settings tabs keep their icons when a label is long (#3191, reported and contributed by @Thomansky in #3192)** — In German the Notifications tab lost its bell: "Benachrichtigungen" plus the provider count didn't fit the tab list on wide screens, and the icon was squeezed to nothing instead of the word. The tab list is a little wider now, and tab icons, counts and status dots always keep their size, so a long label can only wrap. "Virtueller Drucker" also fits on one line again.
 - **"Print labels…" starts from the spools you ticked (#2980, reported by @apizz)** — With spools ticked in the inventory list, the header's **Print labels…** button still opened the label picker with every visible spool selected, so you had to find your spools again. It now starts from the ticked spools, and from every visible spool only when none is ticked.
 - **In Spoolman mode, a new Bambu Lab roll links to a filament of its own product line (#2907, reported and contributed by @ojimpo in #2944)** — Bambuddy matched a new roll to a Spoolman filament by material and colour alone. Several product lines share a colour, so a PLA Matte Charcoal roll was linked to the PLA Basic "Black" filament, took its name and density, and showed up as Black. The same happened to other lines: PLA Tough took PLA Basic's library entry, PLA Silk White took "Aero White", and PETG HF took plain PETG's. The match now also checks the product line the AMS reports, both among your existing filaments and in the Spoolman filament library, and each roll takes its own library entry and density. New filaments are named for the line ("PLA Matte"), including ones taken from the library, and the colour name ("Charcoal") is stored on the spool. Spoolman mode now shows the same subtype and colour name as the built-in inventory. When no filament or library entry fits the line, Bambuddy creates one from what the AMS reports instead of taking the first one with the same colour. Filaments that earlier versions named after a colour ("Black") are still reused, so existing installs don't get duplicates. A filament you renamed yourself in Spoolman is no longer reused for new rolls.

+ 6 - 1
backend/app/api/routes/print_queue.py

@@ -616,7 +616,12 @@ async def list_queue(
             # Cross-model candidates (#671) and their files, for the card label.
             selectinload(PrintQueueItem.variants).selectinload(PrintQueueVariant.library_file),
         )
-        .order_by(PrintQueueItem.printer_id.nulls_first(), PrintQueueItem.position)
+        # The order the scheduler dispatches in (#3200), so the first pending
+        # item for a printer is the one it will start next -- which is what the
+        # printer card's "Next in queue" shows. Sorting by printer first put
+        # every "Any <model>" job (no printer_id) ahead of a job pinned to that
+        # printer, whatever their positions.
+        .order_by(PrintQueueItem.position, PrintQueueItem.id)
     )
     if user is not None and not can_read_all:
         query = query.where(PrintQueueItem.created_by_id == user.id)

+ 29 - 0
backend/tests/integration/test_print_queue_api.py

@@ -119,6 +119,35 @@ class TestPrintQueueAPI:
         assert response.status_code == 200
         assert isinstance(response.json(), list)
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    @pytest.mark.parametrize("pinned_first", [True, False], ids=["pinned-above-any", "any-above-pinned"])
+    async def test_list_follows_queue_position_across_pinned_and_any_jobs(
+        self, async_client: AsyncClient, printer_factory, queue_item_factory, pinned_first
+    ):
+        """A printer's first pending item is the one the scheduler starts next (#3200).
+
+        The printer card's "Next in queue" shows that first item. The list used
+        to sort by printer first, which put every "Any <model>" job (no
+        printer_id) ahead of a job pinned to the printer -- the order the
+        scheduler itself had until #3200 -- so the card kept naming a lower
+        "Any" job after dispatch was fixed.
+        """
+        printer = await printer_factory(model="P2S")
+        pinned_pos, any_pos = (1, 2) if pinned_first else (2, 1)
+        pinned = await queue_item_factory(printer_id=printer.id, position=pinned_pos)
+        any_model = await queue_item_factory(printer_id=None, target_model="P2S", position=any_pos)
+        later_any = await queue_item_factory(printer_id=None, target_model="P2S", position=3)
+
+        response = await async_client.get(
+            "/api/v1/queue/", params={"printer_id": printer.id, "status": "pending", "target_model": "P2S"}
+        )
+
+        assert response.status_code == 200
+        ids = [item["id"] for item in response.json()]
+        expected_head = [pinned.id, any_model.id] if pinned_first else [any_model.id, pinned.id]
+        assert ids == [*expected_head, later_any.id]
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_add_to_queue(self, async_client: AsyncClient, printer_factory, archive_factory, db_session):