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. -->
{{ rev.sharepath }}
+
+