diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b2050e7..1d46109c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ ADR-007 and ADR-002. `rename` fixes a stripped name with no twin, which neither of the others can reach. All three report and change nothing unless `--apply` is passed, and `dedupe` is idempotent. References are discovered from the live schema, not - listed in code: there are 15 foreign keys into these three tables, and + listed in code: there are 14 foreign keys into these three tables, and `machines.controllervendorid` is not one anybody would enumerate by hand. - **Secure Send: hand somebody a password without leaving it in a chat @@ -90,6 +90,16 @@ ADR-007 and ADR-002. ### Fixed +- **The shipped catalog offered two ways to spell the same vendor, and one row + that should never have left a test.** `HP Inc.` and `Zebra Technologies` were + empty rows duplicating `HP` and `Zebra`, which carry every model between + them; neither exists in this site's database, so they would have arrived as + duplicates at a new site and nowhere else. `UnscopedLiveVendor` was a test + artifact: `export_catalog.py` reads the live development database, so a row + left behind by a lab run was published as reference data. The canonical name + is whichever spelling holds the entries, which is why it is `Dell Inc.` but + `HP` and `Zebra`. Vendors 51 to 48. + - **The shipped seed catalog contained duplicate vendors, models and operating systems.** Four space-stripped twins, each with a correctly spaced original already present, so a new site inherited the same company listed twice. diff --git a/plugins/backups/api/routes.py b/plugins/backups/api/routes.py index b707fa81..86ac20ba 100644 --- a/plugins/backups/api/routes.py +++ b/plugins/backups/api/routes.py @@ -156,9 +156,34 @@ def asset_info(assetid): if revision is None: return success_response({'fields': [], 'sectioncount': 0}) + try: + projection = revision.payload + except ValueError: + # payload deliberately raises on unreadable payloadjson so a corrupt row + # is named rather than mistaken for an opaque one. That is right for a + # download; for an at-a-glance card it would take the whole asset page + # down over one bad row, so the card degrades to empty instead. + current_app.logger.warning( + 'backuprevision %s has unreadable payloadjson; info card empty', + revision.backuprevisionid) + return success_response({'fields': [], 'sectioncount': 0}) + data = kind.buildinfo( - revision.payload, assetid, - partmarkertypes=current_app.config.get('BACKUPS_PARTMARKER_TYPES')) + projection, assetid, + partmarkertypes=current_app.config.get('BACKUPS_PARTMARKER_TYPES'), + # A kind that diffs against its own previous revision needs to know + # which revision it is looking at. Passed generically rather than + # queried inside the kind, so the endpoint stays the single place that + # decides "latest". + revisionid=revision.backuprevisionid, + # The card names the PC a snapshot came from, and chains its diff on the + # same value. Passed from the column rather than read out of the stored + # manifest, which carries its own copy that can disagree. + sourcehostname=revision.sourcehostname, + # Where the snapshot actually is. Read off the revision rather than + # composed from the configured share root, so a snapshot written before + # the layout changed still points at where it was really put. + sharepath=revision.sharepath) data['backuprevisionid'] = revision.backuprevisionid data['collectedat'] = _utciso(revision.collectedat) return success_response(data) diff --git a/plugins/backups/frontend/routes.js b/plugins/backups/frontend/routes.js index db2870be..ee75326d 100644 --- a/plugins/backups/frontend/routes.js +++ b/plugins/backups/frontend/routes.js @@ -12,6 +12,17 @@ * JWT + a backups.* permission; an anonymous visit would only render errors. */ export default [ + { + // The plugin's five settings had no page at all until this one: the core + // settings catalog is a hardcoded list that a plugin cannot extend, so + // backups_shareroot and friends were reachable only through the API or a + // shell on the server. Paired with get_settings_cards in plugin.py, which + // puts the card in the rail - both vanish when the plugin is disabled. + path: 'settings/backups', + name: 'backups-settings', + component: () => import('./views/BackupsSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true, plugin: 'backups' } + }, { path: 'backups/asset/:assetid', name: 'backups-asset-history', diff --git a/plugins/backups/frontend/views/BackupHistory.vue b/plugins/backups/frontend/views/BackupHistory.vue index 6ab4fec3..803c8571 100644 --- a/plugins/backups/frontend/views/BackupHistory.vue +++ b/plugins/backups/frontend/views/BackupHistory.vue @@ -66,6 +66,16 @@ UNC path so the tech can open it directly. -->