Просмотр исходного кода

feat(scraper): add content enable/disable and improve preview dialog

- Add setContentEnabled method to scraper-client for toggling scraped content
- Add content case in scraper-management to handle PATCH /content/:contentId
- Update to use real content IDs from scraper API instead of synthetic ones
- Fix preview dialog text visibility with prose-invert classes
- Fix Raw/Rendered button styling with proper dark backgrounds
- Hide preview button for disabled URLs
- Handle 409 error when adding duplicate custom URLs with translated message
- Add translations for preview, rendered, raw, openSourceUrl, urlAlreadyExists

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 месяцев назад
Родитель
Сommit
9196db3aa5

+ 87 - 98
shopcall.ai-main/src/components/ManageStoreDataContent.tsx

@@ -75,6 +75,7 @@ interface ScraperContent {
   content_type: 'shipping' | 'contacts' | 'terms' | 'faq';
   title?: string;
   content: string;
+  enabled: boolean;
   scraped_at: string;
   metadata?: Record<string, unknown>;
 }
@@ -550,12 +551,21 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
         fetchCustomUrls();
       } else {
         const error = await response.json();
+        // Handle 409 conflict - URL already exists in scraped content
+        if (response.status === 409 || error.message?.includes('already exists') || error.message?.includes('already been scraped')) {
+          toast({
+            title: t('common.error'),
+            description: t('manageStoreData.website.toast.urlAlreadyExists') || 'This URL has already been scraped from your sitemap. You can enable/disable it in the scraped content list.',
+            variant: "destructive"
+          });
+          return;
+        }
         throw new Error(error.error || 'Failed to add custom URL');
       }
     } catch (error) {
       toast({
         title: t('common.error'),
-        description: error.message || t('manageStoreData.website.toast.urlAddError'),
+        description: (error as Error).message || t('manageStoreData.website.toast.urlAddError'),
         variant: "destructive"
       });
     }
@@ -1545,75 +1555,42 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
                                         </div>
                                       </div>
                                       <div className="flex items-center gap-2 ml-2">
-                                        <Button
-                                          variant="outline"
-                                          size="sm"
-                                          onClick={() => {
-                                            setPreviewContent(content);
-                                            setPreviewMode('rendered');
-                                          }}
-                                          className="border-slate-600 hover:bg-slate-700"
-                                        >
-                                          <Eye className="w-3 h-3 mr-1" />
-                                          Preview
-                                        </Button>
+                                        {content.enabled && (
+                                          <Button
+                                            variant="outline"
+                                            size="sm"
+                                            onClick={() => {
+                                              setPreviewContent(content);
+                                              setPreviewMode('rendered');
+                                            }}
+                                            className="border-slate-600 hover:bg-slate-700 text-slate-200"
+                                          >
+                                            <Eye className="w-3 h-3 mr-1" />
+                                            {t('manageStoreData.website.contentViewer.preview', 'Preview')}
+                                          </Button>
+                                        )}
                                         <Switch
