Browse Source

Startup: create the HMS notified-faults table (#3502)

- init_db() did not import the hms_notified_fault model, so
  create_all never made its table: every start logged "no such
  table: hms_notified_faults" and HMS notifications were sent again
  after each restart, as before #3486
- a test now starts init_db() in a clean process on an empty
  database and checks that every model's table exists; the suite
  imports all models itself, which is why this went unnoticed
maziggy 1 day ago
parent
commit
933801345b

+ 1 - 0
CHANGELOG.md

@@ -20,6 +20,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 - **"Any model" jobs go to the printer with the fewest print hours (#3480, sponsor vote Q4 2026)** — When several printers of a model can take a job, the queue used to pick the first one in its list, so on a farm the same printer got most of the work. It now picks the one with the fewest print hours (the Maintenance counter, including baseline hours entered there), so jobs and wear spread across the farm. Preferred colours still come first, and the queue powers on the least used printer too when it has to switch one on with an Auto Power On plug.
 
 
 ### Fixed
 ### Fixed
+- **HMS notifications are no longer sent again after a restart (#3502, reported by @pleasantone)** — The fix for #3486 remembers which printer errors were already notified, in a table that was never created: startup did not load its model, so every start logged "no such table: hms_notified_faults" and the errors were sent again. The table is now created on the next start.
 - **A backup from Windows restored on Linux or macOS finds its files (#3501, reported by @Tech0verlord)** — A Windows install stores the location of each archive, library file and print log entry with backslashes, which Linux and macOS read as part of the file name. After restoring a Windows backup on Docker or another Linux install, every archive showed no thumbnail and refused its 3D preview and reprint with "Archive data unavailable". The restore now turns those backslashes into slashes. It runs only during a restore, does nothing on Windows, and leaves a path alone when a file with that exact name exists. An install already restored from a Windows backup is fixed by restoring the backup again after updating.
 - **A backup from Windows restored on Linux or macOS finds its files (#3501, reported by @Tech0verlord)** — A Windows install stores the location of each archive, library file and print log entry with backslashes, which Linux and macOS read as part of the file name. After restoring a Windows backup on Docker or another Linux install, every archive showed no thumbnail and refused its 3D preview and reprint with "Archive data unavailable". The restore now turns those backslashes into slashes. It runs only during a restore, does nothing on Windows, and leaves a path alone when a file with that exact name exists. An install already restored from a Windows backup is fixed by restoring the backup again after updating.
 - **Tags written by SpoolBuddy can be read by phones and other readers (#3500, reported by @pleasantone; likely #3076)** — SpoolBuddy's **Write Tag** wrote a copy of the tag's Capability Container in front of the OpenTag3D data. On an NTAG tag that copy landed in the data area, where a phone, NFC Tools or another OpenTag3D reader took it for unknown data, skipped over the spool record and reported an empty tag, although the write had succeeded. Bambuddy itself was unaffected, since it links spools by the tag's ID. The data now starts with the spool record, and the tag's own Capability Container is left as it is. Tags written before need writing again for other readers to see them.
 - **Tags written by SpoolBuddy can be read by phones and other readers (#3500, reported by @pleasantone; likely #3076)** — SpoolBuddy's **Write Tag** wrote a copy of the tag's Capability Container in front of the OpenTag3D data. On an NTAG tag that copy landed in the data area, where a phone, NFC Tools or another OpenTag3D reader took it for unknown data, skipped over the spool record and reported an empty tag, although the write had succeeded. Bambuddy itself was unaffected, since it links spools by the tag's ID. The data now starts with the spool record, and the tag's own Capability Container is left as it is. Tags written before need writing again for other readers to see them.
 - **An A2L with an AMS and an AMS Lite shows both (#3499, reported by @auberonkai)** — With an AMS 2 Pro and an AMS Lite attached to an A2L at the same time, the printer card showed only the external spool, until one of them was unplugged and the printer reconnected. Bambuddy failed to sort the two units, because the printer names a regular AMS with text and the AMS Lite with a number, and from then on it could not read the AMS data of any later update. Both units now show, in order, and an AMS update that can't be read no longer blocks the ones after it. The connection diagnostic also no longer fails an A2L on "Store sent files on external storage": neither the printer nor the slicers offer that option for it, so the check is skipped, as on the P1 series, and the explanation no longer names the P1 series only.
 - **An A2L with an AMS and an AMS Lite shows both (#3499, reported by @auberonkai)** — With an AMS 2 Pro and an AMS Lite attached to an A2L at the same time, the printer card showed only the external spool, until one of them was unplugged and the printer reconnected. Bambuddy failed to sort the two units, because the printer names a regular AMS with text and the AMS Lite with a number, and from then on it could not read the AMS data of any later update. Both units now show, in order, and an AMS update that can't be read no longer blocks the ones after it. The connection diagnostic also no longer fails an A2L on "Store sent files on external storage": neither the printer nor the slicers offer that option for it, so the check is skipped, as on the P1 series, and the explanation no longer names the P1 series only.

+ 1 - 0
backend/app/core/database.py

@@ -632,6 +632,7 @@ async def init_db():
         finance,
         finance,
         github_backup,
         github_backup,
         group,
         group,
+        hms_notified_fault,
         kprofile_note,
         kprofile_note,
         library,
         library,
         local_preset,
         local_preset,

+ 66 - 0
backend/tests/integration/test_startup_creates_every_table.py

@@ -0,0 +1,66 @@
+"""Startup creates the table of every model (#3502).
+
+init_db() imports the model modules so create_all knows their tables. A module
+missing from that list (and from models/__init__) gets no table on a real
+start, while the test suite, which imports every model itself, never notices:
+hms_notified_faults (#3486) was never created that way. So this starts a clean
+Python process, runs init_db() against an empty SQLite database and checks the
+tables against the ones the model files declare.
+"""
+
+import os
+import re
+import sqlite3
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+REPO = Path(__file__).resolve().parents[3]
+MODELS = REPO / "backend" / "app" / "models"
+
+
+def _declared_tables() -> set[str]:
+    tables = set()
+    for path in MODELS.glob("*.py"):
+        source = path.read_text()
+        tables.update(re.findall(r'__tablename__\s*=\s*"(\w+)"', source))
+        tables.update(re.findall(r'Table\(\s*"(\w+)"', source))
+    return tables
+
+
+@pytest.mark.integration
+@pytest.mark.sqlite_only  # inspects the SQLite file the started process creates
+def test_a_fresh_start_creates_every_model_table(tmp_path):
+    db_path = tmp_path / "bambuddy.db"
+    env = {
+        **os.environ,
+        "DATA_DIR": str(tmp_path),
+        "LOG_DIR": str(tmp_path / "logs"),
+        "DATABASE_URL": f"sqlite+aiosqlite:///{db_path}",
+    }
+    result = subprocess.run(
+        [
+            sys.executable,
+            "-c",
+            "import asyncio\n"
+            "from backend.app.core.database import init_db, engine\n"
+            "async def main():\n"
+            "    await init_db()\n"
+            "    await engine.dispose()\n"
+            "asyncio.run(main())\n",
+        ],
+        cwd=REPO,
+        env=env,
+        capture_output=True,
+        text=True,
+        timeout=300,
+    )
+    assert result.returncode == 0, result.stderr[-3000:]
+
+    with sqlite3.connect(db_path) as db:
+        created = {row[0] for row in db.execute("SELECT name FROM sqlite_master WHERE type = 'table'")}
+    declared = _declared_tables()
+    assert "hms_notified_faults" in declared
+    assert declared - created == set()