3.3.1 kept `nodes` writable when the backfill failed, but the backfill still
failed — so a canvas upgraded from 3.2.0 shows every device stripped of its ip,
services, hostname, notes and hardware. The facts are intact in the legacy
columns; nothing was ever reading them across.
The backfill reads those columns with raw SQL, so SQLite hands `last_seen` and
`last_scan` back as text. Writing text into a DateTime column raises
`TypeError: SQLite DateTime type only accepts Python datetime and date objects`,
which is neither IntegrityError nor OperationalError — so it went straight past
the per-node savepoint and aborted the whole run, exactly as before.
- Parse the legacy timestamps back into datetimes, the way `_decode_json`
already handles the JSON columns. An unparseable stamp becomes None rather
than an error: the status checker refreshes both within a minute.
- Catch every exception per node, not two chosen classes. Picking the exception
types was the defect; a node that fails for any reason must cost only itself.
The same run would also have destroyed data. A canvas saved while an earlier
migration was stuck minted a blank inventory row from a UI that had no facts to
show and linked the node to it. The backfill only looked at unlinked nodes, so
it skipped that one, counted the canvas migrated, and dropped the columns
holding its only copy of the ip and services.
- Read linked nodes too, and fill only what their row is missing. A row that
already holds a fact is never overwritten, so an edit made after the migration
survives, and the pass stays a no-op once everything has moved across.
Verified end to end against a rebuilt 3.2.0 database stuck the way the reports
describe: one boot restores ip, services, hostname, mac, notes, check method,
hardware, properties, status and last_seen, then drops the legacy columns.
Refs #347, #348, #351
ha-relevant: no