-                                          checked={(() => {
-                                            // Check if this URL exists in customUrls as disabled
-                                            const customUrl = customUrls.find(cu => cu.url === content.url);
-                                            return customUrl ? customUrl.enabled : true;
-                                          })()}
-                                          onCheckedChange={async (checked) => {
-                                            // Toggle URL enabled/disabled
-                                            try {
-                                              const sessionData = localStorage.getItem('session_data');
-                                              if (!sessionData) throw new Error('No session data found');
-
-                                              const session = JSON.parse(sessionData);
-
-                                              // Check if URL already exists in custom URLs
-                                              const existingCustomUrl = customUrls.find(cu => cu.url === content.url);
-
-                                              if (existingCustomUrl) {
-                                                // Update existing custom URL
-                                                const response = await fetch(`${API_URL}/scraper-management/custom-urls/${existingCustomUrl.id}/toggle`, {
-                                                  method: 'PATCH',
-                                                  headers: {
-                                                    'Authorization': `Bearer ${session.session.access_token}`,
-                                                    'Content-Type': 'application/json'
-                                                  },
-                                                  body: JSON.stringify({
-                                                    store_id: selectedStore?.id,
-                                                    enabled: checked
-                                                  })
-                                                });
-
-                                                if (!response.ok) {
-                                                  throw new Error('Failed to toggle URL');
-                                                }
-                                              } else {
-                                                // Add as new custom URL with specified enabled status
-                                                const response = await fetch(`${API_URL}/scraper-management/custom-urls`, {
-                                                  method: 'POST',
-                                                  headers: {
-                                                    'Authorization': `Bearer ${session.session.access_token}`,
-                                                    'Content-Type': 'application/json'
-                                                  },
-                                                  body: JSON.stringify({
-                                                    store_id: selectedStore?.id,
-                                                    url: content.url,
-                                                    content_type: content.content_type,
-                                                  })
-                                                });
-
-                                                if (!response.ok) {
-                                                  throw new Error('Failed to add custom URL');
-                                                }
-
-                                                // If we want to disable it, toggle it right after adding
-                                                if (!checked) {
-                                                  const newCustomUrl = await response.json();
-                                                  await fetch(`${API_URL}/scraper-management/custom-urls/${newCustomUrl.id}/toggle`, {
+                                          checked={content.enabled}
+                                          onCheckedChange={(checked) => {
+                                            // Show warning dialog about toggle consequences
+                                            const isEnabling = checked;
+                                            setConfirmDialog({
+                                              open: true,
+                                              title: isEnabling
+                                                ? t('manageStoreData.website.urlToggle.enableTitle', 'Enable URL Scraping')
+                                                : t('manageStoreData.website.urlToggle.disableTitle', 'Disable URL Scraping'),
+                                              description: isEnabling
+                                                ? t('manageStoreData.website.urlToggle.enableWarning', 'Re-enabling this URL will immediately re-scrape the content and create new embeddings. This may take a few moments.')
+                                                : t('manageStoreData.website.urlToggle.disableWarning', 'Disabling this URL will remove all scraped content and embeddings for this page. If you re-enable it later, the content will be re-scraped immediately.'),
+                                              action: async () => {
+                                                try {
+                                                  const sessionData = localStorage.getItem('session_data');
+                                                  if (!sessionData) throw new Error('No session data found');
+
+                                                  const session = JSON.parse(sessionData);
+
+                                                  // Call the content enable/disable endpoint
+                                                  const response = await fetch(`${API_URL}/scraper-management/content/${content.id}`, {
                                                     method: 'PATCH',
                                                     headers: {
                                                       'Authorization': `Bearer ${session.session.access_token}`,
@@ -1621,28 +1598,36 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
                                                     },
                                                     body: JSON.stringify({
                                                       store_id: selectedStore?.id,
-                                                      enabled: false
+                                                      enabled: checked
                                                     })
                                                   });
+
+                                                  if (!response.ok) {
+                                                    const errorData = await response.json().catch(() => ({}));
+                                                    throw new Error(errorData.error || 'Failed to toggle content');
+                                                  }
+
+                                                  toast({
+                                                    title: checked
+                                                      ? t('manageStoreData.website.toast.urlEnabled', 'Content enabled')
+                                                      : t('manageStoreData.website.toast.urlDisabled', 'Content disabled'),
+                                                    description: checked
+                                                      ? t('manageStoreData.website.urlToggle.enableSuccess', 'URL has been re-scraped and content is now available')
+                                                      : t('manageStoreData.website.urlToggle.disableSuccess', 'Scraped content and embeddings have been removed'),
+                                                  });
+
+                                                  // Refresh content list
+                                                  fetchScraperContent();
+                                                } catch (error) {
+                                                  console.error('Error toggling content:', error);
+                                                  toast({
+                                                    title: t('manageStoreData.website.toast.urlToggleError', 'Error'),
+                                                    description: error instanceof Error ? error.message : 'Failed to toggle content status',
+                                                    variant: "destructive",
+                                                  });
                                                 }
                                               }
-
-                                              toast({
-                                                title: checked ? "URL enabled" : "URL disabled",
-                                                description: checked ? "This URL will be included in future scrapes" : "This URL will be skipped in future scrapes",
-                                              });
-
-                                              // Refresh both content and custom URLs
-                                              fetchScraperContent();
-                                              fetchCustomUrls();
-                                            } catch (error) {
-                                              console.error('Error toggling URL:', error);
-                                              toast({
-                                                title: "Error",
-                                                description: "Failed to toggle URL status",
-                                                variant: "destructive",
-                                              });
-                                            }
+                                            });
                                           }}
                                           className="data-[state=checked]:bg-cyan-500"
                                         />
@@ -1720,22 +1705,26 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
               </span>
               <div className="flex items-center gap-2 ml-4">
                 <Button
-                  variant={previewMode === 'rendered' ? 'default' : 'outline'}
+                  variant="outline"
                   size="sm"
                   onClick={() => setPreviewMode('rendered')}
-                  className={previewMode === 'rendered' ? 'bg-cyan-500 hover:bg-cyan-600 text-white' : 'border-slate-600 text-slate-200 hover:text-white hover:bg-slate-700'}
+                  className={previewMode === 'rendered'
+                    ? 'bg-cyan-500 hover:bg-cyan-600 text-white border-cyan-500'
+                    : 'bg-slate-700 border-slate-600 text-slate-200 hover:text-white hover:bg-slate-600'}
                 >
                   <Eye className="w-3 h-3 mr-1" />
-                  Rendered
+                  {t('manageStoreData.website.contentViewer.rendered', 'Rendered')}
                 </Button>
                 <Button
-                  variant={previewMode === 'raw' ? 'default' : 'outline'}
+                  variant="outline"
                   size="sm"
                   onClick={() => setPreviewMode('raw')}
-                  className={previewMode === 'raw' ? 'bg-cyan-500 hover:bg-cyan-600 text-white' : 'border-slate-600 text-slate-200 hover:text-white hover:bg-slate-700'}
+                  className={previewMode === 'raw'
+                    ? 'bg-cyan-500 hover:bg-cyan-600 text-white border-cyan-500'
+                    : 'bg-slate-700 border-slate-600 text-slate-200 hover:text-white hover:bg-slate-600'}
                 >
                   <Code className="w-3 h-3 mr-1" />
-                  Raw
+                  {t('manageStoreData.website.contentViewer.raw', 'Raw')}
                 </Button>
               </div>
             </DialogTitle>
@@ -1750,20 +1739,20 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
                 className="text-cyan-400 hover:text-cyan-300 underline text-xs flex items-center gap-1"
               >
                 <Globe className="w-3 h-3" />
-                Open source URL
+                {t('manageStoreData.website.contentViewer.openSourceUrl', 'Open source URL')}
                 <ExternalLink className="w-3 h-3" />
               </a>
             </DialogDescription>
           </DialogHeader>
-          <div className="flex-1 overflow-y-auto mt-4">
+          <div className="flex-1 overflow-y-auto mt-4 bg-slate-900 rounded-lg p-4">
             {previewMode === 'rendered' ? (
-              <div className="prose prose-invert prose-cyan max-w-none prose-headings:text-white prose-p:text-slate-200 prose-li:text-slate-200 prose-strong:text-white prose-a:text-cyan-400">
+              <div className="prose prose-invert max-w-none text-slate-200 prose-headings:text-white prose-p:text-slate-200 prose-li:text-slate-200 prose-strong:text-white prose-a:text-cyan-400 prose-code:text-cyan-300 prose-blockquote:text-slate-300 prose-blockquote:border-slate-600">
                 <ReactMarkdown>
                   {previewContent?.content || ''}
                 </ReactMarkdown>
               </div>
             ) : (
-              <pre className="bg-slate-900 p-4 rounded-lg text-slate-300 text-sm overflow-x-auto whitespace-pre-wrap">
+              <pre className="text-slate-300 text-sm overflow-x-auto whitespace-pre-wrap">
                 {previewContent?.content || ''}
               </pre>
             )}
@@ -1771,7 +1760,7 @@ export function ManageStoreDataContent({ defaultTab }: ManageStoreDataContentPro
           {previewContent?.scraped_at && (
             <div className="mt-4 pt-4 border-t border-slate-700 text-slate-400 text-xs">
               <Clock className="w-3 h-3 inline mr-1" />
-              Scraped: {new Date(previewContent.scraped_at).toLocaleString()}
+              {t('manageStoreData.website.contentViewer.scraped', 'Scraped')}: {new Date(previewContent.scraped_at).toLocaleString()}
             </div>
           )}
         </DialogContent>

+ 14 - 1
shopcall.ai-main/src/i18n/locales/de.json

@@ -1421,6 +1421,10 @@
         "noContentDescription": "Keine Inhalte gefunden. Der Scraper wird automatisch Inhalte entdecken.",
         "untitled": "Ohne Titel",
         "scraped": "Gescrapt",
+        "preview": "Vorschau",
+        "rendered": "Formatiert",
+        "raw": "Rohtext",
+        "openSourceUrl": "Quell-URL öffnen",
         "groupedByPage": "Nach Seite gruppiert",
         "scrapingInProgress": "Scraping läuft...",
         "enableScheduling": "Aktivieren Sie oben das geplante Scraping, um automatisch Inhalte zu sammeln.",
@@ -1445,7 +1449,16 @@
         "urlDisabled": "Benutzerdefinierte URL erfolgreich deaktiviert",
         "urlToggleError": "Fehler beim Aktualisieren der benutzerdefinierten URL",
         "urlDeleted": "Benutzerdefinierte URL erfolgreich gelöscht",
-        "urlDeleteError": "Fehler beim Löschen der benutzerdefinierten URL"
+        "urlDeleteError": "Fehler beim Löschen der benutzerdefinierten URL",
+        "urlAlreadyExists": "Diese URL wurde bereits aus Ihrer Sitemap gescrapt. Sie können sie in der Liste der gescrapten Inhalte aktivieren/deaktivieren."
+      },
+      "urlToggle": {
+        "enableTitle": "URL-Scraping aktivieren",
+        "disableTitle": "URL-Scraping deaktivieren",
+        "enableWarning": "Das Reaktivieren dieser URL wird den Inhalt sofort erneut scrapen und neue Embeddings erstellen. Dies kann einen Moment dauern.",
+        "disableWarning": "Das Deaktivieren dieser URL entfernt alle gescrapten Inhalte und Embeddings für diese Seite. Wenn Sie sie später wieder aktivieren, wird der Inhalt sofort erneut gescrapt.",
+        "enableSuccess": "URL wurde erneut gescrapt und der Inhalt ist jetzt verfügbar",
+        "disableSuccess": "Gescrapte Inhalte und Embeddings wurden entfernt"
       }
     }
   },

+ 14 - 1
shopcall.ai-main/src/i18n/locales/en.json

@@ -1577,6 +1577,10 @@
         "noContentDescription": "No content found. The scraper will discover content automatically.",
         "untitled": "Untitled",
         "scraped": "Scraped",
+        "preview": "Preview",
+        "rendered": "Rendered",
+        "raw": "Raw",
+        "openSourceUrl": "Open source URL",
         "groupedByPage": "Grouped by page",
         "scrapingInProgress": "Scraping in progress...",
         "enableScheduling": "Enable scheduled scraping above to automatically collect content.",
@@ -1601,7 +1605,16 @@
         "urlDisabled": "Custom URL disabled successfully",
         "urlToggleError": "Failed to update custom URL",
         "urlDeleted": "Custom URL deleted successfully",
-        "urlDeleteError": "Failed to delete custom URL"
+        "urlDeleteError": "Failed to delete custom URL",
+        "urlAlreadyExists": "This URL has already been scraped from your sitemap. You can enable/disable it in the scraped content list."
+      },
+      "urlToggle": {
+        "enableTitle": "Enable URL Scraping",
+        "disableTitle": "Disable URL Scraping",
+        "enableWarning": "Re-enabling this URL will immediately re-scrape the content and create new embeddings. This may take a few moments.",
+        "disableWarning": "Disabling this URL will remove all scraped content and embeddings for this page. If you re-enable it later, the content will be re-scraped immediately.",
+        "enableSuccess": "URL has been re-scraped and content is now available",
+        "disableSuccess": "Scraped content and embeddings have been removed"
       }
     }
   },

+ 14 - 1
shopcall.ai-main/src/i18n/locales/hu.json

@@ -1567,6 +1567,10 @@
         "noContentDescription": "Nem található tartalom.",
         "untitled": "Névtelen",
         "scraped": "Letöltve",
+        "preview": "Előnézet",
+        "rendered": "Formázott",
+        "raw": "Nyers",
+        "openSourceUrl": "Forrás URL megnyitása",
         "groupedByPage": "Oldalak szerint csoportosítva",
         "scrapingInProgress": "Scraping folyamatban...",
         "enableScheduling": "Engedélyezze fent az ütemezett scrapinget a tartalom automatikus gyűjtéséhez.",
@@ -1591,7 +1595,16 @@
         "urlDisabled": "Egyedi URL sikeresen letiltva",
         "urlToggleError": "Hiba az egyedi URL frissítése során",
         "urlDeleted": "Egyedi URL sikeresen törölve",
-        "urlDeleteError": "Hiba az egyedi URL törlése során"
+        "urlDeleteError": "Hiba az egyedi URL törlése során",
+        "urlAlreadyExists": "Ez az URL már le lett olvasva a sitemap-ből. Az engedélyezés/tiltás a scraped tartalom listában módosítható."
+      },
+      "urlToggle": {
+        "enableTitle": "URL Scraping Engedélyezése",
+        "disableTitle": "URL Scraping Letiltása",
+        "enableWarning": "Az URL újraengedélyezése azonnal újra beolvassa a tartalmat és létrehozza az új beágyazásokat. Ez néhány pillanatig eltarthat.",
+        "disableWarning": "Az URL letiltása eltávolítja az összes beolvasott tartalmat és beágyazást erről az oldalról. Ha később újra engedélyezi, a tartalom azonnal újra lesz olvasva.",
+        "enableSuccess": "Az URL újra lett olvasva és a tartalom elérhető",
+        "disableSuccess": "A beolvasott tartalom és beágyazások eltávolítva"
       }
     }
   },

