Skip to content

fix(server): handle missing uhubctl + missing DYMO without traceback spam - #36

Merged
szrudi merged 9 commits into
mainfrom
fix/uhubctl-missing-graceful
May 22, 2026
Merged

szrudi merged 9 commits into
mainfrom
fix/uhubctl-missing-graceful

Conversation

@szrudi

@szrudi szrudi commented May 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Local dev outside Docker, or a Pi without a printer plugged in, would spam the npm run dev console with full Python tracebacks on two different code paths — both of which are expected states, not errors. This PR converts both into clean log lines and 404 responses without changing API contracts.

The two issues

1. uhubctl not on PATH (/api/power/status)

Every poll raised FileNotFoundError out of find_or_recall_printer_port() — before the endpoint's try/except caught it — and the frontend's auto-retry produced a wall of tracebacks. The frontend already treats 404 from /api/power/status as "hide the power-toggle UI" (see client/src/components/PowerToggle.tsx:6-10), so the cleanest fix is to convert "uhubctl missing" into that same 404 path.

Changes in server/usb_power.py:

  • _run() logs a single WARNING the first time uhubctl can't be executed (latched via _uhubctl_missing_logged), then re-raises FileNotFoundError unchanged. Wording is general enough for both "not on PATH" and "UHUBCTL_BIN points at a missing path".
  • find_printer_port() propagates FileNotFoundError unchanged — cleaner contract (the function does what it says, no hidden masking).
  • find_or_recall_printer_port() catches FileNotFoundError and returns None, explicitly bypassing _last_known_port. Without that bypass, a cached port from a prior run on a host that had uhubctl would still get returned, and the endpoint would proceed to get_port_status() / power_* and 500-traceback there.

2. No DYMO printer connected (/api/printers)

list_printers() caught all scan exceptions and called traceback.print_exc(), but labelle raises DeviceManagerNoDevices as its normal "no DYMO plugged in" signal. Every page-load scan logged a 5-line stack trace.

Changes in server/printer_service.py:

  • Catch DeviceManagerNoDevices specifically and surface it as a single INFO line per scan. Other unexpected exceptions still print a traceback so genuine USB-stack failures aren't swallowed silently.
  • No latch: scans are user-triggered (page load / manual refresh), not polled, and plug/unplug events should be visible in the log.

Tests

  • server/tests/test_usb_power.py — new TestUhubctlMissing class: find_printer_port raises; find_or_recall returns None with no cache; find_or_recall bypasses cache when uhubctl missing (regression); WARNING is logged exactly once.
  • server/tests/test_printer_service.py — new test_no_devices_logs_info_without_traceback: confirms DeviceManagerNoDevices produces an INFO log + empty result, no traceback.
  • Full suite: 218/218 passing.

Before / after on npm run dev

Before: every poll prints either the Flask + subprocess traceback (uhubctl path) or the labelle scan traceback (printers path).

After:

WARNING:usb_power:uhubctl executable not found ('uhubctl'); USB power-cycle features disabled. Install uhubctl (e.g. `sudo apt install uhubctl`) or set UHUBCTL_BIN to a working path.
INFO:printer_service:No USB DYMO printers detected.

The WARNING is logged once per process; the INFO appears per scan (cheap, useful diagnostic across plug/unplug).

Sequencing note

No version bump in this PR — leaving the call to whoever merges first. If this lands before #34, bump #34 from 1.6.5 → 1.6.6 (and #35 from 1.6.6 → 1.6.7).

Test plan

  • pytest server/tests/ — 218 passed (29 usb_power + 9 printer_service)
  • Local npm run dev no longer spams tracebacks; one WARNING + INFOs on first poll
  • Power toggle UI hides cleanly (already behaves this way on 404)
  • Plug/unplug a DYMO and watch the INFO log toggle on each scan

🤖 Generated with Claude Code

Local dev outside Docker doesn't have uhubctl on PATH. Without it,
every /api/power/status poll raised FileNotFoundError out of
find_or_recall_printer_port() — *before* the endpoint's try/except
caught it — and the frontend retried, producing a wall of tracebacks
on every page open.

Now:
- _run() logs a single WARNING the first time uhubctl isn't found,
  then re-raises FileNotFoundError unchanged
- find_printer_port() catches FileNotFoundError and returns None, so
  _printer_port_or_404 returns a clean 404 "Printer not detected"

The frontend already treats 404 as "hide the power-toggle UI" (see
PowerToggle.tsx:6-10), so this works without any client change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@szrudi
szrudi force-pushed the fix/uhubctl-missing-graceful branch from 17dcf99 to 605cb09 Compare May 22, 2026 12:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to prevent repeated 500 tracebacks when uhubctl is missing by turning that condition into the existing “no controllable printer” 404 behavior and logging a single warning instead of spamming logs.

