mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-05 05:31:31 +02:00
Obico's ml_api container takes an optional ML_API_TOKEN environment variable.
With it set, ml_api/auth.py answers a bare 401 to any request whose
Authorization header isn't "Bearer <token>"; with it unset it ignores the
header entirely. Bambuddy never sent one, so pointing it at a protected server
meant deleting the token there — which the reporter had set for their Home
Assistant integration and did not want to undo.
Settings -> Failure Detection gains an ML API Token field. When it is empty no
header is sent, so an unconfigured install's request stays byte-identical to
what shipped before the setting existed.
This failed in the worst possible way, and that is the more important half of
the change. Obico decorates /p/ with token_required but leaves /hc/ open. Test
Connection pinged /hc/, so it reported success against a server that was
rejecting every real detection call, the settings looked right, and detection
silently never ran. The only symptom was a generic "ML API call failed" buried
in the status card.
So the test now proves what it claims. After health passes it probes GET /p/
with no img parameter: the auth decorator runs before the handler, so 401 means
the token was rejected and 422 ("Invalid request params") means it was
accepted. No inference work is done either way. A probe that itself errors
reports the token as unknown rather than as working — the UI says it could not
be checked instead of claiming success.
The detection loop checks for 401 before raise_for_status, so a rejected token
is reported as a rejected token, naming the setting and the environment
variable, instead of surfacing "401 Unauthorized" with no hint of what to do.
The message never contains the token; a test pins that.
The setting name carries "token", so the support bundle's keyword redactor
masks it with no new rule. Resolving "field omitted" to the saved token is the
route's job, keeping test_connection a pure outbound call with no database
access.
Second fix, same issue: support bundles misreported which printers Obico
watches. The bundle split obico_enabled_printers on commas and read an empty
value as "no printers". The settings UI writes a JSON array, and empty means
*all* printers — the default — so a working Obico setup showed obico_enabled
false against every printer in its own bundle. That is the reporter's bundle
exactly, and it points anyone reading it at the wrong subsystem. The bundle now
parses the setting the way ObicoDetectionService does, keeps a comma fallback
for any install that stored the legacy shape, and factors in the global switch.
100 lines
3.8 KiB
Python
100 lines
3.8 KiB
Python
"""API routes for Obico AI failure detection."""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Response
|
|
from pydantic import BaseModel
|
|
|
|
from backend.app.core.auth import RequirePermissionIfAuthEnabled
|
|
from backend.app.core.permissions import Permission
|
|
from backend.app.models.user import User
|
|
from backend.app.services.obico_detection import obico_detection_service, pop_frame
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/obico", tags=["obico"])
|
|
|
|
|
|
class TestConnectionRequest(BaseModel):
|
|
url: str
|
|
# Omitted entirely = test with the saved token; "" = test with no token.
|
|
token: str | None = None
|
|
|
|
|
|
@router.get("/status")
|
|
async def get_status(
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_READ),
|
|
):
|
|
"""Scheduler status, per-printer classification, and recent detection history."""
|
|
settings = await obico_detection_service._load_settings()
|
|
status = obico_detection_service.get_status(settings["sensitivity"])
|
|
return {
|
|
**status,
|
|
"enabled": settings["enabled"],
|
|
"ml_url": settings["ml_url"],
|
|
"sensitivity": settings["sensitivity"],
|
|
"action": settings["action"],
|
|
"poll_interval": settings["poll_interval"],
|
|
"external_url_configured": bool(settings["external_url"]),
|
|
}
|
|
|
|
|
|
@router.get("/printer-status")
|
|
async def get_printer_status(
|
|
user: User | None = RequirePermissionIfAuthEnabled(Permission.PRINTERS_READ),
|
|
):
|
|
"""Per-printer live classification for the printer cards (#1546).
|
|
|
|
Deliberately excludes configuration (ML URL, action, history) so users
|
|
with printers:read but no settings:read can still render the badge.
|
|
"""
|
|
settings = await obico_detection_service._load_settings()
|
|
enabled_printers = settings["enabled_printers"]
|
|
# Error strings can embed configured URLs (ML API base, external URL), so
|
|
# they stay behind settings:read like the rest of the configuration.
|
|
can_see_error = user is None or user.has_permission(Permission.SETTINGS_READ.value)
|
|
return {
|
|
"enabled": settings["enabled"],
|
|
# None = all printers are monitored
|
|
"monitored_printers": sorted(enabled_printers) if enabled_printers is not None else None,
|
|
"per_printer": obico_detection_service.get_per_printer(),
|
|
"last_error": obico_detection_service._last_error if can_see_error else None,
|
|
}
|
|
|
|
|
|
@router.post("/test-connection")
|
|
async def test_connection(
|
|
req: TestConnectionRequest,
|
|
_: User | None = RequirePermissionIfAuthEnabled(Permission.SETTINGS_UPDATE),
|
|
):
|
|
"""Ping the Obico ML API health endpoint and check the token. Returns ok + raw body."""
|
|
if not req.url:
|
|
return {"ok": False, "status_code": None, "body": None, "error": "URL is empty", "auth_ok": None}
|
|
token = req.token
|
|
if token is None:
|
|
# Field omitted entirely — test what the service actually uses.
|
|
settings = await obico_detection_service._load_settings()
|
|
token = settings.get("ml_token") or ""
|
|
return await obico_detection_service.test_connection(req.url, token)
|
|
|
|
|
|
@router.get("/cached-frame/{nonce}")
|
|
async def cached_frame(nonce: str):
|
|
"""Serve a pre-captured JPEG to the Obico ML API.
|
|
|
|
The detection loop captures a snapshot locally (where we control the timeout),
|
|
stashes the bytes under a one-shot random nonce, then hands this URL to Obico's
|
|
ML API. Obico's hardcoded 5s read timeout never races our snapshot pipeline.
|
|
|
|
Unauthenticated: the unguessable 32-byte nonce is single-use and expires in
|
|
seconds, so exposing this path doesn't widen the camera access surface.
|
|
"""
|
|
data = await pop_frame(nonce)
|
|
if data is None:
|
|
raise HTTPException(status_code=404, detail="Frame not found or expired")
|
|
return Response(
|
|
content=data,
|
|
media_type="image/jpeg",
|
|
headers={"Cache-Control": "no-store"},
|
|
)
|