diff --git a/README.md b/README.md
index 175c414..a21f450 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ encrypted Frappe archives cannot be validated before upload.
## Usage
-Open **Restic Backups → Backup Control** (`/app/restic-backup-control` on v15,
+Open **Restic Backup → Backup Control** (`/app/restic-backup-control` on v15,
`/desk/restic-backup-control` on v16). From there you can run a backup, check off-site
snapshots and change settings. Only System Managers can access the page, the settings,
the history and the backup APIs; there is no restore endpoint on the web.
diff --git a/frappe_restic/desktop_icon/restic_backup.json b/frappe_restic/desktop_icon/restic_backup.json
new file mode 100644
index 0000000..df19e53
--- /dev/null
+++ b/frappe_restic/desktop_icon/restic_backup.json
@@ -0,0 +1,14 @@
+{
+ "app": "frappe_restic",
+ "doctype": "Desktop Icon",
+ "hidden": 0,
+ "icon": "database",
+ "icon_type": "Link",
+ "label": "Restic Backup",
+ "link_to": "Restic Backup",
+ "link_type": "Workspace Sidebar",
+ "modified": "2026-09-30 12:00:00.000000",
+ "name": "Restic Backup",
+ "restrict_removal": 0,
+ "standard": 1
+}
diff --git a/frappe_restic/patches.txt b/frappe_restic/patches.txt
index 8a2400b..229c7e0 100644
--- a/frappe_restic/patches.txt
+++ b/frappe_restic/patches.txt
@@ -1,3 +1,4 @@
[pre_model_sync]
[post_model_sync]
+frappe_restic.patches.name_desk_entries_after_module
diff --git a/frappe_restic/patches/__init__.py b/frappe_restic/patches/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/frappe_restic/patches/name_desk_entries_after_module.py b/frappe_restic/patches/name_desk_entries_after_module.py
new file mode 100644
index 0000000..f5e38f7
--- /dev/null
+++ b/frappe_restic/patches/name_desk_entries_after_module.py
@@ -0,0 +1,15 @@
+import frappe
+
+LEGACY = "Restic Backups"
+
+
+def execute():
+ """Remove the desk entries named after the old "Restic Backups" workspace.
+
+ The workspace, sidebar and desktop icon are now named after the module, so Frappe v16 finds
+ one sidebar for every backup screen instead of generating a second one for the module.
+ """
+ # Workspace Sidebar and Desktop Icon exist from Frappe v16; v15 only has the workspace.
+ for doctype in ("Desktop Icon", "Workspace Sidebar", "Workspace"):
+ if frappe.db.table_exists(doctype) and frappe.db.exists(doctype, LEGACY):
+ frappe.delete_doc(doctype, LEGACY, force=True, ignore_permissions=True)
diff --git a/frappe_restic/public/icons/desktop_icons/solid/restic_backup.svg b/frappe_restic/public/icons/desktop_icons/solid/restic_backup.svg
new file mode 100644
index 0000000..5d7325d
--- /dev/null
+++ b/frappe_restic/public/icons/desktop_icons/solid/restic_backup.svg
@@ -0,0 +1,10 @@
+
diff --git a/frappe_restic/public/icons/desktop_icons/subtle/restic_backup.svg b/frappe_restic/public/icons/desktop_icons/subtle/restic_backup.svg
new file mode 100644
index 0000000..64fbad9
--- /dev/null
+++ b/frappe_restic/public/icons/desktop_icons/subtle/restic_backup.svg
@@ -0,0 +1,10 @@
+
diff --git a/frappe_restic/public/scss/restic_backups.bundle.scss b/frappe_restic/public/scss/restic_backups.bundle.scss
index 904dabc..b75c24f 100644
--- a/frappe_restic/public/scss/restic_backups.bundle.scss
+++ b/frappe_restic/public/scss/restic_backups.bundle.scss
@@ -1,3 +1,12 @@
+// Frappe themes its own .frappe-card, but Bootstrap's .card stays white in dark mode.
+.restic-backup-control,
+.restic-backup-settings-dialog {
+ .card {
+ border-color: var(--border-color);
+ background-color: var(--card-bg);
+ }
+}
+
.restic-backup-settings-dialog {
.modal-dialog {
max-width: 880px;
@@ -35,15 +44,6 @@
.restic-backup-day {
min-width: 2.9rem;
- border-color: var(--gray-300);
- background: var(--control-bg);
- color: var(--text-color);
-
- &.is-selected {
- border-color: var(--primary-color);
- background: var(--primary-color);
- color: var(--fg-color);
- }
}
.restic-backup-schedule-time {
diff --git a/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js b/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js
index b2fee8e..e56bc76 100644
--- a/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js
+++ b/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js
@@ -65,11 +65,6 @@
listview.page.add_inner_button(__("Backup Control"), () =>
frappe.set_route("restic-backup-control")
);
- frappe.breadcrumbs.add({
- type: "Custom",
- route: frappe.router.make_url(["restic-backup-control"]),
- label: __("Backup Control"),
- });
},
formatters: {
// Frappe inserts subject text with textContent, not HTML.
diff --git a/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js b/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js
index 0250f80..c40f3ea 100644
--- a/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js
+++ b/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js
@@ -1,11 +1,6 @@
frappe.ui.form.on("Restic Backup Settings", {
refresh(frm) {
frm.page.set_title(__("Backup Settings"));
- frappe.breadcrumbs.add({
- type: "Custom",
- route: frappe.router.make_url(["restic-backup-control"]),
- label: __("Backup Control"),
- });
frm.add_custom_button(__("Back to Backup Control"), () => {
frappe.set_route("restic-backup-control");
});
diff --git a/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js b/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js
index bf9d7f1..b94035e 100644
--- a/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js
+++ b/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js
@@ -1,5 +1,4 @@
frappe.pages["restic-backup-control"].on_page_load = function (wrapper) {
- frappe.breadcrumbs.add("Setup");
const page = frappe.ui.make_app_page({
parent: wrapper,
title: __("Backup Control"),
@@ -11,6 +10,12 @@ frappe.pages["restic-backup-control"].on_page_load = function (wrapper) {
page.add_inner_button(__("Settings"), () => backupControl.openSettings());
};
+// Frappe v16 picks the sidebar for a page from the module of the last doctype opened, so opening
+// this page from another app's screen keeps that app's sidebar. Pick again from this page's route.
+frappe.pages["restic-backup-control"].on_page_show = function () {
+ frappe.app.sidebar?.set_workspace_sidebar?.();
+};
+
class ResticBackupControl {
constructor(page, wrapper) {
this.page = page;
@@ -234,7 +239,7 @@ class ResticBackupControl {
.map((day) => {
const selected = entry.days.includes(day);
return ``;
diff --git a/frappe_restic/restic_backup/workspace/restic_backups/restic_backups.json b/frappe_restic/restic_backup/workspace/restic_backup/restic_backup.json
similarity index 75%
rename from frappe_restic/restic_backup/workspace/restic_backups/restic_backups.json
rename to frappe_restic/restic_backup/workspace/restic_backup/restic_backup.json
index d0417db..9a9461c 100644
--- a/frappe_restic/restic_backup/workspace/restic_backups/restic_backups.json
+++ b/frappe_restic/restic_backup/workspace/restic_backup/restic_backup.json
@@ -1,8 +1,9 @@
{
"doctype": "Workspace",
- "name": "Restic Backups",
- "label": "Restic Backups",
- "title": "Restic Backups",
+ "name": "Restic Backup",
+ "label": "Restic Backup",
+ "title": "Restic Backup",
+ "app": "frappe_restic",
"module": "Restic Backup",
"public": 1,
"is_hidden": 0,
@@ -22,5 +23,5 @@
}
],
"content": "[{\"id\": \"backup-shortcut\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Backup Control\", \"col\": 3}}]",
- "modified": "2026-09-23 13:48:25.000000"
+ "modified": "2026-09-30 12:00:00.000000"
}
diff --git a/frappe_restic/test_desk.py b/frappe_restic/test_desk.py
new file mode 100644
index 0000000..6dcc1de
--- /dev/null
+++ b/frappe_restic/test_desk.py
@@ -0,0 +1,64 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from frappe_restic.patches import name_desk_entries_after_module
+
+MODULE = "Restic Backup"
+LEGACY = "Restic Backups"
+
+
+def has_sidebars():
+ # Workspace Sidebar and Desktop Icon exist from Frappe v16.
+ return frappe.db.table_exists("Workspace Sidebar")
+
+
+class TestDeskEntries(FrappeTestCase):
+ def test_workspace_is_named_after_the_module(self):
+ self.assertEqual(frappe.get_all("Workspace", filters={"module": MODULE}, pluck="name"), [MODULE])
+
+ def test_one_sidebar_and_icon_for_the_module(self):
+ if not has_sidebars():
+ self.skipTest("Frappe v15 has no sidebars")
+ from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import (
+ auto_generate_sidebar_from_module,
+ )
+
+ sidebars = frappe.get_all(
+ "Workspace Sidebar", filters={"module": MODULE, "for_user": ["is", "not set"]}, pluck="name"
+ )
+ self.assertEqual(sidebars, [MODULE])
+ # Frappe generates a second sidebar for a module that has none named after it.
+ self.assertNotIn(MODULE, [sidebar.title for sidebar in auto_generate_sidebar_from_module()])
+ icons = frappe.get_all("Desktop Icon", filters={"link_to": ["in", [MODULE, LEGACY]]}, pluck="name")
+ self.assertEqual(icons, [MODULE])
+
+ def test_patch_removes_the_entries_named_after_the_old_workspace(self):
+ frappe.get_doc(
+ {
+ "doctype": "Workspace",
+ "label": LEGACY,
+ "title": LEGACY,
+ "module": MODULE,
+ "public": 1,
+ "content": "[]",
+ }
+ ).insert()
+ doctypes = ["Workspace"]
+ if has_sidebars():
+ frappe.get_doc({"doctype": "Workspace Sidebar", "title": LEGACY, "module": MODULE}).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Desktop Icon",
+ "label": LEGACY,
+ "icon_type": "Link",
+ "link_type": "Workspace Sidebar",
+ "link_to": LEGACY,
+ }
+ ).insert()
+ doctypes += ["Workspace Sidebar", "Desktop Icon"]
+
+ name_desk_entries_after_module.execute()
+
+ for doctype in doctypes:
+ self.assertFalse(frappe.db.exists(doctype, LEGACY), doctype)
+ self.assertTrue(frappe.db.exists("Workspace", MODULE))
diff --git a/frappe_restic/workspace_sidebar/restic_backup.json b/frappe_restic/workspace_sidebar/restic_backup.json
new file mode 100644
index 0000000..843c2df
--- /dev/null
+++ b/frappe_restic/workspace_sidebar/restic_backup.json
@@ -0,0 +1,33 @@
+{
+ "app": "frappe_restic",
+ "doctype": "Workspace Sidebar",
+ "header_icon": "database",
+ "items": [
+ {
+ "icon": "database-backup",
+ "label": "Backup Control",
+ "link_to": "restic-backup-control",
+ "link_type": "Page",
+ "type": "Link"
+ },
+ {
+ "icon": "history",
+ "label": "Backup Runs",
+ "link_to": "Restic Backup Run",
+ "link_type": "DocType",
+ "type": "Link"
+ },
+ {
+ "icon": "settings",
+ "label": "Backup Settings",
+ "link_to": "Restic Backup Settings",
+ "link_type": "DocType",
+ "type": "Link"
+ }
+ ],
+ "modified": "2026-09-30 12:00:00.000000",
+ "module": "Restic Backup",
+ "name": "Restic Backup",
+ "standard": 1,
+ "title": "Restic Backup"
+}