Part of the "SQLite migration is half-done" parent issue.
Background
Sandbox builds are meant to live in the SQLite builds table (BuildRecord in backend/app/db/models.py:110). The legacy_import.py script already knows how to write that table.
Problem / Goal
The runtime build code still uses flat files:
backend/app/agents/build.py:120 save_build writes data/builds/<id>.json.
build.py:129 get_build, build.py:137 delete_build, build.py:151 list_builds all read/scan that directory.
So the builds table is empty during normal use, and the orphan-nulling logic in runs.py:99 / customers.py:92 never has anything to act on.
Goal: move build persistence to the BuildRecord table, keeping the record JSON shape identical.
Where to look
backend/app/agents/build.py:87-188 — _builds_dir, save_build, get_build, delete_build, list_builds.
backend/app/db/models.py:110-124 — BuildRecord columns: id (12-char hex), section_design_id, model, record (JSON), saved_at, gns3_project_id.
backend/app/db/legacy_import.py:144-191 — import_builds maps a flat-file build onto BuildRecord; mirror that field-for-field.
backend/app/api/build.py — callers: /list, /{id}, delete, deploy-gns3 (which re-saves the record with a gns3 block via save_build, see api/build.py:529-543).
Suggested approach
- Rewrite
save_build to upsert a BuildRecord row: keep the 12-char hex id, put the full dict in record, and denormalize model, saved_at, gns3_project_id, and section_design_id into their columns.
- Rewrite
get_build/delete_build/list_builds to query the table. list_builds currently derives node/link counts and kinds from record["topology"] — preserve that summary shape so the Sandbox page is unchanged.
- Make sure the GNS3 deploy path (
api/build.py:_do_deploy) still round-trips: it calls get_build → mutates rec["gns3"] → save_build(rec). That must now update the row's record and gns3_project_id.
Acceptance criteria
- Creating a build inserts a row into
builds; no data/builds/*.json is written at runtime.
GET /api/build/list and GET /api/build/{id} return the same JSON shape as before, sourced from SQLite.
- Deleting a build removes its row; deploy-to-GNS3 updates the row's
gns3 block and gns3_project_id.
- After deleting a run/customer, builds built from its sections have
section_design_id nulled (the existing orphan logic now works).
Testing
Local backend. Build a sandbox (see parent Testing), then:
sqlite3 data/netbud.sqlite "select id, model, gns3_project_id from builds;" shows the row.
GET /api/build/list returns it with correct node_count/link_count/kinds.
- Deploy to GNS3 (if configured) and confirm
gns3_project_id populates.
Add a unit test covering save → get → list → delete against an in-memory SQLite engine.
Out of scope
- The build journal table (separate sub-issue).
- Design lookup for building (separate sub-issue 1a).
Part of the "SQLite migration is half-done" parent issue.
Background
Sandbox builds are meant to live in the SQLite
buildstable (BuildRecordinbackend/app/db/models.py:110). Thelegacy_import.pyscript already knows how to write that table.Problem / Goal
The runtime build code still uses flat files:
backend/app/agents/build.py:120save_buildwritesdata/builds/<id>.json.build.py:129get_build,build.py:137delete_build,build.py:151list_buildsall read/scan that directory.So the
buildstable is empty during normal use, and the orphan-nulling logic inruns.py:99/customers.py:92never has anything to act on.Goal: move build persistence to the
BuildRecordtable, keeping therecordJSON shape identical.Where to look
backend/app/agents/build.py:87-188—_builds_dir,save_build,get_build,delete_build,list_builds.backend/app/db/models.py:110-124—BuildRecordcolumns:id(12-char hex),section_design_id,model,record(JSON),saved_at,gns3_project_id.backend/app/db/legacy_import.py:144-191—import_buildsmaps a flat-file build ontoBuildRecord; mirror that field-for-field.backend/app/api/build.py— callers:/list,/{id}, delete, deploy-gns3 (which re-saves the record with agns3block viasave_build, seeapi/build.py:529-543).Suggested approach
save_buildto upsert aBuildRecordrow: keep the 12-char hexid, put the full dict inrecord, and denormalizemodel,saved_at,gns3_project_id, andsection_design_idinto their columns.get_build/delete_build/list_buildsto query the table.list_buildscurrently derives node/link counts andkindsfromrecord["topology"]— preserve that summary shape so the Sandbox page is unchanged.api/build.py:_do_deploy) still round-trips: it callsget_build→ mutatesrec["gns3"]→save_build(rec). That must now update the row'srecordandgns3_project_id.Acceptance criteria
builds; nodata/builds/*.jsonis written at runtime.GET /api/build/listandGET /api/build/{id}return the same JSON shape as before, sourced from SQLite.gns3block andgns3_project_id.section_design_idnulled (the existing orphan logic now works).Testing
Local backend. Build a sandbox (see parent Testing), then:
sqlite3 data/netbud.sqlite "select id, model, gns3_project_id from builds;"shows the row.GET /api/build/listreturns it with correctnode_count/link_count/kinds.gns3_project_idpopulates.Add a unit test covering save → get → list → delete against an in-memory SQLite engine.
Out of scope