From 418123d74230a89469c7693f67cd446d84fb4534 Mon Sep 17 00:00:00 2001 From: Ben Miller Date: Wed, 24 Dec 2025 21:14:19 -0700 Subject: [PATCH] feat: add troubleshooting side panel and advanced queue controls - Implemented a global toggle to enable/disable background queue processing. - Added a Side Panel (Sidebar.html) to view pending edits. - Added per-item controls: 'Delete' to remove from queue, 'Push' to force update. - Updated 'onEditQueue.ts' with robust error handling for batch processing. - Updated documentation (README, ARCHITECTURE) to reflect new features. - Fixed 'clasp' deployment issues by cleaning up manifest management. --- .gitignore | 3 +- README.md | 1 + docs/ARCHITECTURE.md | 14 ++++ docs/SETUP.md | 15 +++- package-lock.json | 119 ++++++++++++++++++++++++++++ package.json | 3 +- src/Sidebar.html | 181 +++++++++++++++++++++++++++++++++++++++++++ src/appsscript.json | 12 +++ src/global.ts | 7 ++ src/initMenu.ts | 2 + src/onEditQueue.ts | 14 +++- src/sidebar.ts | 83 ++++++++++++++++++++ webpack.config.js | 10 ++- 13 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 src/Sidebar.html create mode 100644 src/appsscript.json create mode 100644 src/sidebar.ts diff --git a/.gitignore b/.gitignore index 1c6e801..e30675b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ **/node_modules/** dist/** desktop.ini -.continue/** \ No newline at end of file +.continue/** +.clasp.json \ No newline at end of file diff --git a/README.md b/README.md index 328c99e..88b2bc7 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ The system allows you to: - Automatically upload product photos from Google Drive to Shopify. - specific triggers (`onEdit`, `onOpen`) to sync changes to Shopify in real-time or on-demand. - Handle rate limiting and concurrency using a custom queue system. +- Monitor and troubleshoot background processes via a custom side panel. ## Prerequisites diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fe8c48a..3432a4c 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -66,3 +66,17 @@ Triggers are managed programmatically via `src/triggers.ts`. Running `reinstallT - `onEdit` -> `matchProductToShopifyOnEditHandler` - `onEdit` -> `onEditQueue` - `TimeBased (1 min)` -> `processBatchedEdits` + +### 5. Troubleshooting Panel (`src/sidebar.ts`, `src/Sidebar.html`) + +A dedicated side panel provides visibility into the background queue system. + +- **Backend (`src/sidebar.ts`)**: + - `getQueueStatus()`: Returns the current state of the queue and global toggle. + - `setQueueEnabled()`: Toggles the global `queueEnabled` script property. + - `deleteEdit()` / `pushEdit()`: Manages specific items in the queue with safety checks. + +- **Frontend (`src/Sidebar.html`)**: + - Displays pending edits with timestamps. + - Provides controls to globally enable/disable processing. + - Allows manual intervention (delete/push) for individual items. diff --git a/docs/SETUP.md b/docs/SETUP.md index 325d297..cbe31e1 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -21,10 +21,23 @@ 3. **Clasp Login** Authenticate with Google to allow pushing code. - ```bash + + ```powershell clasp login ``` +4. **Initialize Project** + You must either create a new Apps Script project or clone an existing one. + - **Option A: New Project** + ```bash + clasp create --type sheets --title "Product Inventory" --rootDir ./dist + ``` + - **Option B: Existing Project** + ```bash + clasp clone --rootDir ./dist + ``` + *Note: The `--rootDir ./dist` flag is crucial so that clasp knows where to look for files.* + ## Google Sheets Configuration 1. **Create a Google Sheet** (or use existing). diff --git a/package-lock.json b/package-lock.json index 3cf7ba5..76b26c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "devDependencies": { "@types/google-apps-script": "^1.0.85", + "copy-webpack-plugin": "^13.0.1", "gas-webpack-plugin": "^2.6.0", "graphql-tag": "^2.12.6", "shopify-admin-api-typings": "github:beepmill/shopify-admin-api-typings", @@ -609,6 +610,30 @@ "dev": true, "license": "MIT" }, + "node_modules/copy-webpack-plugin": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", + "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -898,6 +923,19 @@ "webpack-sources": "^3.1.1" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -1008,6 +1046,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1200,6 +1261,16 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -1606,6 +1677,54 @@ } } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index aeaae4d..3b102f3 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,11 @@ ], "scripts": { "build": "webpack --mode production", - "deploy": "clasp push -P ./dist" + "deploy": "clasp push" }, "devDependencies": { "@types/google-apps-script": "^1.0.85", + "copy-webpack-plugin": "^13.0.1", "gas-webpack-plugin": "^2.6.0", "graphql-tag": "^2.12.6", "shopify-admin-api-typings": "github:beepmill/shopify-admin-api-typings", diff --git a/src/Sidebar.html b/src/Sidebar.html new file mode 100644 index 0000000..467fdcc --- /dev/null +++ b/src/Sidebar.html @@ -0,0 +1,181 @@ + + + + + + + + +

System Status

+ +
+ +
+ +
+
-
+
Pending Edits
+ + + + + + + + + + + + +
SKUTimeActions
+
+ + + + + + diff --git a/src/appsscript.json b/src/appsscript.json new file mode 100644 index 0000000..3285671 --- /dev/null +++ b/src/appsscript.json @@ -0,0 +1,12 @@ +{ + "timeZone": "America/Denver", + "dependencies": { + }, + "exceptionLogging": "STACKDRIVER", + "runtimeVersion": "V8", + "oauthScopes": [ + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/script.external_request", + "https://www.googleapis.com/auth/script.container.ui" + ] +} diff --git a/src/global.ts b/src/global.ts index 1110434..d265f6a 100644 --- a/src/global.ts +++ b/src/global.ts @@ -10,6 +10,7 @@ import { updateShopifyProductHandler, reauthorizeScript, } from "./initMenu" + import { createMissingPhotoFolders } from "./createMissingPhotoFolders" import { reinstallTriggers } from "./triggers" import { newSkuHandler } from "./newSku" @@ -19,6 +20,7 @@ import { processBatchedEdits } from "./onEditQueue" import { fillProductFromTemplate } from "./fillProductFromTemplate" +import { showSidebar, getQueueStatus, setQueueEnabled, deleteEdit, pushEdit } from "./sidebar" // prettier-ignore ;(global as any).onOpen = onOpen @@ -36,3 +38,8 @@ import { fillProductFromTemplate } from "./fillProductFromTemplate" ;(global as any).newSkuHandler = newSkuHandler ;(global as any).fillProductFromTemplate = fillProductFromTemplate ;(global as any).createMissingPhotoFolders = createMissingPhotoFolders +;(global as any).showSidebar = showSidebar +;(global as any).getQueueStatus = getQueueStatus +;(global as any).setQueueEnabled = setQueueEnabled +;(global as any).deleteEdit = deleteEdit +;(global as any).pushEdit = pushEdit diff --git a/src/initMenu.ts b/src/initMenu.ts index 85299b1..0eb6802 100644 --- a/src/initMenu.ts +++ b/src/initMenu.ts @@ -4,6 +4,7 @@ import { createMissingPhotoFolders } from "./createMissingPhotoFolders" import { matchProductToShopify, updateProductToShopify } from "./match" import { reinstallTriggers } from "./triggers" import { toastAndLog } from "./sheetUtils" +import { showSidebar } from "./sidebar" export function initMenu() { let ui = SpreadsheetApp.getUi() @@ -29,6 +30,7 @@ export function initMenu() { .createMenu("Utilities...") .addItem("Reauthorize script", reauthorizeScript.name) .addItem("Reinstall triggers", reinstallTriggers.name) + .addItem("Troubleshoot", showSidebar.name) ) .addToUi() } diff --git a/src/onEditQueue.ts b/src/onEditQueue.ts index 3e06730..a7a50c1 100644 --- a/src/onEditQueue.ts +++ b/src/onEditQueue.ts @@ -8,6 +8,7 @@ const LOCK_TIMEOUT_MS = 10 * 1000 // 10 seconds for lock acquisition const CACHE_KEY_EDITS = "pendingEdits" const CACHE_KEY_LAST_EDIT_TIME = "lastEditTime" const SCRIPT_PROPERTY_TRIGGER_SCHEDULED = "batchTriggerScheduled" +export const SCRIPT_PROPERTY_QUEUE_ENABLED = "queueEnabled" export function onEditQueue(e) { const sheet = e.source.getActiveSheet() @@ -79,6 +80,13 @@ export function processBatchedEdits() { } console.log(`Total SKUs in queue: ${pendingEdits.length}`) + + const queueEnabled = scriptProperties.getProperty(SCRIPT_PROPERTY_QUEUE_ENABLED) !== "false" + if (!queueEnabled) { + console.log("Queue disabled, skipping processing.") + return + } + const now = Date.now() const toProcess = pendingEdits.filter( (edit) => now - edit.timestamp > BATCH_INTERVAL_MS @@ -96,7 +104,11 @@ export function processBatchedEdits() { return } let p = new Product(edit.sku) - p.UpdateShopifyProduct(shop) + try { + p.UpdateShopifyProduct(shop) + } catch (err) { + console.error(`Failed to update SKU ${edit.sku}: ${err.message}`) + } }) pendingEdits = pendingEdits.filter( diff --git a/src/sidebar.ts b/src/sidebar.ts new file mode 100644 index 0000000..8081559 --- /dev/null +++ b/src/sidebar.ts @@ -0,0 +1,83 @@ +import { SCRIPT_PROPERTY_QUEUE_ENABLED } from "./onEditQueue"; +import { Product } from "./Product"; +import { Shop } from "./shopifyApi"; + +const CACHE_KEY_EDITS = "pendingEdits" +const LOCK_TIMEOUT_MS = 10000; + +export function showSidebar() { + const html = HtmlService.createHtmlOutputFromFile('Sidebar') + .setTitle('Troubleshooting') + .setWidth(300); + SpreadsheetApp.getUi().showSidebar(html); +} + +export function getQueueStatus() { + const scriptProperties = PropertiesService.getScriptProperties(); + let pendingEdits = []; + try { + const pendingEditsStr = scriptProperties.getProperty(CACHE_KEY_EDITS); + pendingEdits = pendingEditsStr ? JSON.parse(pendingEditsStr) : []; + } catch (e) { + console.log("Cache corruption: " + e.message); + } + + const queueEnabled = scriptProperties.getProperty(SCRIPT_PROPERTY_QUEUE_ENABLED) !== "false"; + + // Convert timestamps to readable strings + return { + enabled: queueEnabled, + items: pendingEdits.map(edit => ({ + sku: edit.sku, + time: new Date(edit.timestamp).toLocaleString() + })) + }; +} + +export function setQueueEnabled(enabled: boolean) { + const scriptProperties = PropertiesService.getScriptProperties(); + scriptProperties.setProperty(SCRIPT_PROPERTY_QUEUE_ENABLED, enabled.toString()); +} + +export function deleteEdit(sku: string) { + const lock = LockService.getScriptLock(); + try { + lock.waitLock(LOCK_TIMEOUT_MS); + const scriptProperties = PropertiesService.getScriptProperties(); + const pendingEditsStr = scriptProperties.getProperty(CACHE_KEY_EDITS); + let pendingEdits = pendingEditsStr ? JSON.parse(pendingEditsStr) : []; + + pendingEdits = pendingEdits.filter(edit => edit.sku !== sku); + + scriptProperties.setProperty(CACHE_KEY_EDITS, JSON.stringify(pendingEdits)); + } finally { + lock.releaseLock(); + } +} + +export function pushEdit(sku: string) { + const lock = LockService.getScriptLock(); + try { + lock.waitLock(LOCK_TIMEOUT_MS); + const scriptProperties = PropertiesService.getScriptProperties(); + + // 1. Process the edit safely (try/catch already handled in Product methods or here) + // We do this BEFORE removing from queue to ensure data safety if it fails. + let shop = new Shop(); + let p = new Product(sku); + p.UpdateShopifyProduct(shop); + + // 2. If successful, remove from queue + const pendingEditsStr = scriptProperties.getProperty(CACHE_KEY_EDITS); + let pendingEdits = pendingEditsStr ? JSON.parse(pendingEditsStr) : []; + + pendingEdits = pendingEdits.filter(edit => edit.sku !== sku); + scriptProperties.setProperty(CACHE_KEY_EDITS, JSON.stringify(pendingEdits)); + + } catch (e) { + throw new Error(`Failed to push edit for SKU ${sku}: ${e.message}`); + } finally { + // Ensure lock is always released + lock.releaseLock(); + } +} diff --git a/webpack.config.js b/webpack.config.js index 5d6738d..4ed7811 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,6 +2,8 @@ const path = require('path'); const GasPlugin = require("gas-webpack-plugin"); const TerserPlugin = require('terser-webpack-plugin'); +const CopyPlugin = require("copy-webpack-plugin"); + module.exports = { entry: './src/global.ts', optimization: { @@ -35,6 +37,12 @@ module.exports = { new GasPlugin({ comment: true, autoGlobalExportsFiles: ['**/*.ts'], - }) + }), + new CopyPlugin({ + patterns: [ + { from: "src/*.html", to: "[name][ext]" }, + { from: "src/appsscript.json", to: "[name][ext]" }, + ], + }), ] }; \ No newline at end of file