diff --git a/.gitignore b/.gitignore index 7f6c135..bbe2bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ tmp/ *.log *.sql *.sql.gz +!sql/local-scrape-db.sql test-results/ playwright-report/ diff --git a/agent.md b/agent.md index 432f31c..150a1d8 100644 --- a/agent.md +++ b/agent.md @@ -65,15 +65,28 @@ This project replaces the Excel/VBA workflow named `Catalog maker - 20` with a N ## Database architecture -There are two selectable data sources: +There are three selectable data sources: -### Live DB +### Local 9bplus DB + +- Uses the imported local MariaDB database named `9bplus`. +- This is the only database allowed to receive explicit catalog writes. +- Writes remain disabled unless the user enables the local permission switches. + +### Live 9bplus DB - Uses the existing 9b-plus API endpoint configured in `config/local.json`. - Credentials are secret and must stay server-side. - Must remain read-only. -### Local DB +### Local scrape DB + +- Uses the separate local MariaDB database named `catalog_scrape`. +- Stores scraper source configuration, scrape runs, discovered products and assets. +- It must never be used as a fallback for catalog reads. When selected in the UI, existing catalog reads continue using Local 9bplus DB until dedicated scrape-storage endpoints are added. +- The schema is versioned in `sql/local-scrape-db.sql` and can be created with `npm run db:create-scrape`. + +### Local MariaDB connection - MariaDB is installed locally as service `MariaDB`. - Host: `localhost` / `127.0.0.1`. @@ -86,12 +99,13 @@ There are two selectable data sources: ## Database safety behavior -- The UI has `Local DB` and `Live DB` settings. +- The UI has `Local 9bplus DB`, `Live 9bplus DB` and `Local scrape DB` settings. - The UI sends the selected mode using the `X-Database-Mode` request header. - Backend data API routes reject a Local DB request when MariaDB is not configured instead of silently using the endpoint. - Backend data API routes reject a Live DB request when the project is configured only for MariaDB. - Static HTML/CSS/JS files must remain available even when a database is not configured. - The top status shows the selected source, connection state, permissions and execution mode. +- Selecting Live 9bplus DB or Local scrape DB disables catalog write permissions in the UI. ## Important current files diff --git a/config/local.example.json b/config/local.example.json index 63647af..5327aa6 100644 --- a/config/local.example.json +++ b/config/local.example.json @@ -19,7 +19,8 @@ "allowWrites": false, "host": "127.0.0.1", "port": 3306, - "name": "catalog_maker_test", + "name": "9bplus", + "scrapeName": "catalog_scrape", "user": "catalog_maker", "password": "", "connectionLimit": 5 diff --git a/package.json b/package.json index d3504e0..b0ab817 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "tsx src/cli.ts", "dev": "tsx src/server.ts", "catalog:dry-run": "tsx src/cli.ts catalog-maker --dry-run", + "db:create-scrape": "tsx scripts/create-local-scrape-db.ts", "test": "tsx --test test/*.test.ts", "test:ui": "playwright test", "test:e2e": "playwright test tests/ui/database.e2e.spec.ts", diff --git a/public/app.js b/public/app.js index 00da89e..f845614 100644 --- a/public/app.js +++ b/public/app.js @@ -59,7 +59,10 @@ window.fetch = (input, init = {}) => { if (!url.startsWith("/api/")) return nativeFetch(input, init); const headers = new Headers(init.headers || (typeof input !== "string" ? input.headers : undefined)); - headers.set("X-Database-Mode", localStorage.getItem("catalog-maker:database-mode") || "local"); + const selectedMode = localStorage.getItem("catalog-maker:database-mode") || "local"; + // Scrape storage is separate; existing catalog endpoints continue reading Local 9bplus. + headers.set("X-Database-Mode", selectedMode === "live" ? "live" : "local"); + headers.set("X-Scrape-Database", selectedMode === "scrape" ? "1" : "0"); return nativeFetch(input, { ...init, headers }); }; let currentPosition = 1; @@ -940,7 +943,7 @@ function loadDatabaseMode() { refreshDatabaseStatusLabel(); } function saveDatabaseMode(event) { - const mode = event.target.value === "live" ? "live" : "local"; + const mode = ["local", "live", "scrape"].includes(event.target.value) ? event.target.value : "local"; localStorage.setItem("catalog-maker:database-mode", mode); syncDatabasePermissions(mode); syncLocalDbFields(mode); @@ -987,10 +990,10 @@ function syncLocalDbFields(mode) { localDbFields.hidden = mode !== "local"; } function syncDatabasePermissions(mode) { - const isLive = mode === "live"; + const isReadOnly = mode === "live" || mode === "scrape"; for (const input of permissionInputs) { - input.disabled = isLive; - if (isLive) + input.disabled = isReadOnly; + if (isReadOnly) input.checked = false; } } @@ -1008,7 +1011,7 @@ function refreshDatabaseStatusLabel() { const transactionMode = localStorage.getItem("catalog-maker:transaction-mode") || "transaction"; const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}"); const writesEnabled = mode === "local" && (permissions.allowInsertToggle || permissions.allowUpdateToggle || permissions.allowDeleteToggle); - const dbLabel = mode === "live" ? "Live DB" : "Local DB"; + const dbLabel = mode === "live" ? "Live 9bplus DB" : mode === "scrape" ? "Local scrape DB" : "Local 9bplus DB"; const configuredDriver = databaseStatus.dataset.driver; const connectionError = (mode === "local" && configuredDriver !== "mariadb") || (mode === "live" && configuredDriver !== "endpoint"); @@ -1022,8 +1025,10 @@ function refreshDatabaseStatusLabel() { } function renderDatabaseModeNote(mode) { databaseModeNote.textContent = mode === "live" - ? "Live DB selected for preparation. The current project still stays read-only." - : "Local test database selected."; + ? "Live 9bplus DB selected for read-only data." + : mode === "scrape" + ? "Local scrape DB selected for scraper settings and scraped data. Catalog reads stay on Local 9bplus DB." + : "Local 9bplus DB selected."; } function renderProduct(product) { const productKey = `${manufacturerSelect.value}:${product.idProductCatalog || product.supplierReference || product.name || ""}`; diff --git a/public/app.ts b/public/app.ts index f0391a7..c0bd70b 100644 --- a/public/app.ts +++ b/public/app.ts @@ -60,7 +60,10 @@ window.fetch = (input, init = {}) => { if (!url.startsWith("/api/")) return nativeFetch(input, init); const headers = new Headers(init.headers || (typeof input !== "string" ? input.headers : undefined)); - headers.set("X-Database-Mode", localStorage.getItem("catalog-maker:database-mode") || "local"); + const selectedMode = localStorage.getItem("catalog-maker:database-mode") || "local"; + // Scrape storage is separate; existing catalog endpoints continue reading Local 9bplus. + headers.set("X-Database-Mode", selectedMode === "live" ? "live" : "local"); + headers.set("X-Scrape-Database", selectedMode === "scrape" ? "1" : "0"); return nativeFetch(input, { ...init, headers }); }; @@ -1034,7 +1037,7 @@ function loadDatabaseMode() { } function saveDatabaseMode(event) { - const mode = event.target.value === "live" ? "live" : "local"; + const mode = ["local", "live", "scrape"].includes(event.target.value) ? event.target.value : "local"; localStorage.setItem("catalog-maker:database-mode", mode); syncDatabasePermissions(mode); syncLocalDbFields(mode); @@ -1082,10 +1085,10 @@ function syncLocalDbFields(mode) { } function syncDatabasePermissions(mode) { - const isLive = mode === "live"; + const isReadOnly = mode === "live" || mode === "scrape"; for (const input of permissionInputs) { - input.disabled = isLive; - if (isLive) input.checked = false; + input.disabled = isReadOnly; + if (isReadOnly) input.checked = false; } } @@ -1105,7 +1108,7 @@ function refreshDatabaseStatusLabel() { const writesEnabled = mode === "local" && ( permissions.allowInsertToggle || permissions.allowUpdateToggle || permissions.allowDeleteToggle ); - const dbLabel = mode === "live" ? "Live DB" : "Local DB"; + const dbLabel = mode === "live" ? "Live 9bplus DB" : mode === "scrape" ? "Local scrape DB" : "Local 9bplus DB"; const configuredDriver = databaseStatus.dataset.driver; const connectionError = (mode === "local" && configuredDriver !== "mariadb") || (mode === "live" && configuredDriver !== "endpoint"); @@ -1120,8 +1123,10 @@ function refreshDatabaseStatusLabel() { function renderDatabaseModeNote(mode) { databaseModeNote.textContent = mode === "live" - ? "Live DB selected for preparation. The current project still stays read-only." - : "Local test database selected."; + ? "Live 9bplus DB selected for read-only data." + : mode === "scrape" + ? "Local scrape DB selected for scraper settings and scraped data. Catalog reads stay on Local 9bplus DB." + : "Local 9bplus DB selected."; } function renderProduct(product) { diff --git a/public/index.html b/public/index.html index a58596d..e19aa7b 100644 --- a/public/index.html +++ b/public/index.html @@ -151,11 +151,15 @@ +
Local test database selected.
+Local 9bplus database selected.