Skip to content

v2.6.0

Choose a tag to compare

@github-actions github-actions released this 08 Oct 10:34
93849ac

Added

  • A detection rate for each camera, under Cameras. Lower it to cut the load PrintGuard puts on a
    shared host. A defect takes longer to confirm at a lower rate. It's detect_fps over the API and
    MCP server. Thanks to @eikaramba.
  • A review at the end of each print. PrintGuard keeps a few frames per print on your hub, and you
    can label them and send them to help train the detection model. Nothing is sent unless you send
    it, and the training inbox keeps no logs or traces. Settings, under Advanced, switches the review
    off, which also stops frames other than alert snapshots being kept. What's sent. Thanks to @eikaramba.
  • A plugin panel receives the names of the secrets its plugin holds as pg.secrets, never the values.

Changed

  • The hub only answers to IP addresses, localhost, names with no dot, names ending .local,
    .lan, .home, .home.arpa, .internal or .localhost, and the addresses in
    PRINTGUARD_ORIGINS, which stops a DNS rebinding page reaching it. If you open PrintGuard at any
    other name, such as a domain, a Tailscale ts.net name or printguard.fritz.box, add it to
    PRINTGUARD_ORIGINS with its http:// or https:// before updating. The API and MCP server are
    held to the same rule. Host and origin checking.
  • PRINTGUARD_ORIGINS matches whatever the capitals, a :443 or a trailing dot, and only takes
    http:// and https:// entries. The hub warns at start about one with another scheme, no scheme,
    a wildcard or one it can't read, and ignores it. An Origin has to match the scheme and port you opened the
    hub on, so behind a proxy that ends TLS send X-Forwarded-Proto. A request with no Host header,
    an Origin header that can't be read, or an engine or camera publish WebSocket with no Origin
    is refused. The API and MCP server refuse a request whose Origin is another site's, so a web
    page can't call them. Scripts and agents send none and are unaffected.
  • A name with letters outside ASCII in PRINTGUARD_ORIGINS, such as müller.example, matches the
    punycode a browser sends.
  • The hub won't start when state.json is there but can't be read, or when the data directory
    or prints/ in it can't be created or written, such as with the wrong owner on it. The log names the directory, or
    the nearest one that exists, and its owner. An unreadable file used to start an empty hub.
  • Alert snapshots in the risk history survive a restart. They're kept 512 pixels on the short
    side, 40 a print, for the last 20 prints or 200 MB. The chart and the alert log still reset.
  • The history's watch time is how long the readings spanned, in whole minutes with gaps over 30
    seconds left out, and each history bucket carries the seconds it watched, never more than 60.
  • Review frames, snapshots and sliced files are stored readable by the hub's user only. Files stored
    before 2.6.0 keep the permissions they had.
  • A recovery notice, and one sent by a plugin, goes quietly, so only the test alert, a
    fault warning and a defect alert ring as urgent. ntfy leaves out its urgent priority, Pushover
    sends at normal priority or lower, and Telegram and Discord suppress the notification.
  • Live scores, alerts, warnings, printer status and errors only reach a plugin granted
    Read the dashboard.
  • A plugin's request or WebSocket doesn't follow redirects, and its sign-in endpoints must be
    https, with a plain host that has no login, backslash or percent-encoded name in it. An installed
    plugin that signs in over plain http is removed when the hub starts.
  • A plugin that signs in at an address on your own network needs Reach your own network to install
    or to open its sign-in page.
  • A plugin's address pattern matches the path on its own, and one without a ? covers any query
    string.
  • Site-local and multicast addresses count as your own network for plugins.
  • A printer, alert channel or MQTT setting of the wrong type is refused, and mqtt refuses a field
    it doesn't have.
  • A notification sent by a plugin is refused when its title or text isn't text.
  • Adding a camera whose device, path or address isn't text is refused.
  • A plugin that lists a local address written as 127.1, 0x7f.0.0.1 or one long number, or a
    wildcard over a local suffix such as *.local or *.lan, needs Reach your own network to
    install. One already installed without it stays installed, off and with its data, until you
    accept it. .home.arpa names count as your own network for plugins.
  • A plugin request with a secret anywhere before the path of its address is refused, and so is
    one with .. in its path.
  • A plugin's panel.html can't use inline handlers such as onclick="...", and makes no sound,
    audio or video, unless its plugin was granted sound.
  • A plugin's ctx.background only takes a base64 data: URL of a PNG, JPEG, WebP or GIF.
  • A plugin can only call, publish to or answer another plugin over its link.
  • A plugin that doesn't run on your platform is refused when it's installed from GitHub or a
    zip and when it's updated. Only the plugin store used to check.
  • A manifest that asks for sign-in without describing it is refused.
  • An update that changes where a plugin signs in signs it out and has to be accepted again.
  • The consent dialog and the update view show the addresses a plugin signs in at.
  • A plugin's README is shown as Markdown only, so it can't put forms, audio, video or its own
    styling on the plugin's page, and its images load from the plugin's own repository only.
  • An answer to a plugin's request over 256 KB fails the request, where text used to be cut short.
  • A plugin's request fails when the answer is compressed any way but gzip.
  • A plugin zip over 12 MB is refused in the dashboard before it's sent and again by the hub. One
    with a member that unpacks past its size limit is refused, whatever size the zip gives for it,
    and so is one with a member compressed any way but stored or deflate.
  • A plugin route that answers a status outside 100 to 599, or headers that aren't text, stops the
    plugin and answers 502.
  • A plugin's own pages under /plugins/<id>/ can't load anything from another host or use fetch,
    XMLHttpRequest or WebSockets. A page that loaded its scripts, styles or images from its own
    routes needs them inline.
  • A plugin worker's state handler runs on the hub's once-a-second tick, not on every command, and
    a worker call saves only the keys it changed.
  • /api/v1/state and the MCP get_state tool no longer return what a plugin has stored or list the
    API tokens. startup_warnings lists what the hub found wrong or worked around at start, and
    secrets_set, reviews and feedback_hub are new.
  • The dashboard, API and MCP server show the query values of a camera or printer address as
    [redacted], so ?action=stream reads back as ?action=[redacted]. A path segment of 16 or
    more letters and digits, or a UUID, is redacted too. A new or changed address that still holds
    [redacted] is refused when you save it, with "type it in full".
  • The dashboard takes 10 MB of bug report attachments in total, down from 20 MB. A report's
    diagnostics no longer carry the MQTT broker's address or login, the theme or layout, or the file
    a printer is printing, and the dialog lists what is sent, including the dashboard's address, your
    browser's user agent and window size.
  • A printer can't be registered with a required field left blank, and an alert channel can't be
    saved or tested with one.
  • Changing a printer's, an alert channel's or the MQTT broker's address needs its key or password
    typed again, and the whole address, since the one shown leaves out any login it carries.
  • A printer or notifier address with a stray square bracket is refused when you save it. A printer
    saved with a [ in its password needs it written as %5B.
  • An MQTT port must be a whole number from 1 to 65535. Text such as "1883", 0, decimals and
    anything over 65535 are refused, and so is turning the Home Assistant bridge on with no host.
  • The test alert carries a picture, so a channel that can't take one shows up at setup.
  • Sliced files with over 32 MB of gcode upload without the 3D toolpath or a drawn preview.
  • A sliced file with nothing in it is refused.
  • A 3mf is refused when its files unpack to more than 512 MB between them, including one that
    understates its size, when any member is compressed any way but stored or deflate, when it's
    damaged, and when the gcode of its plate is empty. A binary gcode file with no gcode in it is
    refused too. A file with a print time over a year, or a filament amount or temperature no slicer
    would write, is refused and leaves nothing in the library, and the slicer and printer names it
    carries are cut to 80 characters.
  • The alert threshold stops at 0.95, since a higher one almost never alerted. A monitor set above
    it is lowered.
  • A heater target above 350°C for the nozzle or 150°C for the bed is refused over the REST API,
    MCP server and dashboard socket, where it used to be clamped. The dashboard's own heater field,
    preheat presets and the temperatures corrected on upload still stop at those limits, and so do a
    file's other shifted temperatures.
  • A file tagged only for a printer you then remove stays tagged, so it can't start on another
    printer until you tag it for one.
  • The same camera device or stream address can't be registered twice, whether it's written with a
    default port, a trailing dot or slash, 127.1, another spelling of an IPv6 address, its query in
    another order or with credentials. That includes a printer's webcam you add by hand while the
    printer is opening it, the same WHEP endpoint written http or whep, and a path that is
    another camera's own stream.
  • The ntfy topic address is treated as a secret, so it's hidden in Settings and left out of
    bug reports and API responses.
  • A theme or layout of the wrong shape is refused when saved, and one already stored is reset
    when the hub starts.
  • The dashboard and website serve their own fonts and no longer contact Google Fonts.
  • The interactive API docs pages are gone, so the hub no longer loads Swagger UI or ReDoc from a
    CDN. The schema is at /api/v1/openapi.json.
  • POST /api/v1/classify and the MCP classify_frame tool refuse a frame over 32 MB or 50
    megapixels, and take a JPEG or a PNG only.
  • PrusaLink takes a Username, for a Raspberry Pi set up with one other than maker.
  • Empty values in the camera and print details read "none".
  • Removing a camera, printer, monitor, print or API token that doesn't exist is an error, where it
    used to answer as if it had worked. The REST API answers 400 for a camera, printer, monitor or
    print.
  • A monitor can't be bound to a camera or printer that isn't registered.
  • A monitor or camera setting with the wrong kind of value is refused, such as text for a switch,
    which used to read as on, and a number sent as text or true. That holds over the dashboard
    socket, the REST API and the MCP server, and covers preheat targets. The REST API and MCP server
    take fault_grace_s, update_check and feedback as settings. A value of the wrong type answers
    422 over REST and a tool error over MCP. A REST 422 no longer repeats the value that was sent. An
    unknown setting is refused over the socket, and the REST API and MCP server still
    drop a field they don't know.
  • A camera rotation of -90 or 450 is refused.
  • Once a token is issued, the MCP server refuses a connection without one. It used to answer with an
    empty tool list.
  • The MCP server answers at /mcp as well as /mcp/.
  • Once a token exists, a bearer token with a non-ASCII character answers 401, where it was a server
    error.
  • Adding a camera waits up to 40 seconds for a first frame, refreshing a printer's cameras 115 and
    switching the inference runtime 85, over the REST API, MCP server and plugins. REST used to give
    up at 15. The dashboard's socket has no deadline.
  • A pause or cancel the printer never answers is reported as failed after 45 seconds, 135 on an
    Elegoo printer, when PrintGuard sends it after a defect. On a Centauri Carbon 2 it could take 4.5
    minutes. A pause, resume or cancel you send over the REST API, MCP server or Home Assistant waits
    15 seconds, 105 on an Elegoo printer, where resuming a Centauri Carbon 2 timed out after 15.
  • A plugin update that answers other plugins on a new channel or asks for a new sign-in scope waits
    for you to accept it.
  • Recent events no longer hold a printer's status updates, so an alert stays in them.
  • Reading a camera's stream from the hub on port 8554 or 1935 needs a login the hub makes up at
    each start, so another program such as VLC, Frigate or Home Assistant can no longer read it there.
    Publishing to those ports is unchanged.
  • To run a MediaMTX of your own with the shipped mediamtx.yml, use 1.19.0 or newer. The file
    grants its API and reading to no login, so add a user with api and read and put its login in
    MEDIAMTX_API, MEDIAMTX_RTSP and MEDIAMTX_HLS.
  • Running from source needs Python 3.12.4 or newer.
  • The image and both desktop apps include LICENSE.md and the notices for the Intel GPU packages,
    WebView2 and the other native wheels.
  • Light theme accent and red are a shade darker for contrast, and the dark theme's red a shade
    lighter.
  • A settings edit naming an alert channel that doesn't exist is refused.
  • The image declares port 1935 as well as 8000 and 8554.
  • A USB camera missing when the container starts stays under Cameras as offline and keeps its
    name, crop and tuning. Remove it there if it's gone for good.
  • Resume is only offered while a print is paused, Cancel while one is printing or paused, and Pause
    while one is printing. A switched-off monitor reads "off".
  • A startup notice, such as a GPU the hub skipped, shows on the dashboard once per page load.
  • Cancelling a print, deleting a monitor, removing a printer, camera, file, plugin or theme,
    revoking a token and resetting the layout each ask for a second press. So does closing the upload
    sheet with files still in it.
  • Buttons, tabs and the dialog close button are 44 pixels tall on touch screens.
  • Enter submits the add monitor, API token and register printer forms.
  • A printer or alert channel is refused when a choice isn't one of those offered, such as a Pushover
    priority of 7 or an unknown Elegoo family.
  • An alert channel keeps only the fields its service has, as a printer does, so a field sent by
    mistake isn't stored or shown.
  • POST /api/v1/classify and the MCP classify_frame tool read two images at a time. Eight more
    callers wait and one past that answers 503. The MCP server answers 413 to a request body over
    42 MB without reading it.
  • A retained MQTT message on a command topic is ignored, so a retained cancel no longer cancels
    the print each time the hub reconnects.
  • A plugin's stored data, the JSON it sends in a request and what it passes another plugin can nest
    32 levels deep at most. A deeper store could stop the hub saving its settings, or starting at all.
    One already saved that deep is emptied when the hub starts, and a state.json nested too deep to
    read is kept as state.json.corrupt.
  • A plugin that signs in at a local address written with a trailing dot, such as localhost., needs
    Reach your own network to install. A sign-in address is refused when its host ends in a number but
    isn't an IPv4 address, such as 256.256.256.256.
  • A plugin can't be installed from a GitHub repository or path containing . or .., which fetched
    it from a different repository to the one shown.
  • A zip that verifies against the catalogue is recorded as an install from the catalogue, so its
    page shows the README, icon and screenshots from the pinned commit and not the ones in the zip.
  • A plugin manifest with a field of the wrong type is refused with a message naming the field,
    permissions written as one string is refused, and a plugin's name is cut at 80 characters.
  • A plugin worker's ctx.log can't start a new line in the hub's log, and a worker no longer has
    gc, scriptArgs, argv0 or execArgv in scope.
  • A plugin's gate approval is only reused for a request with the same client address and browser as
    well as the same cookie, path and query.
  • The consent dialog no longer says a plugin's code matches what it asks for when the code reaches
    its API under another name, and the code check refuses the same addresses the hub does.
  • The camera:control permission says it changes a camera's detection rate, not its frame rate, and
    the plugin manifest schema agrees with the hub on asset name length, the number of OAuth scopes
    and the letter case of an address.
  • An alert counts as delivered only when ntfy, Discord, Telegram or Pushover answers in its own way.
    A topic or webhook address that answers with a sign-in page is reported as failed, where it
    counted as sent.
  • A printer, ntfy or Discord address without http:// or https:// is refused when saved or
    tested.
  • A pause, resume, cancel or heater target the printer doesn't take in time answers 400 naming the
    printer over the REST API, where it was a 504. The dashboard waits the same 15 seconds, 105 on
    an Elegoo printer.
  • Messages sent through your alert channels by a plugin or the API are limited to 20 a minute.
  • A plugin's request is cut off after 10 seconds in total, where each read had 10 seconds.
  • A plugin's WebSocket sent more than 300 frames in 10 seconds is closed with an error.
  • A plugin's render runs when its state or store has changed, not on every update from the hub,
    and a store written more than once a second is saved once a second.
  • Every request, socket and sign-in the hub makes for a plugin carries an X-PrintGuard-Plugin
    header, which a plugin can't set.
  • Removing a plugin that isn't installed is an error, where it answered as if it had worked.
  • An MCP tool call with NaN or Infinity anywhere in its arguments is a tool error. A nested one
    used to be read as a field left out.
  • The shipped mediamtx.yml no longer grants playback without a login.
  • Stopping the container gives open requests 3 seconds before it shuts down.
  • The container's health check ignores HTTP_PROXY, so a hub behind a proxy no longer reads as
    unhealthy.
  • The website's download button says the macOS app is for Apple silicon.
  • THIRD_PARTY_NOTICES.md carries the PyInstaller bootloader's licence, which is inside both
    desktop apps.

Fixed

  • A damaged state.json is kept as state.json.corrupt, where it used to be overwritten with an
    empty hub. So is one of the wrong shape, such as null or a list. A later damaged file is kept
    beside it, up to state.json.corrupt.4, so the first is never replaced, and a file saved
    with a byte order mark, as Notepad does, is read.
  • One unreadable entry in state.json, or a setting of the wrong kind, no longer stops the hub
    starting. The entry is left out or the setting goes back to its default, with the reason in the
    log and a startup warning, and a print's file is kept. A NaN or infinite number in the file is
    left out with a line in the log, and a monitor bound to a printer that was left out is unlinked
    from it.
  • Saving state no longer runs on the event loop, so a slow disk such as an SD card doesn't stall
    detection. A save that fails is reported once.
  • Editing settings or a printer over REST no longer wipes its secrets. A secret sent blank or left
    out keeps the saved one and null clears it.
  • Removing a camera, printer, monitor or print, or editing a printer, finishes even when the tab
    that asked disconnects or the request times out. A removal whose clean-up fails still removes it
    and warns.
  • Quitting while a camera is opening no longer risks saving a state with cameras missing.
  • A settings change made while the inference runtime is switching is no longer undone, a camera
    that stalls during a switch is restarted and an image sent to /api/v1/classify during one no
    longer fails.
  • Two people starting a print on one printer at the same moment no longer send it two files.
  • An error from a printer or notification that timed out says what kind of failure it was, where the
    reason used to be blank.
  • A failed update check is tried again after 15 minutes, not the next day.
  • A print upload that times out no longer leaves its file behind. When the hub starts it deletes
    unfinished uploads and keeps the files of a record it couldn't read. The files in prints/ with
    a name it generated that no print or review uses are deleted too, unless the state file is missing
    or a state.json.corrupt is waiting.
  • A very thin image sent to /api/v1/classify no longer uses hundreds of megabytes.
  • A print upload that outruns the engine answers 504, not 500.
  • Starting a print on OctoPrint or Moonraker holds the file in memory once, not three times.
  • A streaming server that can't start no longer fills the log a bug report attaches, stopping the
    hub while it launches no longer hangs, and Ctrl+C no longer reports the video server as failed.
  • Correcting a large file's temperatures on upload uses about half the memory it did.
  • A camera looking at a still scene holds a steady score, where a grainy webcam used to jump
    from frame to frame. Frames are shrunk for the model the way it was trained, which also stops
    noise pulling a failing print's score under the threshold.
  • A GPU or NPU that's offered but can't run the model, or returns output it can't use, is skipped,
    and detection falls back to the next device or the CPU with a notice on the dashboard and in the
    log. Some failures used to stop PrintGuard starting.
  • A slow camera no longer holds the cameras beside it to a few detections a second.
  • An inference cancelled while its camera restarts no longer stops that camera detecting.
  • A heater target, threshold or camera setting sent as NaN or Infinity is refused. NaN used to be
    read as the top of the range, so a nozzle target of NaN heated to 350°C. A REST body carrying
    either answers 422, and the engine socket refuses a command that carries them.
  • A camera that stays connected while its feed is frozen or the model keeps failing on it is
    reported as stalled after the fault grace period.
  • A camera that keeps freezing and briefly recovering is reported as an unreliable feed, and one
    warned about as unreliable is no longer announced as steady again while it's offline.
  • A stalled camera watched by several monitors is restarted once, not once per monitor.
  • A camera that is restarted or moved is no longer opened twice.
  • A monitor with no camera warns that it has none and no longer reads as watching.
  • Switching a monitor off resets its defect streak, as does a camera dropout or a frozen feed, so
    defect frames either side of one no longer count as consecutive. Removing a monitor clears what
    the watchdog held for it.
  • A monitor that stands down forgets its camera faults, so the next print starts with the full
    fault grace period.
  • A cooldown of zero no longer repeats the pause or cancel command on every defect frame.
  • A defect cooldown no longer carries into the next print, and a pause or cancel the printer didn't
    take is tried again after 30 seconds if the defect is still there, however short the cooldown.
  • A defect alert is pushed when the hub starts within 30 seconds of its computer booting, which used
    to hold it back, and a full disk no longer stops an alert or the next pause attempt.
  • A pause that worked is pushed even when a failed attempt was pushed seconds earlier.
  • A pause refused because the print is already paused or over is no longer announced as
    "AUTOMATIC PAUSE FAILED".
  • A status read that began before a pause, resume, cancel, heater target or print start, whether
    PrintGuard or you sent it, no longer replaces the one read after it, which could pause a paused
    print a second time.
  • A pause no longer waits behind a slow command, such as registering a stream, sent from the
    same tab.
  • Stopping the hub no longer lets an inference in flight start a pause after the printers are
    closed.
  • A command that worked is no longer reported as failed when the status read after it fails, and a
    pause or cancel PrintGuard sends after a defect is sent once and doesn't mark the printer offline.
  • Camera adjustments no longer hold up the hub while a frame is prepared, and neither does
    uploading a large print file.
  • The camera frame from the API, MCP server and plugins is no longer a stale one while the camera
    is on standby.
  • A stream FFmpeg can open but can't decode, such as an SVG, no longer ends the hub. Adding it fails
    with "no decoder for this stream", a saved one shows offline with that reason, and an image
    sent to /api/v1/classify answers "could not decode image".
  • A whep:// or MediaMTX path camera that fails names the address you gave with its credentials
    removed, and a mistyped stream address says "PrintGuard can't use that address" and why, where it
    showed a raw MediaMTX 404.
  • A saved camera that can't be opened shows why it's offline, on its entry in the registry and in
    the camera rail, and keeps trying. The API's camera carries it as reason.
  • A crop that runs off the right or bottom edge of the frame is cut back to the part inside it.
  • Discover no longer lists the hub's own camera streams, so one can't become a second camera.
  • A USB, MJPEG or Bambu A1 or P1 camera keeps detecting when its live view can't start, such as
    with another program on port 8554 beside the desktop app. It used to read as offline.
  • A live view that stops answering is given up on after 3 seconds and is pushed from a thread of its
    own, so it never delays detection.
  • A sleeping camera's live view comes back after the video server restarts, not only at the next
    print.
  • A Linux webcam with no MJPEG format opens with its other formats.
  • A USB camera that stops answering no longer piles up capture threads, and one that delivers no
    frames is released when it stands down.
  • A Mac running from source can add its cameras, where it failed with no module named objc.
  • Two USB cameras of the same model can both be added in the Windows app.
  • A Bambu A1 or P1 camera is given its measured frame rate, not 25 fps, and gives up on a frame that
    never finishes arriving or can't be right.
  • A printer's webcam you had already added by hand is no longer added a second time, and one that
    can't be opened shows a warning on the dashboard. A printer's camera follows the printer to a new
    address or access code.
  • A webcam is found where it used to be missed. OctoPrint's on port 5000, a Moonraker one with a
    relative address when the printer is on another port, and one on MediaMTX or go2rtc, which is
    pulled over WHEP in place of its web page or a still. Press Refresh under Printer cameras or
    restart the hub to pick them up. A camera that already works is left where it is.
  • A Moonraker webcam whose uid or name has a space or other character a stream path can't take
    registers under a safe one.
  • Restarting PrintGuard while an idle printer is switched off no longer warns that the printer
    and its camera are offline. The printer's last status is kept across restarts. Thanks to
    @dnstkrv for the report.
  • An unexpected error in a printer poll or health check is reported and retried, where it could stop
    printer status updating until a restart.
  • A printer that isn't answering no longer delays status updates from the others, or the wake of
    their cameras when one starts printing.
  • A printer whose address you edit no longer keeps the status read from the old one, and its old
    service can't register a camera on the new one.
  • A printer that answers with a number the dashboard can't read, such as NaN, shows offline and no
    longer drops the dashboard.
  • Removing a printer while its webcam is still opening no longer leaves that camera behind.
  • Changing a printer's service keeps its files tagged and a monitor on the printer's own webcam
    watching it, and carries none of the old service's settings across to the new one.
  • A Bambu pause, cancel, heater target or print start the printer rejects is reported as failed,
    including with Developer Mode off or a wrong access code.
  • PrintGuard holds one connection to a Bambu printer, where it used to reconnect and ask for a
    full report every 5 seconds. It reads the printer on threads of its own, so inference doesn't
    hold it up, and a printer that never reports says to check the serial number.
  • A Bambu upload starts the print, where it could hang after sending the file.
  • Bambu H2C, H2D and H2S printers start an uploaded print. Every model outside the X1, P1 and A1
    series is handed the file the same way, which I haven't tested on a P2S, X2D or H2D Pro.
  • A malformed report from a Bambu printer no longer leaves it showing a stale state for up to a
    minute.
  • A different Centauri Carbon at a previous one's address connects without restarting PrintGuard.
  • A Moonraker webcam whose host name contains snapshot or webrtc is read from the right
    address. One set to Janus with no snapshot URL, an iframe or jMuxer is no longer registered as a
    camera that can't open.
  • A Bambu printer whose last job failed reads as idle, and it can start a print from the library
    after a cancelled or failed job.
  • A large 3mf sent to a Bambu printer from the dashboard is no longer cut off after five minutes,
    and the printer gets 60 seconds to confirm it has stored it. A start over the REST API or MCP
    server waits up to 10 minutes.
  • Test connection gives the reason when a Bambu printer refuses the connection.
  • An Elegoo Centauri Carbon command the printer refuses is reported as failed.
  • An Elegoo Centauri Carbon that stops reporting shows as offline.
  • A paused Centauri Carbon whose connection drops is found again, so it can be resumed.
  • A Centauri connection a cancelled request left opening is closed, and closing a Centauri Carbon
    2's no longer freezes the hub for several seconds.
  • A heater target outside what an Elegoo Centauri takes no longer drops its connection.
  • A Centauri Carbon 2 that is starting up, loading filament, levelling or calibrating is no
    longer read as idle or printing.
  • An original Centauri Carbon shows the time left on a print, never below zero once it runs over its
    estimate, and a command a Carbon 2 refuses no longer drops its connection.
  • A file OctoPrint stores but doesn't start is reported as a failed print.
  • A Klipper print that uploads but doesn't start is reported as failed, on an Elegoo Neptune 4
    or OrangeStorm too, and one Moonraker queued says it's queued.
  • Prusa uploads skip read-only storage, PrusaLink's error state reads as error and a printer with
    no job reports its own state, or unknown when it reports none. Its attention and busy states
    read as unknown, since a warning can sit over a print that keeps running, so watching carries
    on. A PrusaLink upload sends the file once, not twice.
  • A file starts on PrusaLink on a Raspberry Pi when the printer holds none of that name, where the
    upload used to fail.
  • Test connection says when OctoPrint, Moonraker or PrusaLink rejects the key or password, or doesn't
    answer like its API, where it read offline. The log says why a printer went offline.
  • A command counts as taken only when OctoPrint, Moonraker or PrusaLink answers it in its own way,
    so an address that answers with a sign-in page is reported as failed.
  • A print start cut short by a cancelled request says to check whether it started.
  • A print whose file has gone from the data directory answers 404 to a download and 400 to a start,
    without naming the path.
  • A temperature correction on upload also moves M109 R and M190 R waits, and one that would
    move nothing in the file is refused, where it uploaded the file unchanged.
  • A 3mf whose plate is written as plate_01.gcode is read, rewritten and started under that name.
  • A file sliced with several filaments shows the temperature of the one its first layer prints
    with, and changing it moves only that filament.
  • Tagging a file for two printers in quick succession keeps both.
  • A binary gcode file whose metadata and previews come to more than 16 MB, compressed or not, or
    that holds more than 1000 blocks before its gcode, is refused.
  • A desktop notification the system won't schedule or refuses is reported as failed.
  • ntfy alerts send when a monitor name has accents or other non-ASCII characters.
  • A monitor or camera name too long for Pushover, Telegram, Discord or ntfy is cut with an ellipsis,
    where it made the alert fail.
  • A defect alert reaches a self-hosted ntfy server with attachments switched off. It's sent as
    text and the dashboard says the picture was refused.
  • An alert whose picture can't be encoded is sent without it, and the dashboard says so.
  • A notification channel that never answers no longer holds up the others.
  • Home Assistant shows the hub as unavailable after it stops.
  • Home Assistant commands run side by side, in order for one monitor, so a slow printer holds up
    only its own buttons, and one that times out says it timed out.
  • The MQTT bridge recovers from a bad setting. A + or # in the base topic or discovery prefix is
    reported once.
  • An MQTT broker that's down raises one warning, not one every 5 seconds, so alerts stay in the
    recent events.
  • Two hubs on one MQTT broker no longer disconnect each other.
  • An unrecognised MQTT payload no longer disables a monitor.
  • Removing a monitor also clears its Home Assistant state and snapshot topics. One removed while the
    MQTT bridge was off is cleared when the bridge comes back, as long as the hub hasn't restarted.
  • MCP returns alert snapshots as images.
  • The dashboard shows when it has lost the hub and re-sends unsaved changes once it reconnects. Its
    boot screen says what to check when the hub isn't answering.
  • A connection test, an alert channel test, a diagnostics download or a plugin sign-in only shows in
    the dashboard tab that started it, and a failed command no longer clears another test's spinner.
    A key typed for a test no longer comes back in its error. A test button no longer sticks when its
    test was never sent, and a dashboard only shows the errors of commands it sent itself.
  • A printer's connection test result shows only under the printer tested, and clears when you edit
    the form it tested.
  • A button pressed while the dashboard is reconnecting says the command wasn't sent, and the
    dashboard reconnects on its own when its connection has died without closing or never answers,
    after 10 seconds.
  • A heater target the printer refuses goes back to the printer's value.
  • A setting the hub refuses goes back on screen at once. Layout edits no longer jump back for a
    moment after you change them, and a setting changed while another is still saving no longer sends
    the first one again.
  • Deleting a monitor or camera right after renaming it no longer reports an error for the rename.
  • Closing Settings while editing a custom theme no longer leaves the theme controls dead, and
    a theme saved while the hub is away is sent once it reconnects.
  • Selecting text and releasing outside a dialog no longer closes it.
  • Copy buttons work when the hub is reached over plain http.
  • Download logs saves the file in the desktop app.
  • Clicking the defect banner on a tile opens the monitor.
  • A tile whose camera is offline, or whose monitor is switched off or on standby, no longer shows
    its last score and rate as live, and an offline camera shows no rate or risk in the camera list
    or history sheet. Switching a monitor off clears its "DEFECT DETECTED" banner.
  • Deleting a monitor from its panel no longer leaves the other tiles on "starting stream", and a
    history sheet closes when its monitor is removed elsewhere.
  • The history sheet asks the hub for the history once a minute, not on every result, and loads an
    alert's snapshot as it nears the screen. The history chart leaves a gap between prints and says
    when it's still loading.
  • A page that fails to draw says so and offers a reload, where the dashboard used to go blank.
  • A notice raised while a dialog is open is read out by a screen reader.
  • Sliders give a screen reader their value as it's shown on screen.
  • A camera floated with picture in picture keeps playing when you switch tabs or open a dialog.
  • A feed the browser won't autoplay, such as on an iPhone in Low Power Mode, shows "Tap to play".
  • A live view that fails in a browser that plays HLS itself, with no Media Source support, is tried
    again.
  • A camera published from a browser stops capturing when it fails to register or is removed from
    another device, and one that's unplugged says so once.
  • The add camera forms clear once the camera registers, and Enter in a camera's name or address
    registers it.
  • A stream address sent over the API with spaces around it or a capital in its scheme opens.
  • A sliced file your browser can't draw a preview for uploads without one.
  • A file dropped just outside the print library's drop zone uploads, where the browser used to
    open it.
  • The upload sheet stays open while the file uploads and keeps what you typed if the hub refuses it.
    It says why Upload is off when a first layer temperature is cleared.
  • Removing or sending one file, camera or printer no longer greys out the same button on the
    others.
  • Print times read "2h 0m", not "1h 60m", and a file uploaded seconds ago reads "just now".
  • The print library scrolls as a whole on a phone held sideways, where its list was squeezed into
    a strip.
  • Sheets and the page keep clear of the right safe area on a phone held sideways.
  • Toasts no longer run off the edge of a phone screen, and the header wraps on narrow tablets.
  • The Add monitor dialog stays open with what you typed if the add fails.
  • The Register printer form keeps what you typed when the registration fails.
  • A token's name stays in its form until the hub accepts it, and Revoke greys out only that
    token's button. The saved chip only shows on the form or tab that saved.
  • Switching an alert channel off and on keeps what you typed in it.
  • Dashboard alerts say what happened to the print, such as "print paused", and one from an
    alert-only monitor no longer reads "(none)".
  • Stream and printer addresses are no longer capitalised or corrected by an iPhone's keyboard.
  • Small touch targets are larger, focus rings no longer appear after a tap, and more controls are
    named for screen readers, including progress bars, file inputs and Glass sliders.
  • Form fields and the setup checklist's Open buttons have names for screen readers.
  • Glass keeps accent and status colours readable and distinct over a background picture, and applies
    to sheets. A background picture the browser can't decode is ignored, and the popover opens in a
    fixed place in browsers that can't anchor it to its button.
  • A custom theme started from dark keeps readable text on its accent buttons.
  • The landing page's links that open in a new tab show their icon.
  • The guide and website say watchdog warnings come after the grace period.
  • Guide buttons open the settings tab they name.
  • A removed bug report attachment can be attached again, and the attach button works from the
    keyboard.
  • Opening Updates no longer contacts GitHub when the daily check is off. It has a Release notes
    button, and the links in the notes open on GitHub.
  • The consecutive detections slider goes up to 30, matching the API.
  • An edit made just before closing the tab is saved, and so is a monitor name or heater target
    typed when a dialog is closed with Escape, a click outside it or its close button.
  • A dashboard left open through an update reloads itself onto the new version.
  • The dashboard loads in Safari with Block All Cookies on.
  • The How PrintGuard works dialog scrolls on a short screen, where it covered its own buttons.
  • PRINTGUARD_PLUGINS=off switches off every plugin, including the half that runs in your
    dashboard, and the hub answers nothing a plugin sends for itself. It used to stop only the half
    on the hub.
  • A plugin that's switched off can't make requests.
  • A hub plugin that returns a malformed answer is switched off and reported, where it could stop
    every plugin's timer until a restart.
  • A plugin answer holding NaN or Infinity no longer drops every open dashboard.
  • A plugin sign-in survives a restart straight after connecting, works from a hub opened as
    localhost.lan, and keeps a secret typed while it finishes.
  • A plugin is signed out once when its provider refuses the refresh token, where it kept asking.
  • Plugin sign-in opens its tab as you click Connect, so Safari no longer blocks it, and works
    with a provider whose authorise address already has a query. One answered after 10 minutes is
    refused.
  • A plugin sign-in fails with the usual page, not a server error, when the provider sends a token
    lifetime that isn't a number, can't be reached or answers with more than 64 KB.
  • Two plugin requests on an expired token refresh it once, and a late duplicate answer to a plugin
    is ignored.
  • Updating a plugin installed from a branch, as owner/repo@branch, follows that branch. Reinstall
    one installed on 2.5.0 to pick it up.
  • An updated plugin restarts on its new code, and its stored data written while the hub was away is
    sent when it reconnects.
  • A GitHub plugin install or update fails with the status when a file errors, instead of leaving
    that file out.
  • A plugin whose plugin.json isn't an object is refused cleanly, and the manifest schema's urls
    pattern compiles in editors.
  • A plugin address pattern with a capital letter in its path, such as Telegram's
    /bot*/sendMessage, matches, and reinstalling keeps its consent. A zip keeps its credentials and
    stored data but asks for its permissions again.
  • A plugin address pattern with many wildcards can't stall the hub, and one with an IPv6 host
    matches.
  • A plugin's WebSocket closes when the plugin is stopped for failing or loses its network
    permission.
  • A plugin whose README contains an SVG link or a picture named constructor, or whose store page
    holds a plugin.json with the wrong kinds of field, no longer blanks the dashboard.
  • A plugin panel's scripts run wherever they sit in its markup. It's drawn on a hub with no
    monitors too, and a password manager doesn't fill a plugin's secret field.
  • A plugin held at its limit of 60 requests a minute gets through again once its earlier requests
    age out, and opening a socket counts towards the limit.
  • The store page of a plugin that signs in no longer crashes while it loads.
  • A button a plugin draws says so when its command couldn't be sent.
  • The Spotify panel polls only while you're signed in. Once Spotify turns the sign-in down it stops
    asking and says to disconnect and connect again. Spotify is 1.0.1. Press Update on an installed
    copy to get it.
  • A worker-only plugin granted sound plays its own audio files.
  • Alert tones from plugins play in Safari after your next tap or key press, on iPhone and iPad too.
    A tone held back for more than a second is dropped, where they used to all play at once.
  • The Windows desktop app opens its window when the zip was downloaded in a browser and
    extracted with Explorer. It used to show only the tray icon.
  • The Windows desktop app no longer opens a terminal window for its video server.
  • The desktop app shows its "PrintGuard could not start" page, with the reason in the log under
    it, when another program is using its port. On Windows it used to start anyway and could show that
    program's page in its window.
  • The Windows desktop app opens the dashboard in your browser on a PC without WebView2, where
    it used to show a blank window. It does the same while the hub is still starting.
  • Opening the desktop app a second time, while it runs or while it's still starting, opens the
    dashboard in your browser, where it showed a failure page. On macOS, opening the running app again
    shows its window.
  • The desktop app shows "PrintGuard is still starting" and opens the dashboard when it's ready,
    where a slow first launch used to say it could not start.
  • A desktop app window that fails to open writes the reason to the log, and the log file is written
    as UTF-8.
  • Start at login follows the installed app and is left alone by a copy run from a disk image or
    from source. It works on macOS when the app's path contains & or <.
  • The macOS app reports its version.
  • Only the standard image is tagged latest, so a GPU image can't take the default tag.
  • A change of inference runtime, or installing, removing or changing a plugin, finishes when the
    dashboard tab closes or the request times out part way.
  • A browser camera that declares under one frame a second no longer drops its connection.
  • MediaMTX is still restarted after a crash when the hub couldn't tie it to its own lifetime.
  • An answer from one plugin to another is labelled with the channel it was asked on.
  • A monitor whose camera has dropped shows No signal under the Defect detected banner, not behind
    it.
  • Glass is tinted for the background picture on show when a plugin changes or clears it quickly.
  • Saving or deleting a custom theme no longer flashes the old theme.
  • The guide holds the space for each picture, so it no longer jumps as they load.
  • Setting the nozzle target no longer locks the bed target while the printer answers.
  • The update dialog loads its changelog once the hub is back if it was opened while reconnecting.
  • The history chart shows the date at each end when it crosses midnight, and a single minute of
    history is drawn as a line across its minute.
  • Switching the inference runtime gives up after 10 seconds behind an inference that never finishes
    and says the runtime wasn't switched.
  • Why a camera can't be removed, and which printers can't print a file, is written out, where it
    only showed on hover.
  • Closing a 3D preview frees its WebGL context, so previews keep drawing after you've opened many
    files.
  • A print or plugin edit the hub refuses changes nothing, where part of it could be applied.
  • A sliced file whose print time is written as a very long run of digits no longer freezes the hub
    while it's read.
  • Removing a camera whose inference had hung no longer stops every other camera being checked or
    blocks a runtime switch.
  • Switching the update check on checks straight away, where it could wait up to a day.
  • The risk history stays in time order when the hub's clock is set back.
  • An API key, token or address pasted with a space or a line break after it works, where every
    request used to fail with "Illegal header value".
  • A printer or alert channel saved by an earlier version with a number where text belongs, such as a
    Pushover priority, no longer stops every alert channel being saved. A value that can't be used is
    cleared and named in a warning when the hub starts.
  • Correcting a file's temperatures also moves M104S215 written without a space, an indented or
    lowercase command and Klipper's SET_HEATER_TEMPERATURE, and reads a file whose lines end in a
    carriage return alone. A line a host numbered and checksummed is left as it is.
  • A long monitor or camera name is cut in an ntfy title too, at 250 characters.
  • A Discord alert mentions nobody, so a monitor named @everyone no longer pings the channel.
  • A camera published from this browser no longer drops in and out every two seconds when the
    dashboard is open in a second tab on the same device, and one that can't be reopened after a
    reload, such as an unplugged webcam, says so.
  • A browser camera that fails to publish no longer shows the streaming server's login in its error
    on a hub with an external MediaMTX.
  • A deeply nested WebSocket frame, an unreadable /hls/ path or a recording sent faster than it
    plays can no longer crash a handler, answer 500 or hold up a stopping hub.
  • A heater target, file rename or plugin secret entered while the hub is reconnecting no longer
    shows as saved.
  • The plugins tab lists installed plugins while the catalogue loads, and loads the catalogue again
    after a reconnect. Installing with Enter sends once.
  • A plugin with a sign-in address the browser can't read no longer replaces the dashboard with the
    error page.
  • A dashboard with every monitor hidden says so and offers Customise.
  • An unpinned tile or camera can no longer be dropped among the pinned ones, where it used to snap
    back.
  • The toolpath preview no longer blocks scrolling in the upload sheet on a phone held sideways,
    toasts no longer cover a dialog's Save button on a phone, and a short dashboard no longer scrolls
    by the toolbar's height on iOS Safari.
  • An open dashboard no longer reapplies its theme every second when nothing has changed.
  • A Bambu X1 or H2 camera follows a changed access code or certificate, where it stayed offline with
    "that stream is already registered".
  • A monitor switched off or set to alert only while another monitor on its camera was keeping a
    frame no longer pauses the print.
  • A monitor removed while its camera is being reattached is no longer warned about.
  • A stream pushed to the hub and added by its address, such as rtsp://localhost:8554/cam, opens
    with the hub's own login. In Docker with bridge networking, add it as
    rtsp://localhost:8554/<path>.
  • Pausing, resuming or cancelling from the API, MCP server or Home Assistant no longer reports a
    timeout when the printer took the command and was slow to be read back.
  • A Moonraker or OctoPrint webcam whose address is on the printer's own loopback, such as
    http://127.0.0.1:8080/?action=snapshot, is read from the printer's address. A camera-streamer
    webcam whose snapshot address is only a still is read from its stream.
  • A pause, resume or cancel PrusaLink turns down for a wrong password says so, where it said
    "InvalidAuth".
  • Starting a print on OctoPrint older than 1.8.0 no longer reports a failure for a file it started.
  • Correcting a file's first layer temperature is refused when it would bring a later layer to 0°C or
    below, where it wrote a set-point that turned the heater off mid-print.
  • Unlinking a printer from a monitor removes its Pause, Resume, Cancel, status, progress and
    temperature entities from Home Assistant.
  • Quitting the desktop app or stopping the container while an MCP client is connected no longer
    skips the shutdown, so Home Assistant is told the hub is offline and the last state is saved.
  • A streaming server that restarts gets every camera path back when one of them is refused.
  • A failed inference is reported for each camera it happens on, where one camera's error hid the
    others' for 30 seconds.
  • An offline camera reports a detection rate of 0 over the API, where it kept its last one.
  • A refused API or MCP command no longer pushes alerts out of GET /events and bug reports, and
    logs one line where each printed a traceback.
  • /hls//… answers 404, where it was a 500.
  • Refreshing printer cameras finishes when the tab that asked closes, so a camera it found is saved.
  • Notifier, MQTT or printer settings of the wrong kind, and a camera name that isn't text, are
    refused in words over the dashboard socket, where they answered a Python error or were stored as
    given.
  • A dashboard, MQTT or plugin connection the hub drops after an error raises a warning, where it
    only wrote a log line.
  • A training upload answered with a reset time that isn't a number is retried in six hours, where it
    restarted every second.
  • The training inbox counts what it holds a few pages at a time, so a count cut short picks up where
    it stopped.
  • A cancelled request for a camera's frame can no longer leave two conversions running on one
    scaler.
  • A plugin's panel saving its store no longer undoes a key its worker saved in the meantime, and a
    plugin.js that writes its store on every render no longer saves and redraws in a loop.
  • A plugin removed or reinstalled while its sign-in is being renewed no longer has its request sent,
    and a reinstall keeps the renewed sign-in. Disconnecting during a renewal stays disconnected.
  • A plugin that signs in can make its other requests before a client id is typed.
  • The redirect URI shown for a plugin's sign-in is right on a hub opened at a name that starts with
    localhost, such as localhost.lan.
  • A plugin's icon or screenshot with a capital in its path, such as shots/Panel.png, shows on its
    page.
  • Picture in picture and Spotify show their current screenshots in the plugin store.
  • A dialog the browser closes by itself, such as on a second Escape some seconds after the first or
    the Android back gesture, no longer leaves the page unable to scroll and later upload sheets
    hidden until a reload.
  • A double click or double tap no longer goes straight through a button that asks twice.
  • The preset remove button, the drag handles, the review frame buttons and the small chips are 44
    pixels on touch screens.
  • Keyboard focus moves to the next control when the one you pressed goes away, such as Next on the
    introduction's last page, Set crop, an attachment's remove button and a new token's Dismiss.
  • The print library shows every file again when the printer it was filtered by is removed.
  • A dashboard with plugin panels hidden counts them as panels, where it called them monitors.
  • The guide says a monitor's printer is optional.

Security

  • The bundled MediaMTX is updated to 1.21.1, past the advisories against 1.18.2. The worst let a
    request to the stream server exhaust its memory.
  • PyJWT is updated to 2.15.1 and DOMPurify to 3.4.16, past the advisories against the versions
    2.5.0 carried.
  • Other websites can't put the dashboard in a frame. An origin in PRINTGUARD_ORIGINS, such as a
    Home Assistant panel, still can.
  • A new API token's secret is only sent to the dashboard tab that created it, where every open
    tab used to receive it.
  • Saved keys, passwords and topic addresses are no longer sent to the dashboard, so they can't be
    read back from it. The field reads "Saved. Leave blank to keep it". Typing replaces the saved
    one and Clear removes it. A provider switch carries no saved key across.
  • A plugin that gates the hub and then fails refuses requests, the dashboard included, where it used
    to leave the hub open. A flood of requests can no longer switch a healthy gate off. Start the hub
    with PRINTGUARD_PLUGINS=off to remove a failed one.
  • A password or key in a printer, notifier or camera address no longer appears in warnings, errors,
    the log or bug reports, including a password that contains /, ?, #, quotes, backslashes or
    non-ASCII letters. An address with an @ later in its path or query loses everything between
    :// and the last @.
  • A login in MEDIAMTX_API, MEDIAMTX_RTSP or MEDIAMTX_HLS is scrubbed from warnings, errors, the log and bug
    reports.
  • A printer's or alert channel's request never follows a redirect, so an API key can't go on to
    another host. A test or a command sent to an address that redirects fails and names the address to
    register, where a pause, heater target, print start or text alert used to be reported as done and
    never arrive.
  • A private plugin catalogue's token is scrubbed from the dashboard, the API, bug reports and the
    log wherever it sits in the address, and its address shows its host with the path and query values
    hidden.
  • A web page open on the same computer as the desktop app can no longer read your camera addresses
    and passwords, run a command or watch a camera through the video server's ports, even by pointing
    a name at the computer. The video server's API and its HLS, RTSP and RTMP readers need a login the
    hub makes up at each start, in the image as well as the desktop app, and publishing takes none.
  • A dashboard plugin without the network permission can no longer send what it reads to another
    server by navigating its own frame or over WebRTC.
  • A web page on another origin can no longer read a camera stream through the hub.
  • A plugin without Reach your own network is only connected to a public address, checked when the
    connection opens, so a name that resolves to your network gets nowhere. That covers its requests,
    its WebSockets and its sign-in endpoint, and an IPv4 address written inside an IPv6 one, such as
    [64:ff9b::c0a8:101], counts as that IPv4 address.
  • An error that answers a command no longer reaches a plugin, and a plugin's own errors never quote
    what it sent, which stops a plugin testing guesses against your stored credentials.
  • A plugin's request can only refer to a sign-in's access token, so it can't send the refresh token
    or client id to a second host.
  • A plugin sign-in finishes only at the endpoint it started at and only while the plugin may still
    connect an account.
  • A plugin zip is read from the folder that holds its one plugin.json, and one that holds two or
    lists a file twice is refused.
  • state.json is never readable by other accounts while it's being saved.
  • The REST API checks the token before it reads a request body.
  • A request to /api/v1/classify can't make the hub listen for a network stream and stall
    detection, since only a JPEG or PNG is read.
  • A query string can't stand in for part of the path in a plugin's address pattern, so a plugin
    allowed /bot*/sendMessage reaches that endpoint only.
  • A plugin's request can't set a Host header to reach another site on the same server.
  • The hub talks to its own MediaMTX directly when HTTP_PROXY is set, so the stream server's login
    never goes to the proxy.
  • A plugin allowed to reach your network can no longer call the hub's own API, MCP server or live
    streams, which gave it state, camera stills and history it had no permission for.
  • A plugin guarding the hub that an update or restart switches off keeps refusing requests until you
    accept or remove it, where the hub was left open.
  • A plugin's server flooding its WebSocket, or a dashboard that stops reading, can no longer use up
    the hub's memory. A dashboard 256 events behind is disconnected and reconnects.
  • A plugin address pattern with a wildcard over the end of an IP address, such as
    http://*.168.1.1/*, needs Reach your own network and is listed under it.
  • A printer address saved without its scheme, such as admin:password@host, used to be shown with
    its password. It's now refused.
  • A key after a # in an address is redacted, and a percent-encoded password is scrubbed in its
    decoded form too.