|
|
@@ -1,16 +1,23 @@
|
|
|
import { ApiService } from '../../services/ApiService';
|
|
|
import { ToastService } from '../../services/ToastService';
|
|
|
import { Router } from '../../app/Router';
|
|
|
-import { SiteDetail, QdrantEmbedding, QdrantError } from '../../types';
|
|
|
+import { SiteDetail, QdrantEmbedding, QdrantError, CustomUrl, UrlExclusion } from '../../types';
|
|
|
import { getIcon } from '../../utils/icons';
|
|
|
import { formatRelativeTime, getDomainFromUrl, getStatusBadgeClass, getContentTypeLabel } from '../../utils/helpers';
|
|
|
|
|
|
+const ALL_CONTENT_TYPES = [
|
|
|
+ 'shipping', 'contacts', 'terms', 'faq', 'services', 'about', 'team',
|
|
|
+ 'blog', 'pricing', 'testimonials', 'gallery', 'landing', 'other'
|
|
|
+] as const;
|
|
|
+
|
|
|
export class SiteDetailPage {
|
|
|
private element: HTMLElement | null = null;
|
|
|
private siteDetail: SiteDetail | null = null;
|
|
|
private eventsbound: boolean = false;
|
|
|
private qdrantEmbeddings: QdrantEmbedding[] = [];
|
|
|
private qdrantErrors: QdrantError[] = [];
|
|
|
+ private customUrls: CustomUrl[] = [];
|
|
|
+ private urlExclusions: UrlExclusion[] = [];
|
|
|
|
|
|
constructor(
|
|
|
private siteId: string,
|
|
|
@@ -45,10 +52,12 @@ export class SiteDetailPage {
|
|
|
if (siteResponse.success && siteResponse.data) {
|
|
|
this.siteDetail = siteResponse.data;
|
|
|
|
|
|
- // Load Qdrant data in parallel (optional - these can fail without breaking the page)
|
|
|
- const [embeddingsResponse, errorsResponse] = await Promise.allSettled([
|
|
|
+ // Load Qdrant data, custom URLs, and exclusions in parallel (optional - these can fail without breaking the page)
|
|
|
+ const [embeddingsResponse, errorsResponse, customUrlsResponse, exclusionsResponse] = await Promise.allSettled([
|
|
|
this.apiService.getSiteQdrantEmbeddings(this.siteId),
|
|
|
- this.apiService.getSiteQdrantErrors(this.siteId)
|
|
|
+ this.apiService.getSiteQdrantErrors(this.siteId),
|
|
|
+ this.apiService.getCustomUrls(this.siteId),
|
|
|
+ this.apiService.getUrlExclusions(this.siteId)
|
|
|
]);
|
|
|
|
|
|
// Handle embeddings response (won't break if it fails)
|
|
|
@@ -69,6 +78,22 @@ export class SiteDetailPage {
|
|
|
console.warn('[SiteDetailPage] Errors fetch failed:', errorsResponse.reason);
|
|
|
}
|
|
|
|
|
|
+ // Handle custom URLs response
|
|
|
+ if (customUrlsResponse.status === 'fulfilled' && customUrlsResponse.value.success && customUrlsResponse.value.data) {
|
|
|
+ const urlsData = (customUrlsResponse.value.data as any).data?.custom_urls || (customUrlsResponse.value.data as any).custom_urls || [];
|
|
|
+ this.customUrls = urlsData;
|
|
|
+ } else if (customUrlsResponse.status === 'rejected') {
|
|
|
+ console.warn('[SiteDetailPage] Custom URLs fetch failed:', customUrlsResponse.reason);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle URL exclusions response
|
|
|
+ if (exclusionsResponse.status === 'fulfilled' && exclusionsResponse.value.success && exclusionsResponse.value.data) {
|
|
|
+ const exclData = (exclusionsResponse.value.data as any).data?.url_exclusions || (exclusionsResponse.value.data as any).url_exclusions || [];
|
|
|
+ this.urlExclusions = exclData;
|
|
|
+ } else if (exclusionsResponse.status === 'rejected') {
|
|
|
+ console.warn('[SiteDetailPage] URL exclusions fetch failed:', exclusionsResponse.reason);
|
|
|
+ }
|
|
|
+
|
|
|
this.renderSiteDetail();
|
|
|
} else {
|
|
|
console.error('[SiteDetailPage] Site API returned error:', siteResponse.error);
|
|
|
@@ -251,6 +276,72 @@ export class SiteDetailPage {
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- Crawl Settings -->
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white">Crawl Settings</h3>
|
|
|
+ <p class="text-sm text-gray-500 dark:text-gray-400">Configure crawling behavior for this site</p>
|
|
|
+ </div>
|
|
|
+ <div class="card-body">
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
+ Sitemap URL
|
|
|
+ </label>
|
|
|
+ <div class="flex items-center space-x-2">
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="sitemap-url-input"
|
|
|
+ value="${site.custom_sitemap_url || site.sitemap_url || ''}"
|
|
|
+ placeholder="https://example.com/sitemap.xml"
|
|
|
+ class="flex-1 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
|
+ />
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ id="redetect-sitemap"
|
|
|
+ class="btn btn-secondary"
|
|
|
+ title="Re-detect sitemap"
|
|
|
+ >
|
|
|
+ Re-detect
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
+ Max Crawl Depth
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="max-crawl-depth-input"
|
|
|
+ value="${site.max_crawl_depth ?? 3}"
|
|
|
+ min="1"
|
|
|
+ max="5"
|
|
|
+ class="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
|
+ Max Pages
|
|
|
+ </label>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ id="max-pages-input"
|
|
|
+ value="${site.max_pages ?? 100}"
|
|
|
+ min="1"
|
|
|
+ max="1000"
|
|
|
+ class="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="mt-4 flex justify-end">
|
|
|
+ <button type="button" id="save-crawl-settings" class="btn btn-primary">
|
|
|
+ <div class="w-4 h-4 mr-1">${getIcon('save')}</div>
|
|
|
+ Save Settings
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- Content Overview -->
|
|
|
<div class="card">
|
|
|
<div class="card-header">
|
|
|
@@ -258,13 +349,143 @@ export class SiteDetailPage {
|
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">Latest scraped content by category</p>
|
|
|
</div>
|
|
|
<div class="card-body">
|
|
|
- <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
- ${Object.entries(content_metadata).map(([key, items]) => `
|
|
|
- <div class="text-center">
|
|
|
- <div class="text-lg font-semibold text-gray-900 dark:text-white">${items.length}</div>
|
|
|
- <div class="text-sm text-gray-500 dark:text-gray-400">${getContentTypeLabel(key)}</div>
|
|
|
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
|
|
+ ${(['shipping', 'contacts', 'terms', 'faq', 'services', 'about', 'team', 'blog', 'pricing', 'testimonials', 'gallery', 'landing', 'other'] as const).map(key => {
|
|
|
+ const items = (content_metadata as unknown as Record<string, any[]>)[key] || [];
|
|
|
+ const count = items.length;
|
|
|
+ const muted = count === 0;
|
|
|
+ return `
|
|
|
+ <div class="rounded-lg border border-gray-200 dark:border-gray-700 p-3 text-center${muted ? ' opacity-40' : ''}">
|
|
|
+ <div class="text-lg font-semibold ${muted ? 'text-gray-400 dark:text-gray-600' : 'text-gray-900 dark:text-white'}">${count}</div>
|
|
|
+ <div class="text-sm ${muted ? 'text-gray-400 dark:text-gray-600' : 'text-gray-500 dark:text-gray-400'}">${getContentTypeLabel(key)}</div>
|
|
|
+ </div>`;
|
|
|
+ }).join('')}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Custom URLs -->
|
|
|
+ <div class="card">
|
|
|
+ <div class="card-header">
|
|
|
+ <h3 class="text-lg font-medium text-gray-900 dark:text-white">Custom URLs</h3>
|
|
|
+ <p class="text-sm text-gray-500 dark:text-gray-400">Manage include URLs and exclude patterns for crawling</p>
|
|
|
+ </div>
|
|
|
+ <div class="card-body space-y-6">
|
|
|
+ <!-- Include URLs -->
|
|
|
+ <div>
|
|
|
+ <div class="flex items-center justify-between mb-3">
|
|
|
+ <h4 class="text-sm font-medium text-gray-900 dark:text-white">Include URLs</h4>
|
|
|
+ <button type="button" id="toggle-add-url-form" class="btn btn-secondary btn-sm">
|
|
|
+ + Add URL
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div id="add-url-form" class="hidden mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="new-custom-url"
|
|
|
+ placeholder="https://example.com/page"
|
|
|
+ class="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
|
+ />
|
|
|
+ <select
|
|
|
+ id="new-custom-url-type"
|
|
|
+ class="px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
|
+ >
|
|
|
+ ${ALL_CONTENT_TYPES.map(t => `<option value="${t}">${getContentTypeLabel(t)}</option>`).join('')}
|
|
|
+ </select>
|
|
|
+ <button type="button" id="add-custom-url-btn" class="btn btn-primary btn-sm">
|
|
|
+ Add
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- `).join('')}
|
|
|
+ </div>
|
|
|
+ ${this.customUrls.length === 0 ? `
|
|
|
+ <p class="text-sm text-gray-500 dark:text-gray-400">No custom URLs configured.</p>
|
|
|
+ ` : `
|
|
|
+ <div class="overflow-x-auto">
|
|
|
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">URL</th>
|
|
|
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Category</th>
|
|
|
+ <th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Enabled</th>
|
|
|
+ <th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ ${this.customUrls.map(cu => `
|
|
|
+ <tr>
|
|
|
+ <td class="px-3 py-2 text-sm text-gray-900 dark:text-white break-all">${cu.url}</td>
|
|
|
+ <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">${getContentTypeLabel(cu.content_type)}</td>
|
|
|
+ <td class="px-3 py-2 text-center">
|
|
|
+ <label class="inline-flex items-center cursor-pointer">
|
|
|
+ <input type="checkbox" class="sr-only custom-url-toggle" data-id="${cu.id}" ${cu.enabled ? 'checked' : ''}>
|
|
|
+ <div class="relative">
|
|
|
+ <div class="block bg-gray-600 w-10 h-6 rounded-full"></div>
|
|
|
+ <div class="dot absolute left-0.5 top-0.5 bg-white w-5 h-5 rounded-full transition ${cu.enabled ? 'transform translate-x-4 bg-blue-600' : ''}"></div>
|
|
|
+ </div>
|
|
|
+ </label>
|
|
|
+ </td>
|
|
|
+ <td class="px-3 py-2 text-center">
|
|
|
+ <button type="button" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 delete-custom-url" data-id="${cu.id}">
|
|
|
+ <div class="w-4 h-4">${getIcon('delete')}</div>
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ `).join('')}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ `}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Exclude Patterns -->
|
|
|
+ <div>
|
|
|
+ <div class="flex items-center justify-between mb-3">
|
|
|
+ <h4 class="text-sm font-medium text-gray-900 dark:text-white">Exclude Patterns</h4>
|
|
|
+ <button type="button" id="toggle-add-exclusion-form" class="btn btn-secondary btn-sm">
|
|
|
+ + Add Pattern
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <div id="add-exclusion-form" class="hidden mb-4 p-3 bg-gray-50 dark:bg-gray-700 rounded-lg">
|
|
|
+ <div class="flex items-center space-x-3">
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ id="new-exclusion-pattern"
|
|
|
+ placeholder="*/admin/*"
|
|
|
+ class="flex-1 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
|
|
+ />
|
|
|
+ <button type="button" id="add-exclusion-btn" class="btn btn-primary btn-sm">
|
|
|
+ Add
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Use * as wildcard, e.g. */admin/*</p>
|
|
|
+ </div>
|
|
|
+ ${this.urlExclusions.length === 0 ? `
|
|
|
+ <p class="text-sm text-gray-500 dark:text-gray-400">No exclusion patterns configured.</p>
|
|
|
+ ` : `
|
|
|
+ <div class="overflow-x-auto">
|
|
|
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Pattern</th>
|
|
|
+ <th class="px-3 py-2 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Actions</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
|
+ ${this.urlExclusions.map(excl => `
|
|
|
+ <tr>
|
|
|
+ <td class="px-3 py-2 text-sm text-gray-900 dark:text-white font-mono">${excl.pattern}</td>
|
|
|
+ <td class="px-3 py-2 text-center">
|
|
|
+ <button type="button" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 delete-exclusion" data-id="${excl.id}">
|
|
|
+ <div class="w-4 h-4">${getIcon('delete')}</div>
|
|
|
+ </button>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ `).join('')}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ `}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -663,5 +884,206 @@ export class SiteDetailPage {
|
|
|
this.toastService.error('Error', 'Failed to clear custom ID');
|
|
|
}
|
|
|
});
|
|
|
+
|
|
|
+ // --- Crawl Settings ---
|
|
|
+ this.element.querySelector('#save-crawl-settings')?.addEventListener('click', async () => {
|
|
|
+ if (!this.siteDetail) return;
|
|
|
+
|
|
|
+ const sitemapInput = this.element?.querySelector('#sitemap-url-input') as HTMLInputElement;
|
|
|
+ const depthInput = this.element?.querySelector('#max-crawl-depth-input') as HTMLInputElement;
|
|
|
+ const pagesInput = this.element?.querySelector('#max-pages-input') as HTMLInputElement;
|
|
|
+
|
|
|
+ const settings: {
|
|
|
+ custom_sitemap_url?: string | null;
|
|
|
+ max_crawl_depth?: number;
|
|
|
+ max_pages?: number;
|
|
|
+ } = {};
|
|
|
+
|
|
|
+ const sitemapVal = sitemapInput?.value.trim() || null;
|
|
|
+ settings.custom_sitemap_url = sitemapVal;
|
|
|
+
|
|
|
+ const depth = parseInt(depthInput?.value || '3', 10);
|
|
|
+ if (depth >= 1 && depth <= 5) {
|
|
|
+ settings.max_crawl_depth = depth;
|
|
|
+ }
|
|
|
+
|
|
|
+ const pages = parseInt(pagesInput?.value || '100', 10);
|
|
|
+ if (pages >= 1 && pages <= 1000) {
|
|
|
+ settings.max_pages = pages;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.apiService.updateSiteSettings(this.siteId, settings);
|
|
|
+ if (response.success) {
|
|
|
+ this.toastService.success('Settings Saved', 'Crawl settings have been updated');
|
|
|
+ await this.loadSiteDetail();
|
|
|
+ } else {
|
|
|
+ this.toastService.error('Save Failed', response.error || 'Failed to save crawl settings');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.toastService.error('Error', 'Failed to save crawl settings');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Re-detect sitemap
|
|
|
+ this.element.querySelector('#redetect-sitemap')?.addEventListener('click', async () => {
|
|
|
+ if (!this.siteDetail) return;
|
|
|
+
|
|
|
+ const btn = this.element?.querySelector('#redetect-sitemap') as HTMLButtonElement;
|
|
|
+ if (btn) { btn.disabled = true; btn.textContent = 'Detecting...'; }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.apiService.detectSite(this.siteDetail.site.url);
|
|
|
+ if (response.success && response.data) {
|
|
|
+ const detected = (response.data as any).data || response.data;
|
|
|
+ const sitemapInput = this.element?.querySelector('#sitemap-url-input') as HTMLInputElement;
|
|
|
+ if (sitemapInput && detected.sitemap_url) {
|
|
|
+ sitemapInput.value = detected.sitemap_url;
|
|
|
+ }
|
|
|
+ this.toastService.success('Detected', detected.sitemap_url ? `Sitemap found: ${detected.sitemap_url}` : 'No sitemap detected');
|
|
|
+ } else {
|
|
|
+ this.toastService.error('Detection Failed', response.error || 'Failed to detect sitemap');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.toastService.error('Error', 'Failed to detect sitemap');
|
|
|
+ } finally {
|
|
|
+ if (btn) { btn.disabled = false; btn.textContent = 'Re-detect'; }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // --- Custom URLs ---
|
|
|
+ // Toggle add URL form
|
|
|
+ this.element.querySelector('#toggle-add-url-form')?.addEventListener('click', () => {
|
|
|
+ const form = this.element?.querySelector('#add-url-form');
|
|
|
+ form?.classList.toggle('hidden');
|
|
|
+ });
|
|
|
+
|
|
|
+ // Add custom URL
|
|
|
+ this.element.querySelector('#add-custom-url-btn')?.addEventListener('click', async () => {
|
|
|
+ const urlInput = this.element?.querySelector('#new-custom-url') as HTMLInputElement;
|
|
|
+ const typeSelect = this.element?.querySelector('#new-custom-url-type') as HTMLSelectElement;
|
|
|
+
|
|
|
+ const url = urlInput?.value.trim();
|
|
|
+ const contentType = typeSelect?.value;
|
|
|
+
|
|
|
+ if (!url) {
|
|
|
+ this.toastService.error('Validation Error', 'URL is required');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.apiService.addCustomUrl(this.siteId, { url, content_type: contentType });
|
|
|
+ if (response.success) {
|
|
|
+ this.toastService.success('URL Added', 'Custom URL has been added');
|
|
|
+ this.eventsbound = false;
|
|
|
+ await this.loadSiteDetail();
|
|
|
+ } else {
|
|
|
+ this.toastService.error('Add Failed', response.error || 'Failed to add custom URL');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.toastService.error('Error', 'Failed to add custom URL');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Toggle custom URL enabled/disabled
|
|
|
+ this.element.querySelectorAll('.custom-url-toggle').forEach(toggle => {
|
|
|
+ toggle.addEventListener('change', async (e) => {
|
|
|
+ const checkbox = e.target as HTMLInputElement;
|
|
|
+ const customUrlId = checkbox.dataset.id;
|
|
|
+ if (!customUrlId) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.apiService.updateCustomUrl(this.siteId, customUrlId, checkbox.checked);
|
|
|
+ if (response.success) {
|
|
|
+ this.toastService.success('Updated', `Custom URL ${checkbox.checked ? 'enabled' : 'disabled'}`);
|
|
|
+ } else {
|
|
|
+ checkbox.checked = !checkbox.checked;
|
|
|
+ this.toastService.error('Update Failed', response.error || 'Failed to toggle custom URL');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ checkbox.checked = !checkbox.checked;
|
|
|
+ this.toastService.error('Error', 'Failed to toggle custom URL');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // Delete custom URL
|
|
|
+ this.element.querySelectorAll('.delete-custom-url').forEach(btn => {
|
|
|
+ btn.addEventListener('click', async (e) => {
|
|
|
+ const button = (e.currentTarget as HTMLElement);
|
|
|
+ const customUrlId = button.dataset.id;
|
|
|
+ if (!customUrlId) return;
|
|
|
+
|
|
|
+ if (!confirm('Delete this custom URL?')) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.apiService.deleteCustomUrl(this.siteId, customUrlId);
|
|
|
+ if (response.success) {
|
|
|
+ this.toastService.success('Deleted', 'Custom URL has been removed');
|
|
|
+ this.eventsbound = false;
|
|
|
+ await this.loadSiteDetail();
|
|
|
+ } else {
|
|
|
+ this.toastService.error('Delete Failed', response.error || 'Failed to delete custom URL');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.toastService.error('Error', 'Failed to delete custom URL');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // --- URL Exclusions ---
|
|
|
+ // Toggle add exclusion form
|
|
|
+ this.element.querySelector('#toggle-add-exclusion-form')?.addEventListener('click', () => {
|
|
|
+ const form = this.element?.querySelector('#add-exclusion-form');
|
|
|
+ form?.classList.toggle('hidden');
|
|
|
+ });
|
|
|
+
|
|
|
+ // Add exclusion pattern
|
|
|
+ this.element.querySelector('#add-exclusion-btn')?.addEventListener('click', async () => {
|
|
|
+ const patternInput = this.element?.querySelector('#new-exclusion-pattern') as HTMLInputElement;
|
|
|
+ const pattern = patternInput?.value.trim();
|
|
|
+
|
|
|
+ if (!pattern) {
|
|
|
+ this.toastService.error('Validation Error', 'Pattern is required');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.apiService.addUrlExclusion(this.siteId, pattern);
|
|
|
+ if (response.success) {
|
|
|
+ this.toastService.success('Pattern Added', 'Exclusion pattern has been added');
|
|
|
+ this.eventsbound = false;
|
|
|
+ await this.loadSiteDetail();
|
|
|
+ } else {
|
|
|
+ this.toastService.error('Add Failed', response.error || 'Failed to add exclusion pattern');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.toastService.error('Error', 'Failed to add exclusion pattern');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Delete exclusion pattern
|
|
|
+ this.element.querySelectorAll('.delete-exclusion').forEach(btn => {
|
|
|
+ btn.addEventListener('click', async (e) => {
|
|
|
+ const button = (e.currentTarget as HTMLElement);
|
|
|
+ const exclusionId = button.dataset.id;
|
|
|
+ if (!exclusionId) return;
|
|
|
+
|
|
|
+ if (!confirm('Delete this exclusion pattern?')) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await this.apiService.deleteUrlExclusion(this.siteId, exclusionId);
|
|
|
+ if (response.success) {
|
|
|
+ this.toastService.success('Deleted', 'Exclusion pattern has been removed');
|
|
|
+ this.eventsbound = false;
|
|
|
+ await this.loadSiteDetail();
|
|
|
+ } else {
|
|
|
+ this.toastService.error('Delete Failed', response.error || 'Failed to delete exclusion pattern');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.toastService.error('Error', 'Failed to delete exclusion pattern');
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
}
|
|
|
}
|