mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 01:57:41 +02:00
feat(diagnostic, archives): install step 4 — proactive check + reactive banner
Two complementary surfaces for the most-missed install step ("Store sent
files on external storage"):
1. Connection diagnostic check (printer-side variant)
- Reads state.store_to_sdcard, parsed from MQTT home_flag bit 11.
- Pass / fail / skip; instant, no I/O.
- Catches the newer-firmware variant where the toggle moved onto the
printer itself (P2S 01.02 / Studio 2.6+).
An FTP upload-and-verify probe was tried first and rejected. /cache
is always writable from Bambuddy regardless of the slicer setting;
only BambuStudio's own behaviour changes when the toggle flips.
Empirically confirmed against X1C + H2D with the slicer option
toggled off: probe still succeeded, home_flag bit 11 stayed True.
2. Archives-page banner (slicer-side variant)
- The slicer-side toggle is invisible to the printer — older
BambuStudio doesn't push the change to the printer. The diagnostic
can't see it.
- Symptom is deterministic: archiver creates rows with
extra_data.no_3mf_available=True (main.py:2770) when it can't pull
the 3MF from /cache after a slicer-initiated print.
- New endpoint GET /archives/no-3mf-warning returns whether any
archive in the last 30 days has the flag (excluding soft-deleted).
- Amber dismissible banner at the top of /archives; one-shot
localStorage dismissal (matches Layout.tsx update-banner pattern,
but persistent across sessions).
- React-Query disabled after dismissal so the endpoint isn't polled
once the user has been told.
This commit is contained in:
@@ -427,6 +427,39 @@ async def list_archives(
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/no-3mf-warning")
|
||||
async def no_3mf_warning(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User | None = RequirePermissionIfAuthEnabled(Permission.ARCHIVES_READ),
|
||||
):
|
||||
"""Whether to nudge the user about install step 4 ("Store sent files on
|
||||
external storage"). True iff any archive in the last 30 days was created
|
||||
via the no-3MF fallback path — that's the deterministic symptom of the
|
||||
slicer-side variant of the setting being off.
|
||||
|
||||
Complements the connection-diagnostic ``external_storage`` check, which
|
||||
only catches the printer-side variant of the setting. On older slicers
|
||||
where the toggle lives only in BambuStudio, the printer never reports it
|
||||
and the diagnostic passes — this endpoint surfaces the symptom instead.
|
||||
|
||||
Dismissal is handled client-side via localStorage (one-shot): once the
|
||||
user has been told, no further nudge until they clear browser storage.
|
||||
The backend stays stateless.
|
||||
"""
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(days=30)
|
||||
result = await db.execute(
|
||||
select(PrintArchive.extra_data).where(
|
||||
PrintArchive.created_at >= cutoff,
|
||||
PrintArchive.deleted_at.is_(None),
|
||||
PrintArchive.extra_data.isnot(None),
|
||||
)
|
||||
)
|
||||
for (extra_data,) in result.all():
|
||||
if extra_data and extra_data.get("no_3mf_available"):
|
||||
return {"has_fallback": True}
|
||||
return {"has_fallback": False}
|
||||
|
||||
|
||||
@router.get("/slim", response_model=list[ArchiveSlim])
|
||||
async def list_archives_slim(
|
||||
date_from: date | None = Query(None),
|
||||
|
||||
@@ -534,6 +534,96 @@ class TestArchivesAPI:
|
||||
assert "successful_prints" in result
|
||||
|
||||
|
||||
class TestNo3MFWarning:
|
||||
"""`GET /archives/no-3mf-warning` — install step 4 reactive nudge.
|
||||
|
||||
The connection diagnostic's external_storage check only catches the
|
||||
printer-side variant of the setting (newer firmware). For older slicers
|
||||
where the toggle lives only in BambuStudio, the printer never reports
|
||||
it. The fallback path in main.py creates the archive with
|
||||
extra_data.no_3mf_available=True; this endpoint exposes that as a
|
||||
boolean so the frontend can surface a one-time banner.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_returns_true_when_recent_fallback_exists(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
printer = await printer_factory()
|
||||
await archive_factory(printer.id, extra_data={"no_3mf_available": True})
|
||||
|
||||
response = await async_client.get("/api/v1/archives/no-3mf-warning")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"has_fallback": True}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_returns_false_when_no_archives(self, async_client: AsyncClient):
|
||||
response = await async_client.get("/api/v1/archives/no-3mf-warning")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"has_fallback": False}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_returns_false_when_only_normal_archives(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
printer = await printer_factory()
|
||||
# extra_data has other keys but no_3mf_available is absent — normal
|
||||
# archives must not trigger the nudge.
|
||||
await archive_factory(printer.id, extra_data={"makerworld_url": "https://example"})
|
||||
await archive_factory(printer.id, extra_data=None)
|
||||
|
||||
response = await async_client.get("/api/v1/archives/no-3mf-warning")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"has_fallback": False}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_ignores_archives_older_than_30_days(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from backend.app.models.archive import PrintArchive
|
||||
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id, extra_data={"no_3mf_available": True})
|
||||
# Backdate past the 30-day window — old fallbacks are forgiven.
|
||||
archive.created_at = datetime.now(timezone.utc) - timedelta(days=45)
|
||||
await db_session.commit()
|
||||
|
||||
response = await async_client.get("/api/v1/archives/no-3mf-warning")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"has_fallback": False}
|
||||
# Sanity: row really is in the DB, we just don't surface it.
|
||||
assert (await db_session.get(PrintArchive, archive.id)) is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_ignores_soft_deleted_fallbacks(
|
||||
self, async_client: AsyncClient, archive_factory, printer_factory, db_session
|
||||
):
|
||||
from datetime import datetime, timezone
|
||||
|
||||
printer = await printer_factory()
|
||||
archive = await archive_factory(printer.id, extra_data={"no_3mf_available": True})
|
||||
archive.deleted_at = datetime.now(timezone.utc)
|
||||
await db_session.commit()
|
||||
|
||||
response = await async_client.get("/api/v1/archives/no-3mf-warning")
|
||||
|
||||
assert response.status_code == 200
|
||||
# Soft-deleted fallbacks have been actioned (user clearing the
|
||||
# evidence). Stop nudging.
|
||||
assert response.json() == {"has_fallback": False}
|
||||
|
||||
|
||||
class TestPrintLogEntryDelete:
|
||||
"""#1687: per-row delete on the Print Log page.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user