Changes:

  • Add “log warning once” behavior when uhubctl is missing and translate FileNotFoundError into None from find_printer_port().
  • Add tests asserting find_printer_port() returns None and that the warning is only logged once.
  • Also includes unrelated changes: a client footer migration banner and a package.json version bump (both appear out of scope vs PR description/title).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
server/usb_power.py Catch missing uhubctl, log once, and return None so API can return 404 instead of 500 spam.
server/tests/test_usb_power.py Adds coverage for “uhubctl missing” return-None and log-once behavior.
package.json Bumps version to 1.6.5 (contradicts PR description stating no version bump).
client/src/components/Footer.tsx Adds a repository-migration banner (unrelated to usb-power fix).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/usb_power.py Outdated
Comment thread server/tests/test_usb_power.py
Address Copilot review on #36:

If state.json from a prior run on a uhubctl-equipped host has saved
_last_known_port, the previous version of find_or_recall_printer_port
would fall back to the cached port even when uhubctl had been removed.
The endpoint would then proceed to get_port_status()/power_*, which
also shell out via _run() → another FileNotFoundError → 500 with
traceback. Same symptom this PR aims to eliminate.

- find_printer_port now propagates FileNotFoundError (no longer hides
  it as None). Cleaner contract: the function does what it says.
- find_or_recall_printer_port catches FileNotFoundError → returns
  None, explicitly bypassing _last_known_port. /api/power/* then
  renders the same clean 404 as the no-cache path.
- Regression test test_find_or_recall_bypasses_cache_when_uhubctl_missing
  exercises the previously-broken scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comment thread server/usb_power.py Outdated
Comment thread server/usb_power.py
Address Copilot review: the previous message said "uhubctl not found on
PATH", which is misleading when UHUBCTL_BIN is set to an explicit path
that doesn't exist (no PATH lookup happens in that case). The new
wording is true for both scenarios and points at the env var as an
alternative remedy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread server/usb_power.py
szrudi and others added 2 commits May 22, 2026 14:43
Copilot flagged the missing lock on the log-once latch. The race is
real but benign (microsecond window, self-limiting, max N duplicate
log lines per process lifetime) — same character of trade-off as the
existing unlocked _last_known_port. Document the choice in-place so
future readers don't have to re-derive it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t_printers

list_printers() catches all scan exceptions and dumps a full traceback,
but labelle raises DeviceManagerNoDevices as its normal "no DYMO
plugged in" signal — so every /api/printers call on a host without a
printer was logging a 5-line stack trace.

Catch DeviceManagerNoDevices specifically and surface it as a single
INFO line per scan. Other unexpected exceptions still print a
traceback so genuine USB-stack failures don't get swallowed silently.

No latch: scans are user-triggered (page load / manual refresh), not
polled, and plug/unplug should be observable in the log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@szrudi szrudi changed the title fix(usb-power): handle missing uhubctl without 500-spam fix(server): handle missing uhubctl + missing DYMO without traceback spam May 22, 2026
@szrudi
szrudi requested a review from Copilot May 22, 2026 12:54
…isible

Adding logger.info() in printer_service was effectively dead code in
production: Python's default root logger threshold is WARNING, so INFO
was silently dropped. Tests passed because pytest's caplog captures at
any level via propagation, regardless of root level — a misleading
green light.

Set logging.basicConfig in app.py before any module-level logger fires.
Default level INFO so service diagnostics show up; honor LOG_LEVEL env
var so production can dial it up or down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread server/usb_power.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment thread server/usb_power.py Outdated
Comment thread server/app.py Outdated
szrudi and others added 3 commits May 22, 2026 15:02
…ple in dev

Earlier commit defaulted to INFO; the user point is that production
should be quieter and dev should be even chattier. Net result:

- app.py default LOG_LEVEL = WARNING (production-safe — operators only
  see things worth looking at, no noise from "no DYMO detected" INFO
  on every page load).
- .env.example sets LOG_LEVEL=DEBUG so `npm run dev` is verbose via the
  template that fresh contributors copy.

Also picks up Copilot's docstring nit: find_printer_port now says "the
uhubctl executable can't be found", matching the more general warning
wording instead of the over-narrow "isn't on PATH".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tartup

Copilot review: passing an unrecognized string (typo like INFOO, or
numeric like "20") to logging.basicConfig(level=...) raises ValueError
at import time, preventing the server from starting. Easy to introduce
in production via a misedit of .env.

Resolve LOG_LEVEL via getattr with an isinstance(int) check; fall back
to WARNING when the value doesn't resolve to a logging level constant.
After logging is configured, surface the bad input as a single WARNING
so operators see it without the server failing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PATCH bump: server-side dev-console cleanups (missing-uhubctl 500-spam
gone, DeviceManagerNoDevices traceback gone, defensive LOG_LEVEL
parsing). No user-visible behavior change for the happy path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@szrudi
szrudi merged commit a2c46c2 into main May 22, 2026
4 of 5 checks passed
@szrudi
szrudi deleted the fix/uhubctl-missing-graceful branch May 22, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants