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:
maziggy
2026-06-09 12:20:01 +02:00
parent 60e31634b8
commit f87b5bb3b8
20 changed files with 362 additions and 123 deletions
@@ -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.