From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 5AA7A1FF0DF for ; Fri, 28 Aug 2026 15:32:22 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 1A5DB216C5; Fri, 28 Aug 2026 15:31:14 +0200 (CEST) From: Shannon Sterz To: pve-devel@lists.proxmox.com Subject: [PATCH manager 12/21] ui: node: add panel to manage backups of a host Date: Fri, 28 Aug 2026 15:30:21 +0200 Message-ID: <20260828133030.351140-13-s.sterz@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260828133030.351140-1-s.sterz@proxmox.com> References: <20260828133030.351140-1-s.sterz@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1787923824747 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.867 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: AGX4RPGR6YQS3LSKPVSCI4XGGMI2HKIQ X-Message-ID-Hash: AGX4RPGR6YQS3LSKPVSCI4XGGMI2HKIQ X-MailFrom: s.sterz@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: allows creating, editing the notes of, changing the protection status of, file restoring and removing backups of a single pve node. Signed-off-by: Shannon Sterz --- www/manager6/Makefile | 1 + www/manager6/node/Config.js | 7 + www/manager6/node/HostBackup.js | 292 ++++++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 www/manager6/node/HostBackup.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index d2ea786be..37f29747d 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -244,6 +244,7 @@ JSSRC= \ node/CmdMenu.js \ node/Config.js \ node/Directory.js \ + node/HostBackup.js \ node/LVM.js \ node/LVMThin.js \ node/StatusView.js \ diff --git a/www/manager6/node/Config.js b/www/manager6/node/Config.js index 217ee284c..385b9c6fd 100644 --- a/www/manager6/node/Config.js +++ b/www/manager6/node/Config.js @@ -169,6 +169,13 @@ Ext.define('PVE.node.Config', { if (caps.nodes['Sys.Audit']) { me.items.push( + { + xtype: 'pveHostBackup', + title: gettext('Host Backup'), + iconCls: 'fa fa-floppy-o', + itemId: 'nodebackup', + nodename: nodename, + }, { xtype: 'proxmoxNodeServiceView', title: gettext('System'), diff --git a/www/manager6/node/HostBackup.js b/www/manager6/node/HostBackup.js new file mode 100644 index 000000000..dcecd873d --- /dev/null +++ b/www/manager6/node/HostBackup.js @@ -0,0 +1,292 @@ +Ext.define('PVE.node.AddBackupPanel', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Host Backup'), + + method: 'POST', + showTaskViewer: true, + nodename: undefined, + + config: { + entry: null, + }, + + items: [ + { + fieldLabel: gettext('Storage'), + name: 'storage', + xtype: 'pveStorageSelector', + reference: 'backupStorageSelector', + storageContent: 'backup', + allowBlank: false, + cbind: { + nodename: '{nodename}', + }, + }, + ], + + initComponent: function () { + let me = this; + me.url = `/nodes/${me.nodename}/host-backup`; + me.callParent(); + + me.lookup('backupStorageSelector') + .getStore() + .addFilter({ filterFn: (item) => item.data.type === 'pbs' }); + + me.setValues(me.getEntry()); + }, +}); + +Ext.define('PVE.node.HostBackup', { + extend: 'Ext.grid.Panel', + alias: ['widget.pveHostBackup'], + mixins: ['Proxmox.Mixin.CBind'], + + // onlineHelp: 'chapter_vzdump', + + nodename: undefined, + stateful: true, + stateId: 'grid-node-backup', + + store: { + model: 'pve-storage-content', + proxy: { + type: 'proxmox', + }, + sorters: [ + { + property: 'vdate', + direction: 'DESC', + }, + ], + }, + + tbar: [ + { + text: gettext('Backup Now'), + xtype: 'button', + handler: 'createBackupHandler', + }, + '-', + { + text: gettext('File Restore'), + xtype: 'proxmoxButton', + handler: 'fileRestoreHandler', + disabled: true, + }, + { + text: gettext('Edit Notes'), + xtype: 'proxmoxButton', + handler: 'editNotesHandler', + disabled: true, + }, + { + text: gettext('Change Protection'), + xtype: 'proxmoxButton', + handler: 'changeProtectionHandler', + disabled: true, + }, + '-', + { + text: gettext('Remove'), + xtype: 'proxmoxStdRemoveButton', + dangerous: true, + customConfirmationMessage: gettext( + 'Are you sure you want to remove {0}. This will permanently erase it.', + ), + getRecordName: (rec) => rec.data.volid, + callback: function () { + this.up('grid').getStore().load(); + }, + getUrl: function (rec) { + let grid = this.up('grid'); + let nodename = grid.nodename; + let storage = grid.lookup('backupStorageSelector').getValue(); + let volid = rec.data.volid; + return `/nodes/${nodename}/storage/${storage}/content/${volid}`; + }, + }, + '->', + { + fieldLabel: gettext('Storage'), + xtype: 'pveStorageSelector', + reference: 'backupStorageSelector', + labelAlign: 'right', + storageContent: 'backup', + allowBlank: false, + listeners: { + change: 'changeStorageHandler', + }, + cbind: { + nodename: '{nodename}', + }, + }, + ], + + listeners: { + itemdblclick: 'dblclickHandler', + }, + + columns: [ + { + header: gettext('Name'), + flex: 2, + sortable: true, + renderer: PVE.Utils.render_storage_content, + dataIndex: 'volid', + }, + { + header: gettext('Notes'), + dataIndex: 'notes', + flex: 1, + renderer: Ext.htmlEncode, + }, + { + header: ``, + tooltip: gettext('Protected'), + width: 30, + renderer: (v) => + v ? `` : '', + sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0), + dataIndex: 'protected', + }, + { + header: gettext('Date'), + width: 150, + dataIndex: 'vdate', + }, + { + header: gettext('Format'), + width: 100, + dataIndex: 'format', + }, + { + header: gettext('Size'), + width: 100, + renderer: Proxmox.Utils.format_size, + dataIndex: 'size', + }, + { + header: gettext('Encrypted'), + dataIndex: 'encrypted', + renderer: PVE.Utils.render_backup_encryption, + }, + { + // TRANSLATORS: The state of the verification task + header: gettext('Verify State'), + dataIndex: 'verification', + renderer: PVE.Utils.render_backup_verification, + }, + ], + + controller: { + dblclickHandler: (element, rec) => { + let storage = element.up('grid').lookup('backupStorageSelector').getValue(); + + Ext.create('Proxmox.window.FileBrowser', { + title: gettext('File Restore') + ' - ' + rec.data.text, + autoShow: true, + listURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/list`, + downloadURL: `/api2/json/nodes/localhost/storage/${storage}/file-restore/download`, + extraParams: { + volume: rec.data.volid, + }, + }); + }, + createBackupHandler: (button, _event) => { + let grid = button.up('grid'); + + Ext.create('PVE.node.AddBackupPanel', { + autoShow: true, + isCreate: true, + nodename: grid.nodename, + listeners: { + close: () => grid.getStore().load(), + }, + }); + }, + fileRestoreHandler: function (button, _event, rec) { + if (!rec) { + console.warn('No host backup selected!'); + } else { + this.dblclickHandler(button, rec); + } + }, + editNotesHandler: (button, _event, rec) => { + if (!rec) { + console.warn('No host backup selected!'); + return; + } + + let grid = button.up('grid'); + let nodename = grid.nodename; + let storage = grid.lookup('backupStorageSelector').getValue(); + let volid = rec.data.volid; + + Ext.create('Proxmox.window.Edit', { + title: gettext('Notes'), + autoShow: true, + width: 600, + height: 400, + layout: 'fit', + autoLoad: true, + resizable: true, + url: `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`, + items: [ + { + xtype: 'textarea', + layout: 'fit', + name: 'notes', + height: '100%', + }, + ], + listeners: { + close: () => grid.getStore().load(), + }, + }); + }, + changeProtectionHandler: (button, _event, rec) => { + if (!rec) { + console.warn('No host backup selected!'); + return; + } + + let grid = button.up('grid'); + let nodename = grid.nodename; + let storage = grid.lookup('backupStorageSelector').getValue(); + let volid = rec.data.volid; + + Proxmox.Utils.API2Request({ + url: `/api2/extjs/nodes/${nodename}/storage/${storage}/content/${volid}`, + method: 'PUT', + waitMsgTarget: grid, + params: { + protected: rec.data.protected ? 0 : 1, + }, + failure: (response) => Ext.Msg.alert('Error', response.htmlStatus), + success: () => grid.getStore().load(), + }); + }, + changeStorageHandler: (selector, value) => { + let store = selector.up('grid').getStore(); + let url = `/api2/json/nodes/${selector.nodename}/host-backup?storage=${value}`; + + store.getProxy().setUrl(url); + store.load(); + }, + }, + + initComponent: function () { + let me = this; + me.callParent(); + Proxmox.Utils.monStoreErrors(me.view, me.store, true); + + // filter out non-pbs storages; the store only exist after + // `initComponent` of the selector + me.lookup('backupStorageSelector') + .getStore() + .addFilter({ filterFn: (item) => item.data.type === 'pbs' }); + }, +}); -- 2.47.3