The remaining routes of the idle-in-transaction class: the file-manager,
storage, camera-snapshot and timelapse routes each took their printer row
via Depends(get_db) and then talked FTP/camera on the same held session, so
a farm dashboard polling cover/snapshot tiles (offline printers included)
crept the pool to exhaustion over ~23h. They now read in a short session and
release before the I/O; timelapse re-opens a fresh session only for the write.
Also caps the four bare-executor FTP helpers with asyncio.wait_for so a
saturated 48-worker pool can't pin a caller (and its DB connection)
indefinitely, and runs the synchronous smtplib send off the event loop with
an explicit timeout so a wedged relay can't freeze the loop.
Pushover rejects priority-2 (Emergency) messages unless they carry retry
and expire. _send_pushover never sent them, so setting priority 2 always
failed with Pushover's "retry and expire are required" error. Now at
priority 2 we send retry/expire (default 60s/3600s, clamped to Pushover's
30-10800s range), surfaced as two provider fields shown only when priority
is 2. Added PushoverConfig schema fields, i18n labels across all locales,
and unit tests.
Reporter (email provider, "Reason: unknown" failures) wanted a camera snapshot
in failure emails. The finish-photo capture path shipped in 0.2.5b1 (#1397)
already loads JPEG bytes into archive_data["image_data"] for terminal print
events, and pushover/telegram/discord/ntfy users have been getting them.
Email was the one provider that dropped the bytes on the floor. Reporter
separately flagged that the Message Templates list shows "Print Completed"
and "User Print Completed" with no visual cue they're different dispatches.
Both fixes in one commit because they touch the same UI surface (Message
Templates) and both stem from the same reporter conversation.
1) Inline finish-photo embed in email — template-driven, opt-in.
_send_email now accepts finish_photo_url alongside image_data. Inline
embed fires only when bytes are present AND URL is set AND the rendered
body contains that URL — i.e. the user's template referenced the existing
{finish_photo_url} variable. The multipart/related shape wraps a
multipart/alternative (plain + HTML) plus an inline MIMEImage with
Content-ID: <bambuddy-finish-photo>. The HTML part replaces the escaped
URL in-place with the cid <img>, so the image appears WHERE the user put
the variable in the template, not stapled to the bottom. Plain-text part
keeps the URL as a clickable link for non-HTML clients.
First draft of this fix unconditionally inlined the photo whenever
image_data was present, which bypassed the template system. Reverted to
the template-driven contract: default templates unchanged, opt-in by
editing the template body to include {finish_photo_url}.
2) user_print_* template name disambiguation.
The four user_print_* templates are the per-user SMTP emails sent to the
print's submitter (advanced-auth-only path). They shared the "Print
Completed" / "Print Failed" / etc. short names with the broadcast
provider templates, so the Message Templates list was indistinguishable.
The EVENT_NAMES display map in routes/notification_templates.py already
used the disambiguated "… Email" labels, but the seed wrote the short
name to the DB.
DEFAULT_TEMPLATES now seeds the four user_print_* rows with " Email"
suffix so fresh installs are correctly labelled. New
_migrate_rename_user_print_template_names runs on startup and updates
existing rows where the name still matches the old default. Admin-edited
names are preserved. Standard SQL UPDATE works on both SQLite and
Postgres without dialect branching.
Split Obico failure-detection dispatch out of the multiplexed
on_printer_error event onto its own on_ai_failure_detection event so
users can subscribe to AI alerts without also enabling HMS hardware-
error pages, and so the discoverable label "AI Failure Detection" is
what subscribes them rather than the unrelated "Printer Error" toggle.
New column on notification_providers (default False, branched
SQLite/Postgres migration), new notification_service.on_ai_failure_detection
method, new ai_failure_detection template, obico_actions._notify swap.
Frontend gets a summary badge, a toggle row with description, and ntfy
priority surfacing. 14 new tests pin the routing + the regression guard
("Printer Error" alone must NOT receive AI notifications now). 11 locales
covered.
Existing providers keep working: HMS hardware errors continue to ride
on_printer_error unchanged; users who want spaghetti alerts opt in via
the new toggle.
The notification service's httpx client was the only outbound client in
the codebase still leaking python-httpx/<version> as User-Agent; all
other clients identify as Bambuddy/1.0 since the May 2026 compliance
pass. Bring it in line.
The reporter's ntfy server was behind a Cloudflare Tunnel and CF returned
its JS challenge page (Just a moment...) to every API request — confirmed
by reproducing the same 403 with curl. Cloudflare can't be solved from a
backend, so add detection for the challenge shape (Server: cloudflare or
cf-mitigated header, or <!DOCTYPE html>...Just a moment... body) and
return an actionable error message that points at the real fix on the
user's CF side instead of dumping the raw HTML.
Normal 403s (auth failures with plain text bodies) still surface the
original body so genuine errors stay debuggable.
Discord's "Copy Webhook URL" button emits discordapp.com URLs; both
hostnames serve the same webhooks. The validation now accepts either
prefix while keeping the check itself in place to catch the
paste-the-wrong-thing error.
Pre-fix, _background_notifications in main.py:3434 built archive_data
with print_time_seconds (the slicer's pre-print estimate parsed from
the 3MF at archive creation), and notification_service.py:909 formatted
that field straight into the {{duration}} template variable. A print
cancelled 2 minutes into a 3-hour estimate notified "duration: 3h".
Compute actual_time_seconds from started_at/completed_at in main.py and
add it to archive_data. notification_service.py prefers it, falls back
to print_time_seconds when the actual can't be derived.
Also add "cancelled" to the list of statuses that get completed_at set
in update_archive_status — pre-fix only completed/failed/aborted got a
timestamp, so queue-UI cancellations had no actual elapsed to compute
from. Audited every completed_at consumer; none depend on NULL to mean
"cancelled" (status field already carries that signal), and the
statistics-totals aggregation gets more accurate too as a side effect.
3 new regression tests in TestNotificationVariableFallbacks pin the
{{duration}} variable contract (actual wins over estimate; estimate
falls in when actual is missing; "Unknown" when both absent).
ntfy supports a Priority header (1=min, 2=low, 3=default, 4=high, 5=urgent)
that controls escalation on the receiving device, but every event was being
sent at the server default — so a "50% complete" ping looked identical to
"print failed" or "printer offline". Add a per-event priority dropdown
section in the Add/Edit Notification modal (visible only for ntfy, listing
only enabled events); the backend reads config.event_priorities and emits
the matching Priority header on POST and PUT (image-attachment) paths.
Unmapped events fall through to the ntfy server default. Out-of-range
and non-numeric values are dropped, not clamped, so a misconfigured value
never silently sends at the wrong urgency. Test sends omit the header by
design so the test path can't accidentally page someone at urgent priority.
Backward compatible: existing providers without event_priorities behave
exactly as before. NtfyConfig.event_priorities is optional; the route
stores config as a JSON blob so no migration is needed.
i18n: full translations across all 8 locales (en/de/fr/it/ja/pt-BR/zh-CN/
zh-TW). README, CHANGELOG, and the wiki notifications page updated.
Tests: 6 backend (Priority set on mapped, omitted on unmapped/missing/
no-priorities, ignored for bad values, propagated through attachment
path), 6 frontend (section visible only for ntfy, lists only enabled
events, save round-trip, edit pre-fill, toggle drops row, non-ntfy
never writes the key).
Thread event_type and template variables through the webhook call chain so
all generic webhook payloads include an "event" field and event-specific
data (printer, filename, duration, etc.) as top-level JSON fields. Existing
title/message/timestamp/source fields are unchanged. Slack format unaffected.
Ntfy notifications with camera snapshots failed when the printer name
or filename contained non-ASCII characters. httpx enforces ASCII
encoding on string header values, but the Title and Message headers
can contain printer names with accented letters or CJK characters.
Encode these header values as UTF-8 bytes, which ntfy handles correctly.
Test notifications were unaffected because they use a hardcoded ASCII
title and no image attachment.
Webhook providers did not include image data (e.g. camera snapshots
from first layer complete notifications) even though other providers
like Telegram, Pushover, and Discord already attached them. The webhook
payload now includes a base64-encoded "image" field when a snapshot is
available (generic format only, excluded from Slack format).
Notify users when the first layer finishes printing so they can check
adhesion remotely. Triggers once per print when layer 2 begins
(layer_num >= 2, capped at <= 5 to handle reconnects). Includes a
camera snapshot attachment. Adds the on_first_layer_complete toggle
to all notification providers, with backend/frontend/i18n support
across all 7 locales.
Sends persistent notifications to the HA dashboard using the existing
HA connection from Settings. Zero config — just select "Home Assistant"
as provider type. Users can forward notifications to mobile via HA
automations.
Add {eta} template variable showing wall-clock completion time
(e.g. "15:53" or "3:53 PM") to print_start, print_progress, and
queue_job_started notifications. Respects the user's time_format
setting (12h/24h). The existing {estimated_time} variable continues
to show duration ("1h 23m").
All backend timestamps used datetime.now() (server local time) or the
deprecated datetime.utcnow(). The frontend's parseUTCDate() assumes
timestamps without timezone indicators are UTC and appends 'Z', so
stored timestamps were off by the timezone offset when the container's
timezone wasn't UTC.
Backend: replaced datetime.now() and datetime.utcnow() with
datetime.now(timezone.utc) across 16 files (~80 call sites) for all
database fields and DB comparisons. Cosmetic timestamps (filenames,
user-facing local time formatting) intentionally left as local time.
Frontend: replaced 13 new Date(backendTimestamp) calls with
parseUTCDate() across 8 files to correctly interpret UTC timestamps.
ntfy servers with attachments disabled reject thumbnail uploads with
"attachments not allowed" (HTTP 400 / code 40014), causing the entire
notification to fail. Now automatically retries without the image so
the notification still gets delivered.
When sending notifications with image attachments (progress/error
events), the message body was placed in an HTTP Message header. Messages
containing newlines (e.g. "Printer: file.stl\nRemaining: 4h 52m") were
rejected by httpx since HTTP headers cannot contain newline characters.
Escape newlines to literal \n which ntfy renders as line breaks.
Pushover and Discord already received print thumbnails, but Telegram
and ntfy only got text. Telegram now uses sendPhoto with the image as
a caption attachment. ntfy sends the image as a binary PUT with
Filename/Message headers. No config changes needed.
Notify users when the print bed cools below a configurable threshold
(default 35°C) after a print finishes, so they know when to remove parts.
- Backend: DB migration, model, schemas, notification template, service
method, background cooldown monitor (polls every 15s, 30min timeout)
- Frontend: event toggle in provider card/modal, threshold setting in
Settings > Notifications, i18n keys for all 5 locales
- Tests: 4 backend + 4 frontend tests
- Docs: README, website, wiki updated
filament_details now shows which AMS unit and tray each filament came
from (e.g. "AMS-A T1 PLA: 12.4g | AMS-A T3 PETG: 2.8g"). External
spool holders show "Ext". Falls back to type-only format when usage
tracking data is unavailable (e.g. Spoolman mode).
- usage_tracker: include spool material in result dicts
- main.py: pass usage_results to notification flow via archive_data
- notification_service: build filament_details with AMS labels when
usage_results available, fall back to existing slot-based format
- Update sample data and changelog
3MF slicer estimates are now the primary tracking source for ALL spools
(BL and non-BL), with AMS remain% delta as fallback. Per-layer G-code
analysis provides accurate partial usage for failed/cancelled prints.
Fix wrong-spool tracking bug: 3MF slot_id was mapped directly to AMS
tray position, but the printer remaps slicer slots at print time. Now
uses queue ams_mapping for queue prints and tray_now from printer state
for single-filament non-queue prints.
Add filament_grams, filament_details, and progress template variables
to print_complete, print_failed, and print_stopped notification events.
Webhook payloads include per-slot filament breakdown. Per-slot data
stored in archive extra_data during 3MF parsing.
Attach printer camera snapshots to Discord webhook notifications using
multipart form-data with embed image references. Images are included
for print start, complete, progress milestones, and error events.
When no image is available, notifications send as text-only as before.
Underscores in job names or error codes caused parse_mode to be
disabled entirely, rendering *bold* markers as literal asterisks.
Now escapes underscores in the body with \_ so Markdown stays enabled.
Model-based queue assignment:
- Extract printer_model from sliced 3MF files during upload
- Display sliced-for model in archive view
- New queue mode: assign to "Any [Model]" instead of specific printer
- Scheduler auto-assigns to first idle printer of matching model
- Filament validation: only assign to printers with required filament types loaded
- Waiting reason display shows why jobs are waiting (e.g., "Waiting for filament: Printer1 (needs PLA)")
- "Waiting" status badge (purple) distinguishes from regular "Pending"
Queue notifications (7 new events):
- Job Added: When a job is added to queue
- Job Assigned: When a model-based job is assigned to a printer
- Job Started: When a queue job starts printing
- Job Waiting: When a job is waiting for filament (enabled by default)
- Job Skipped: When a job is skipped due to previous failure (enabled by default)
- Job Failed: When a job fails to start (enabled by default)
- Queue Complete: When all queued jobs finish
Backend changes:
- New columns: print_queue.target_model, print_queue.required_filament_types, print_queue.waiting_reason
- New columns: notification_providers.on_queue_job_* (7 event triggers)
- Notification templates for all queue events
- Scheduler validates filament compatibility before model-based assignment
- Queue API extracts filament types from 3MF when adding model-based items
- Local backup/restore includes queue notification settings
Frontend changes:
- TypeScript interfaces updated for new fields
- Queue page shows waiting reason and "Waiting" badge
- Notification settings includes "Print Queue" section with 7 toggles
Closes#162
Root Cause: The generic webhook sent a JSON payload with custom field names plus timestamp and source fields. Mattermost/Slack webhooks only accept {"text": "..."}
format and reject unknown fields with HTTP 400.
Users just need to select "Slack / Mattermost" from the Payload Format dropdown when configuring their Mattermost webhook.
Closes#133
- Add {finish_photo_url} template variable for print_complete, print_failed,
print_stopped events
- Photo capture now completes before notification is sent (ensures image exists)
- Add External URL setting in Settings → Network (auto-detects from browser)
- Full URL constructed using external_url setting for external services
- Fix Telegram Markdown parsing error when messages contain URLs
- Add backend schema for external_url setting
- Add unit test for finish_photo_url variable passing
- New APIBrowser component with full OpenAPI schema integration
- Fetches and parses /openapi.json automatically
- Groups endpoints by API tags (printers, archives, settings, etc.)
- Expandable endpoint sections with color-coded method badges
- Path parameter, query parameter, and JSON body editors
- Auto-populates request body with schema examples
- Live API request execution with response display
- Response shows status code, timing, and formatted JSON
- Copy response button with clipboard fallback
- Search to filter endpoints across all categories
- Expand All / Collapse All buttons
- Link to Swagger UI (/docs)
- Two-column layout for API Keys tab
- Left: API key management + webhook documentation
- Right: API Browser with dedicated test key input
- Parameter validation
- Shows warning for missing required parameters
- Validates before sending requests to avoid 422 errors
- UX improvements
- "Use in API Browser" button on newly created keys
- Responsive layout (stacked on mobile, side-by-side on xl+)
Problem: At print start, the MQTT mc_remaining_time was often 0 or unavailable because the printer hadn't fully processed the gcode yet.
Solution: Modified the notification system to prioritize the print_time_seconds from the 3MF file parsing (which is always available after archive creation) over the
MQTT remaining time.
Files changed:
- backend/app/services/notification_service.py:
- Added optional archive_data parameter to on_print_start() method
- Added priority logic: archive data > MQTT remaining_time > raw_data mc_remaining_time
- backend/app/main.py:
- Created _send_print_start_notification() helper function
- Moved notification sending to AFTER archive creation (when print_time_seconds is available)
- Added fallback notification sending at early return points (auto_archive disabled, file not found, etc.)
Issue 2: Timelapse Auto-Assignment
Problem: Timelapses couldn't be auto-assigned reliably because printer dates could be off in LAN-only mode, making timestamp matching unreliable.
Solution: Track whether timelapse recording was active during the print via MQTT, and if so, auto-fetch the most recent timelapse file on print completion.
Files changed:
- backend/app/services/bambu_mqtt.py:
- Added _timelapse_during_print instance variable
- Track when timelapse is active during a print (in both data and xcam parsing)
- Reset the flag on new print start
- Include timelapse_was_active in the print complete callback data
- backend/app/main.py:
- Added timelapse auto-scan logic in on_print_complete()
- If timelapse_was_active is True and status is "completed":
- Wait 5 seconds for timelapse to finalize
- Scan printer's timelapse directory
- Find most recent file by mtime
- Verify it was modified within 30 minutes of print completion
- Download and attach to archive
- Send WebSocket update
- Separate AMS and AMS-HT notification switches (one per device type)
- Fix notification variables not showing (duration, filament, estimated_time)
- Add fallback values for empty notification variables ("Unknown" instead of blank)
Settings:
- Fix API keys badge count only showing after visiting tab
- Move External Links card to third column above Updates
- Add Release Notes modal for viewing full notes before updating
Statistics:
- Fix filament usage trends not showing (wrong API parameters)
- Move dashboard controls (Hidden, Reset Layout) to header row
- Remove duplicate Reset Layout button
Camera:
- Fix ffmpeg processes not killed when closing webcam window
- Add /camera/stop endpoint with POST support for sendBeacon
- Track active streams and proper cleanup on disconnect
Backend:
- pytest configuration with async support and coverage
- Unit tests for notification service (23 tests)
- Unit tests for smart plug manager (12 tests)
- Unit tests for archive service (16 tests)
- Integration tests for API endpoints
- Fix: notifications now send immediately (digest is summary only)
Frontend:
- Vitest configuration with jsdom and coverage
- MSW for API mocking
- Component tests for Toggle, Button, Card, ConfirmModal (77 tests)
- Test utilities with custom render wrapper
CI/CD:
- GitHub Actions workflow for automated testing
- Backend lint, unit tests, integration tests
- Frontend lint, type-check, unit tests, build