From 67753fb02517008d7702ec769644370e150045c8 Mon Sep 17 00:00:00 2001 From: rajch_ales Date: Wed, 5 Aug 2026 23:20:49 +0200 Subject: [PATCH] Persist local database write permissions --- public/app.js | 46 ++++++++++++++++++++++++++++++----- public/app.ts | 46 +++++++++++++++++++++++++++++++---- public/index.html | 2 +- tests/ui/database.e2e.spec.ts | 17 +++++++++++++ 4 files changed, 99 insertions(+), 12 deletions(-) diff --git a/public/app.js b/public/app.js index d1cd04e..c09e7e2 100644 --- a/public/app.js +++ b/public/app.js @@ -63,6 +63,12 @@ const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle"); let sidebarUserClosed = false; let mappingLoadToken = 0; const mappingCacheKey = "catalog-maker:mapping-cache"; +const legacyPermissionStorageKey = "catalog-maker:db-permissions"; +const permissionStorageKeys = { + local: "catalog-maker:db-permissions:local-9bplus", + live: "catalog-maker:db-permissions:live-9bplus", + scrape: "catalog-maker:db-permissions:local-scrape", +}; const nativeFetch = window.fetch.bind(window); window.fetch = (input, init = {}) => { const url = typeof input === "string" ? input : input.url; @@ -70,7 +76,7 @@ window.fetch = (input, init = {}) => { return nativeFetch(input, init); const headers = new Headers(init.headers || (typeof input !== "string" ? input.headers : undefined)); const selectedMode = localStorage.getItem("catalog-maker:database-mode") || "local"; - const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}"); + const permissions = readDatabasePermissions(selectedMode); const localWritesEnabled = selectedMode === "local" && [ permissions.allowInsertToggle, permissions.allowUpdateToggle, @@ -1048,9 +1054,7 @@ function loadDatabaseMode() { const input = document.querySelector(`input[name="databaseMode"][value="${mode}"]`); if (input) input.checked = true; - const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}"); - for (const input of permissionInputs) - input.checked = permissions[input.id] === true; + applyPermissionInputs(readDatabasePermissions(mode)); const transactionMode = localStorage.getItem("catalog-maker:transaction-mode") || "transaction"; const transactionInput = document.querySelector(`input[name="transactionMode"][value="${transactionMode}"]`); if (transactionInput) @@ -1069,6 +1073,7 @@ function loadDatabaseMode() { function saveDatabaseMode(event) { const mode = ["local", "live", "scrape"].includes(event.target.value) ? event.target.value : "local"; localStorage.setItem("catalog-maker:database-mode", mode); + applyPermissionInputs(readDatabasePermissions(mode)); syncDatabasePermissions(mode); syncLocalDbFields(mode); saveSettings(); @@ -1184,10 +1189,14 @@ function syncDatabasePermissions(mode) { } } function saveSettings() { + const mode = localStorage.getItem("catalog-maker:database-mode") || "local"; const permissions = {}; for (const input of permissionInputs) permissions[input.id] = input.checked; - localStorage.setItem("catalog-maker:db-permissions", JSON.stringify(permissions)); + localStorage.setItem(permissionStorageKeys[mode] || permissionStorageKeys.local, JSON.stringify(permissions)); + if (mode === "local") { + localStorage.setItem(legacyPermissionStorageKey, JSON.stringify(permissions)); + } const transactionMode = [...transactionModeInputs].find((input) => input.checked)?.value || "transaction"; localStorage.setItem("catalog-maker:transaction-mode", transactionMode); refreshDatabaseStatusLabel(); @@ -1195,7 +1204,7 @@ function saveSettings() { function refreshDatabaseStatusLabel() { const mode = localStorage.getItem("catalog-maker:database-mode") || "local"; const transactionMode = localStorage.getItem("catalog-maker:transaction-mode") || "transaction"; - const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}"); + const permissions = readDatabasePermissions(mode); const writesEnabled = mode === "local" && (permissions.allowInsertToggle || permissions.allowUpdateToggle || permissions.allowDeleteToggle); const dbLabel = mode === "live" ? "Live 9bplus DB" : mode === "scrape" ? "Local scrape DB" : "Local 9bplus DB"; const configuredDriver = databaseStatus.dataset.driver; @@ -1211,6 +1220,31 @@ function refreshDatabaseStatusLabel() { databaseStateDot.className = `database-state-dot ${databaseConnectionState === "unknown" ? "is-pending" : connectionError ? "is-error" : "is-ready"}`; databaseStateDot.setAttribute("aria-label", connectionError ? "Database disconnected" : databaseConnectionState === "unknown" ? "Database connection pending" : "Database connected"); } +function readDatabasePermissions(mode = "local") { + const storageKey = permissionStorageKeys[mode] || permissionStorageKeys.local; + const stored = readJsonStorage(storageKey); + if (stored) + return stored; + if (mode === "local") { + const legacy = readJsonStorage(legacyPermissionStorageKey); + if (legacy) + return legacy; + } + return {}; +} +function applyPermissionInputs(permissions) { + for (const input of permissionInputs) + input.checked = permissions[input.id] === true; +} +function readJsonStorage(key) { + try { + const value = localStorage.getItem(key); + return value ? JSON.parse(value) : null; + } + catch { + return null; + } +} function renderDatabaseModeNote(mode) { databaseModeNote.textContent = mode === "live" ? "Live 9bplus DB selected for read-only data." diff --git a/public/app.ts b/public/app.ts index 67302a6..6dd027e 100644 --- a/public/app.ts +++ b/public/app.ts @@ -63,6 +63,12 @@ const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle"); let sidebarUserClosed = false; let mappingLoadToken = 0; const mappingCacheKey = "catalog-maker:mapping-cache"; +const legacyPermissionStorageKey = "catalog-maker:db-permissions"; +const permissionStorageKeys = { + local: "catalog-maker:db-permissions:local-9bplus", + live: "catalog-maker:db-permissions:live-9bplus", + scrape: "catalog-maker:db-permissions:local-scrape", +}; const nativeFetch = window.fetch.bind(window); window.fetch = (input, init = {}) => { @@ -71,7 +77,7 @@ window.fetch = (input, init = {}) => { const headers = new Headers(init.headers || (typeof input !== "string" ? input.headers : undefined)); const selectedMode = localStorage.getItem("catalog-maker:database-mode") || "local"; - const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}"); + const permissions = readDatabasePermissions(selectedMode); const localWritesEnabled = selectedMode === "local" && [ permissions.allowInsertToggle, permissions.allowUpdateToggle, @@ -1140,8 +1146,7 @@ function loadDatabaseMode() { const mode = localStorage.getItem("catalog-maker:database-mode") || "local"; const input = document.querySelector(`input[name="databaseMode"][value="${mode}"]`); if (input) input.checked = true; - const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}"); - for (const input of permissionInputs) input.checked = permissions[input.id] === true; + applyPermissionInputs(readDatabasePermissions(mode)); const transactionMode = localStorage.getItem("catalog-maker:transaction-mode") || "transaction"; const transactionInput = document.querySelector(`input[name="transactionMode"][value="${transactionMode}"]`); if (transactionInput) transactionInput.checked = true; @@ -1159,6 +1164,7 @@ function loadDatabaseMode() { function saveDatabaseMode(event) { const mode = ["local", "live", "scrape"].includes(event.target.value) ? event.target.value : "local"; localStorage.setItem("catalog-maker:database-mode", mode); + applyPermissionInputs(readDatabasePermissions(mode)); syncDatabasePermissions(mode); syncLocalDbFields(mode); saveSettings(); @@ -1272,9 +1278,13 @@ function syncDatabasePermissions(mode) { } function saveSettings() { + const mode = localStorage.getItem("catalog-maker:database-mode") || "local"; const permissions = {}; for (const input of permissionInputs) permissions[input.id] = input.checked; - localStorage.setItem("catalog-maker:db-permissions", JSON.stringify(permissions)); + localStorage.setItem(permissionStorageKeys[mode] || permissionStorageKeys.local, JSON.stringify(permissions)); + if (mode === "local") { + localStorage.setItem(legacyPermissionStorageKey, JSON.stringify(permissions)); + } const transactionMode = [...transactionModeInputs].find((input) => input.checked)?.value || "transaction"; localStorage.setItem("catalog-maker:transaction-mode", transactionMode); refreshDatabaseStatusLabel(); @@ -1283,7 +1293,7 @@ function saveSettings() { function refreshDatabaseStatusLabel() { const mode = localStorage.getItem("catalog-maker:database-mode") || "local"; const transactionMode = localStorage.getItem("catalog-maker:transaction-mode") || "transaction"; - const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}"); + const permissions = readDatabasePermissions(mode); const writesEnabled = mode === "local" && ( permissions.allowInsertToggle || permissions.allowUpdateToggle || permissions.allowDeleteToggle ); @@ -1302,6 +1312,32 @@ function refreshDatabaseStatusLabel() { databaseStateDot.setAttribute("aria-label", connectionError ? "Database disconnected" : databaseConnectionState === "unknown" ? "Database connection pending" : "Database connected"); } +function readDatabasePermissions(mode = "local") { + const storageKey = permissionStorageKeys[mode] || permissionStorageKeys.local; + const stored = readJsonStorage(storageKey); + if (stored) return stored; + + if (mode === "local") { + const legacy = readJsonStorage(legacyPermissionStorageKey); + if (legacy) return legacy; + } + + return {}; +} + +function applyPermissionInputs(permissions) { + for (const input of permissionInputs) input.checked = permissions[input.id] === true; +} + +function readJsonStorage(key) { + try { + const value = localStorage.getItem(key); + return value ? JSON.parse(value) : null; + } catch { + return null; + } +} + function renderDatabaseModeNote(mode) { databaseModeNote.textContent = mode === "live" ? "Live 9bplus DB selected for read-only data." diff --git a/public/index.html b/public/index.html index bbdcd10..faff1a6 100644 --- a/public/index.html +++ b/public/index.html @@ -259,7 +259,7 @@ Allow delete -

Live DB is always read-only. Write permissions apply only to Local DB.

+

Write permissions are saved only for Local 9bplus DB. Live and scrape DB stay read-only here.

diff --git a/tests/ui/database.e2e.spec.ts b/tests/ui/database.e2e.spec.ts index 0eb3228..c50b53c 100644 --- a/tests/ui/database.e2e.spec.ts +++ b/tests/ui/database.e2e.spec.ts @@ -40,4 +40,21 @@ test.describe("database connection", () => { expect(data.items.length).toBeGreaterThan(0); expect(data.items.some((item) => item.name === "La Sportiva")).toBeTruthy(); }); + + test("persists Local 9bplus write permissions across reloads", async ({ page }) => { + await page.goto("/"); + await page.locator("#settingsButton").click(); + await page.locator('input[name="databaseMode"][value="local"]').check(); + await page.locator("#allowInsertToggle").check(); + await page.locator("#allowUpdateToggle").check(); + await page.locator("#allowDeleteToggle").check(); + + await page.reload(); + await page.locator("#settingsButton").click(); + + await expect(page.locator('input[name="databaseMode"][value="local"]')).toBeChecked(); + await expect(page.locator("#allowInsertToggle")).toBeChecked(); + await expect(page.locator("#allowUpdateToggle")).toBeChecked(); + await expect(page.locator("#allowDeleteToggle")).toBeChecked(); + }); });