+ 2 - 2
supabase/functions/_shared/cors.ts

@@ -38,8 +38,8 @@ export function getCorsHeaders(requestOrigin?: string, additionalMethods: string
     allowOrigin = allowedOrigins[0] || '*';
   }
 
-  // Default methods
-  const defaultMethods = ['GET', 'POST', 'OPTIONS'];
+  // Default methods - include PATCH and DELETE for API operations
+  const defaultMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
   const allMethods = [...new Set([...defaultMethods, ...additionalMethods])];
 
   return {

+ 24 - 2
supabase/functions/_shared/scraper-client.ts

@@ -50,9 +50,11 @@ export interface ScraperContent {
 
 // Raw content item from scraper API /results endpoint
 export interface ScraperResultItem {
+  id: string;
   url: string;
   content: string;
   changed: boolean;
+  enabled: boolean;
   last_updated: string;
   title?: string;
 }
@@ -318,9 +320,12 @@ export class ScraperClient {
 
   /**
    * Enable or disable a custom URL
+   * customUrlId can be either a scraper-generated ID or a full URL (for sitemap URLs)
    */
   async setCustomUrlEnabled(shopId: string, customUrlId: string, enabled: boolean): Promise<void> {
-    await this.makeRequest(`/shops/${shopId}/custom-urls/${customUrlId}`, {
+    // URL-encode the customUrlId in case it's a full URL (for sitemap URLs)
+    const encodedId = encodeURIComponent(customUrlId);
+    await this.makeRequest(`/shops/${shopId}/custom-urls/${encodedId}`, {
       method: 'PATCH',
       body: JSON.stringify({ enabled }),
     });
@@ -328,12 +333,29 @@ export class ScraperClient {
 
   /**
    * Delete a custom URL
+   * customUrlId can be either a scraper-generated ID or a full URL (for sitemap URLs)
    */
   async deleteCustomUrl(shopId: string, customUrlId: string): Promise<void> {
-    await this.makeRequest(`/shops/${shopId}/custom-urls/${customUrlId}`, {
+    // URL-encode the customUrlId in case it's a full URL (for sitemap URLs)
+    const encodedId = encodeURIComponent(customUrlId);
+    await this.makeRequest(`/shops/${shopId}/custom-urls/${encodedId}`, {
       method: 'DELETE',
     });
   }
+
+  /**
+   * Enable or disable scraped content by content ID
+   * When disabling:
+   * - Deletes all Qdrant embeddings for that content
+   * - Clears content, hash, and title from database
+   * - Keeps URL record with enabled=false
+   */
+  async setContentEnabled(shopId: string, contentId: string, enabled: boolean): Promise<void> {
+    await this.makeRequest(`/shops/${shopId}/content/${contentId}`, {
+      method: 'PATCH',
+      body: JSON.stringify({ enabled }),
+    });
+  }
 }
 
 /**

+ 79 - 7
supabase/functions/scraper-management/index.ts

@@ -329,14 +329,15 @@ Deno.serve(async (req) => {
         const results = rawContent.results || {};
 
         // Helper function to transform content items
-        // Now uses the title from the API response if available
+        // Uses the real content ID from the scraper API for enable/disable functionality
         const transformContentItems = (items: ScraperResultItem[] | undefined, type: string) => {
-          return (items || []).map((item: ScraperResultItem, index: number) => ({
-            id: `${type}-${index}-${item.url}`,
+          return (items || []).map((item: ScraperResultItem) => ({
+            id: item.id, // Use real ID from scraper API
             url: item.url,
             content_type: type,
             title: item.title || item.url.split('/').pop()?.replace(/-/g, ' ') || type,
             content: item.content || '',
+            enabled: item.enabled,
             scraped_at: item.last_updated || new Date().toISOString(),
             metadata: {
               changed: item.changed,
@@ -470,9 +471,12 @@ Deno.serve(async (req) => {
 
         // Handle PATCH /custom-urls/:customUrlId/toggle and DELETE /custom-urls/:customUrlId
         if (pathParts.length >= 2) {
-          const customUrlId = pathParts[1];
+          const rawCustomUrlId = pathParts[1];
           const subAction = pathParts[2]; // 'toggle' or undefined
 
+          // Decode URL-encoded ID (for sitemap URLs passed as identifier)
+          const customUrlId = decodeURIComponent(rawCustomUrlId);
+
           // Parse body once for PATCH/DELETE
           let bodyData: any = null;
           if (req.method === 'PATCH' || req.method === 'DELETE') {
@@ -485,6 +489,7 @@ Deno.serve(async (req) => {
 
           const store_id = bodyData?.store_id;
           const enabled = bodyData?.enabled;
+          const urlFromBody = bodyData?.url; // URL passed in body for sitemap URLs
 
           if (!store_id) {
             return new Response(
@@ -507,8 +512,11 @@ Deno.serve(async (req) => {
             scraper_api_secret: store.scraper_api_secret,
           });
 
+          // Determine the identifier to use - prefer URL from body if provided
+          const urlIdentifier = urlFromBody || customUrlId;
+
           // Log the shop ID being used for debugging
-          console.log(`[scraper-management] custom-urls PATCH/DELETE: Using shop ID: ${store_id}, customUrlId: ${customUrlId}`);
+          console.log(`[scraper-management] custom-urls PATCH/DELETE: Using shop ID: ${store_id}, customUrlId: ${customUrlId}, urlFromBody: ${urlFromBody}, urlIdentifier: ${urlIdentifier}`);
 
           // Verify shop exists in scraper before proceeding
           try {
@@ -533,6 +541,7 @@ Deno.serve(async (req) => {
           if (req.method === 'PATCH') {
             // Toggle custom URL enabled status
             // Support both PATCH /custom-urls/:id and PATCH /custom-urls/:id/toggle
+            // For sitemap URLs, the urlIdentifier will be the full URL
             if (typeof enabled !== 'boolean') {
               return new Response(
                 JSON.stringify({ error: 'enabled (boolean) is required' }),
@@ -540,8 +549,8 @@ Deno.serve(async (req) => {
               );
             }
 
-            console.log(`[scraper-management] Toggling custom URL ${customUrlId} for store ${store_id} to enabled=${enabled}`);
-            await scraperClient.setCustomUrlEnabled(store_id, customUrlId, enabled);
+            console.log(`[scraper-management] Toggling URL ${urlIdentifier} for store ${store_id} to enabled=${enabled}`);
+            await scraperClient.setCustomUrlEnabled(store_id, urlIdentifier, enabled);
 
             return new Response(
               JSON.stringify({ success: true, message: `Custom URL ${enabled ? 'enabled' : 'disabled'}` }),
@@ -568,6 +577,69 @@ Deno.serve(async (req) => {
         break;
       }
 
+      case 'content': {
+        // PATCH /content/:contentId - Enable/disable scraped content
+        // This endpoint proxies to the scraper API: PATCH /api/shops/:id/content/:contentId
+        if (pathParts.length < 2) {
+          return new Response(
+            JSON.stringify({ error: 'Content ID is required' }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          );
+        }
+
+        const contentId = pathParts[1];
+
+        if (req.method !== 'PATCH') {
+          return new Response(
+            JSON.stringify({ error: 'Method not allowed' }),
+            { status: 405, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          );
+        }
+
+        const { store_id, enabled } = await req.json();
+
+        if (!store_id) {
+          return new Response(
+            JSON.stringify({ error: 'store_id is required' }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          );
+        }
+
+        if (typeof enabled !== 'boolean') {
+          return new Response(
+            JSON.stringify({ error: 'enabled (boolean) is required' }),
+            { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          );
+        }
+
+        const store = await getStoreScraperConfig(supabase, store_id, user.id);
+
+        if (!store.scraper_registered) {
+          return new Response(
+            JSON.stringify({ error: 'Store not registered with scraper' }),
+            { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          );
+        }
+
+        const scraperClient = await createScraperClient({
+          scraper_api_url: store.scraper_api_url,
+          scraper_api_secret: store.scraper_api_secret,
+        });
+
+        console.log(`[scraper-management] ${enabled ? 'Enabling' : 'Disabling'} content ${contentId} for store ${store_id}`);
+        await scraperClient.setContentEnabled(store_id, contentId, enabled);
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: enabled
+              ? 'Content enabled - it will be re-scraped on the next scheduled scrape'
+              : 'Content disabled - scraped data has been removed',
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        );
+      }
+
       case 'scheduling': {
         // PATCH /scheduling
         if (req.method !== 'PATCH') {