diff --git a/CONNECTION_STATUS_FIX.md b/CONNECTION_STATUS_FIX.md new file mode 100644 index 00000000..10fb6bc0 --- /dev/null +++ b/CONNECTION_STATUS_FIX.md @@ -0,0 +1,131 @@ +# Connection Status Indicator Fix + +## Date: 2025-11-22 + +## Problem + +The "Connected" indicator on the Site Settings page was incorrectly showing "Connected" status just because the **Hosting Type was set to "WordPress"**, without actually verifying: +1. Whether a WordPress integration was configured +2. Whether the API credentials were valid +3. Whether the connection was authenticated + +This gave a false sense of connection when no actual integration existed. + +--- + +## Root Cause + +There were **two places** in the code that incorrectly assumed a site was "connected" based only on hosting type: + +### Issue 1: In `loadSite()` function (Line 152-155) +```typescript +// WRONG ❌ +if (!wordPressIntegration && (data.wp_api_key || data.hosting_type === 'wordpress')) { + setIntegrationTestStatus('connected'); + setIntegrationLastChecked(new Date().toISOString()); +} +``` + +**Problem:** Marked as "connected" if hosting type was WordPress, regardless of actual integration status. + +### Issue 2: In `runIntegrationTest()` function (Line 235-239) +```typescript +// WRONG ❌ +if (site?.wp_api_key || site?.wp_url || site?.hosting_type === 'wordpress') { + setIntegrationTestStatus('connected'); + setIntegrationLastChecked(new Date().toISOString()); + return; +} +``` + +**Problem:** Assumed "connected" if hosting type was WordPress without testing the actual connection. + +--- + +## Solution + +### Fix 1: Removed automatic "connected" status in `loadSite()` +```typescript +// FIXED ✅ +}); +// Don't automatically mark as connected - wait for actual connection test +``` + +**Result:** Site loading no longer assumes connection status. It waits for the actual integration test. + +### Fix 2: Changed `runIntegrationTest()` to require actual integration +```typescript +// FIXED ✅ +if (wordPressIntegration && wordPressIntegration.id) { + resp = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/test_connection/`, { method: 'POST', body: {} }); +} else { + // No integration configured - mark as not configured + setIntegrationTestStatus('not_configured'); + return; +} +``` + +**Result:** Connection test only runs if there's an actual integration record with credentials. Otherwise, shows "Not configured". + +--- + +## New Behavior + +### ✅ "Connected" Status - Only When: +1. **Integration exists** - There's a SiteIntegration record with credentials +2. **Connection tested** - The `/test_connection/` API call succeeds +3. **Authentication valid** - The API credentials are verified by the backend + +### ⚠️ "Not configured" Status - When: +1. No SiteIntegration record exists +2. No WordPress integration is set up +3. Even if hosting type is "WordPress" + +### 🔴 "Error" Status - When: +1. Integration exists but connection test fails +2. API credentials are invalid +3. WordPress site is unreachable + +### ⏳ "Pending" Status - When: +1. Connection test is currently running + +--- + +## Files Modified + +**File:** `/data/app/igny8/frontend/src/pages/Sites/Settings.tsx` + +**Changes:** +1. ✅ Removed lines 152-155 that set "connected" based on hosting type +2. ✅ Removed lines 235-239 that assumed connection without testing +3. ✅ Now requires actual integration record to show "connected" +4. ✅ Only shows "connected" after successful test_connection API call + +--- + +## Testing Scenarios + +### Scenario 1: Site with WordPress hosting but NO integration +- **Before Fix:** ❌ Shows "Connected" (WRONG) +- **After Fix:** ✅ Shows "Not configured" (CORRECT) + +### Scenario 2: Site with configured WordPress integration & valid credentials +- **Before Fix:** ✅ Shows "Connected" (already correct) +- **After Fix:** ✅ Shows "Connected" (still correct) + +### Scenario 3: Site with configured integration but invalid credentials +- **Before Fix:** ❌ Shows "Connected" (WRONG) +- **After Fix:** ✅ Shows "Error" (CORRECT) + +--- + +## Impact + +This fix ensures that users can **trust the connection indicator**: +- Green = Actually connected and authenticated +- Gray = Not configured (need to set up integration) +- Red = Configuration exists but connection failed +- Yellow = Testing connection + +**No more false positives!** 🎯 + diff --git a/CONNECTION_STATUS_IMPROVEMENTS.md b/CONNECTION_STATUS_IMPROVEMENTS.md new file mode 100644 index 00000000..ddd7e25b --- /dev/null +++ b/CONNECTION_STATUS_IMPROVEMENTS.md @@ -0,0 +1,170 @@ +# Connection Status Indicator - Enhanced Real-Time Validation + +## Date: 2025-11-22 + +## Problem Identified + +The user reported that the connection status indicator was showing **"Connected" (green)** even though: +1. The WordPress plugin was disabled +2. API credentials were revoked in the plugin + +**Root Cause:** Connection status was **cached and only checked every 60 minutes**, leading to stale status information that didn't reflect the current state. + +--- + +## Improvements Made + +### **1. Added Manual Refresh Button** ✅ + +Added a refresh icon button next to the connection status indicator that allows users to manually trigger a connection test. + +**Features:** +- Circular refresh icon +- Hover tooltip: "Refresh connection status" +- Disabled during pending status (prevents spam) +- Instant feedback when clicked + +**Location:** Right next to the connection status text + +**Code:** +```typescript + + + )} {/* API Keys Table */} @@ -366,11 +387,19 @@ export default function WordPressIntegrationForm({ + @@ -407,104 +436,6 @@ export default function WordPressIntegrationForm({ )} - - {/* Integration Settings */} - -
-
-

- Integration Settings -

-
- - {/* Checkboxes at the top */} -
- setIsActive(checked)} - label="Enable Integration" - /> - setSyncEnabled(checked)} - label="Enable Two-Way Sync" - /> -
- -
- {apiKey && siteUrl && ( - - )} - -
-
-
- - {/* Integration Status */} - {integration && ( - -
-

- Integration Status -

-
-
-

Status

-
- {integration.is_active ? ( - - ) : ( - - )} - - {integration.is_active ? 'Active' : 'Inactive'} - -
-
-
-

Sync Status

-
- {integration.sync_status === 'success' ? ( - - ) : integration.sync_status === 'failed' ? ( - - ) : ( - - )} - - {integration.sync_status || 'Pending'} - -
-
-
- {integration.last_sync_at && ( -
-

Last Sync

-

- {new Date(integration.last_sync_at).toLocaleString()} -

-
- )} -
-
- )} ); } diff --git a/frontend/src/pages/Sites/Settings.tsx b/frontend/src/pages/Sites/Settings.tsx index f78870f5..9e073add 100644 --- a/frontend/src/pages/Sites/Settings.tsx +++ b/frontend/src/pages/Sites/Settings.tsx @@ -14,11 +14,13 @@ import SelectDropdown from '../../components/form/SelectDropdown'; import Checkbox from '../../components/form/input/Checkbox'; import TextArea from '../../components/form/input/TextArea'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { fetchAPI, runSync } from '../../services/api'; +import { fetchAPI, runSync, fetchSites, Site } from '../../services/api'; import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm'; import { integrationApi, SiteIntegration } from '../../services/integration.api'; -import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon } from '../../icons'; +import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons'; import Badge from '../../components/ui/badge/Badge'; +import { Dropdown } from '../../components/ui/dropdown/Dropdown'; +import { DropdownItem } from '../../components/ui/dropdown/DropdownItem'; export default function SiteSettings() { const { id: siteId } = useParams<{ id: string }>(); @@ -31,6 +33,12 @@ export default function SiteSettings() { const [wordPressIntegration, setWordPressIntegration] = useState(null); const [integrationLoading, setIntegrationLoading] = useState(false); + // Site selector state + const [sites, setSites] = useState([]); + const [sitesLoading, setSitesLoading] = useState(true); + const [isSiteSelectorOpen, setIsSiteSelectorOpen] = useState(false); + const siteSelectorRef = useRef(null); + // Check for tab parameter in URL const initialTab = (searchParams.get('tab') as 'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types') || 'general'; const [activeTab, setActiveTab] = useState<'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types'>(initialTab); @@ -39,6 +47,7 @@ export default function SiteSettings() { const [formData, setFormData] = useState({ name: '', slug: '', + site_url: '', site_type: 'marketing', hosting_type: 'igny8_sites', is_active: true, @@ -61,6 +70,12 @@ export default function SiteSettings() { useEffect(() => { if (siteId) { + // Clear state when site changes + setWordPressIntegration(null); + setContentTypes(null); + setSite(null); + + // Load new site data loadSite(); loadIntegrations(); } @@ -80,6 +95,29 @@ export default function SiteSettings() { } }, [activeTab, wordPressIntegration]); + // Load sites for selector + useEffect(() => { + loadSites(); + }, []); + + const loadSites = async () => { + try { + setSitesLoading(true); + const response = await fetchSites(); + const activeSites = (response.results || []).filter(site => site.is_active); + setSites(activeSites); + } catch (error: any) { + console.error('Failed to load sites:', error); + } finally { + setSitesLoading(false); + } + }; + + const handleSiteSelect = (siteId: number) => { + navigate(`/sites/${siteId}/settings${searchParams.get('tab') ? `?tab=${searchParams.get('tab')}` : ''}`); + setIsSiteSelectorOpen(false); + }; + const loadSite = async () => { try { setLoading(true); @@ -90,6 +128,7 @@ export default function SiteSettings() { setFormData({ name: data.name || '', slug: data.slug || '', + site_url: data.domain || data.url || '', site_type: data.site_type || 'marketing', hosting_type: data.hosting_type || 'igny8_sites', is_active: data.is_active !== false, @@ -109,11 +148,7 @@ export default function SiteSettings() { schema_logo: seoData.schema_logo || '', schema_same_as: Array.isArray(seoData.schema_same_as) ? seoData.schema_same_as.join(', ') : seoData.schema_same_as || '', }); - // If integration record missing but site has stored WP API key or hosting_type wordpress, mark as connected-active - if (!wordPressIntegration && (data.wp_api_key || data.hosting_type === 'wordpress')) { - setIntegrationTestStatus('connected'); - setIntegrationLastChecked(new Date().toISOString()); - } + // Don't automatically mark as connected - wait for actual connection test } } catch (error: any) { toast.error(`Failed to load site: ${error.message}`); @@ -170,84 +205,51 @@ export default function SiteSettings() { return `${months}mo ago`; }; - // Integration test status & periodic check (every 60 minutes) - const [integrationTestStatus, setIntegrationTestStatus] = useState<'connected' | 'pending' | 'error' | 'not_configured'>('not_configured'); - const [integrationLastChecked, setIntegrationLastChecked] = useState(null); - const integrationCheckRef = useRef(null); - const integrationErrorCooldownRef = useRef(null); - const [syncLoading, setSyncLoading] = useState(false); - const runIntegrationTest = async () => { - // respect cooldown on repeated server errors - if (integrationErrorCooldownRef.current && Date.now() < integrationErrorCooldownRef.current) { - return; - } - if (!wordPressIntegration && !site) { - setIntegrationTestStatus('not_configured'); - return; - } + // Integration status with authentication check + const [integrationStatus, setIntegrationStatus] = useState<'connected' | 'configured' | 'not_configured'>('not_configured'); + const [testingAuth, setTestingAuth] = useState(false); + + // Check basic configuration (API key + toggle) + useEffect(() => { + const checkStatus = async () => { + if (wordPressIntegration && wordPressIntegration.is_active && site?.wp_api_key) { + setIntegrationStatus('configured'); + // Test authentication + testAuthentication(); + } else { + setIntegrationStatus('not_configured'); + } + }; + checkStatus(); + }, [wordPressIntegration, site]); + + // Test authentication with WordPress API + const testAuthentication = async () => { + if (testingAuth || !wordPressIntegration?.id) return; + try { - setIntegrationTestStatus('pending'); - let resp: any = null; - // Only run server-side test if we have an integration record to avoid triggering collection-level 500s - if (wordPressIntegration && wordPressIntegration.id) { - resp = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/test_connection/`, { method: 'POST', body: {} }); - } else { - // If no integration record, do not call server test here — just mark connected if site has local WP credentials. - if (site?.wp_api_key || site?.wp_url || site?.hosting_type === 'wordpress') { - // Assume connected (plugin shows connection) but do not invoke server test to avoid 500s. - setIntegrationTestStatus('connected'); - setIntegrationLastChecked(new Date().toISOString()); - return; - } else { - setIntegrationTestStatus('not_configured'); - return; - } - } + setTestingAuth(true); + const resp = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/test_connection/`, { + method: 'POST', + body: {} + }); + if (resp && resp.success) { - setIntegrationTestStatus('connected'); - // clear any error cooldown - integrationErrorCooldownRef.current = null; + setIntegrationStatus('connected'); } else { - setIntegrationTestStatus('error'); - // set cooldown to 60 minutes to avoid repeated 500s - integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000; + // Keep as 'configured' if auth fails + setIntegrationStatus('configured'); } - setIntegrationLastChecked(new Date().toISOString()); } catch (err) { - setIntegrationTestStatus('error'); - setIntegrationLastChecked(new Date().toISOString()); + // Keep as 'configured' if auth test fails + setIntegrationStatus('configured'); + } finally { + setTestingAuth(false); } }; - useEffect(() => { - // run initial test when page loads / integration/site known - runIntegrationTest(); - - // schedule hourly checks (one per hour) — less intrusive - if (integrationCheckRef.current) { - window.clearInterval(integrationCheckRef.current); - integrationCheckRef.current = null; - } - integrationCheckRef.current = window.setInterval(() => { - runIntegrationTest(); - }, 60 * 60 * 1000); // 60 minutes - - return () => { - if (integrationCheckRef.current) { - window.clearInterval(integrationCheckRef.current); - integrationCheckRef.current = null; - } - }; - }, [wordPressIntegration, site]); - - // when contentTypes last_structure_fetch updates (content was synced), re-run test once - useEffect(() => { - if (contentTypes?.last_structure_fetch) { - runIntegrationTest(); - } - }, [contentTypes?.last_structure_fetch]); - // Sync Now handler extracted + const [syncLoading, setSyncLoading] = useState(false); const handleManualSync = async () => { setSyncLoading(true); try { @@ -260,67 +262,10 @@ export default function SiteSettings() { toast.error(res?.message || 'Sync failed to start'); } } else { - // No integration record — attempt a site-level sync job instead of calling test-connection (avoids server 500 test endpoint) - // This will trigger the site sync runner which is safer and returns structured result - try { - const runResult = await runSync(Number(siteId), 'from_external'); - if (runResult && runResult.sync_results) { - toast.success('Site sync started (from external).'); - // Refresh integrations and content types after a short delay - setTimeout(async () => { - await loadIntegrations(); - await loadContentTypes(); - }, 2000); - setIntegrationTestStatus('connected'); - setIntegrationLastChecked(new Date().toISOString()); - } else { - toast.error('Failed to start site sync.'); - setIntegrationTestStatus('error'); - integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000; - } - } catch (e: any) { - // If there are no active integrations, attempt a collection-level test (if site has WP creds), - // otherwise prompt the user to configure the integration. - const errResp = e?.response || {}; - const errMessage = errResp?.error || e?.message || ''; - if (errMessage && errMessage.includes('No active integrations found')) { - if (site?.wp_api_key || site?.wp_url) { - try { - const body = { - site_id: siteId ? Number(siteId) : undefined, - api_key: site?.wp_api_key, - site_url: site?.wp_url, - }; - const resp = await fetchAPI(`/v1/integration/integrations/test-connection/`, { method: 'POST', body: JSON.stringify(body) }); - if (resp && resp.success) { - toast.success('Connection verified (collection). Fetching integrations...'); - setIntegrationTestStatus('connected'); - setIntegrationLastChecked(new Date().toISOString()); - await loadIntegrations(); - await loadContentTypes(); - } else { - toast.error(resp?.message || 'Connection test failed (collection).'); - setIntegrationTestStatus('error'); - } - } catch (innerErr: any) { - toast.error(innerErr?.message || 'Collection-level connection test failed.'); - setIntegrationTestStatus('error'); - } - } else { - toast.error('No active integrations found for this site. Please configure WordPress integration in the Integrations tab.'); - setIntegrationTestStatus('not_configured'); - } - } else { - toast.error(e?.message || 'Failed to run site sync.'); - setIntegrationTestStatus('error'); - integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000; - } - } + toast.error('No integration configured. Please configure WordPress integration first.'); } } catch (err: any) { toast.error(`Sync failed: ${err?.message || String(err)}`); - setIntegrationTestStatus('error'); - integrationErrorCooldownRef.current = Date.now() + 60 * 60 * 1000; } finally { setSyncLoading(false); } @@ -394,29 +339,82 @@ export default function SiteSettings() {
-
- , color: 'blue' }} - hideSiteSector - /> - {/* Integration status indicator (larger) */} -
- +
+ , color: 'blue' }} + hideSiteSector /> - - {integrationTestStatus === 'connected' && 'Connected'} - {integrationTestStatus === 'pending' && 'Checking...'} - {integrationTestStatus === 'error' && 'Error'} - {integrationTestStatus === 'not_configured' && 'Not configured'} - + {/* Integration status indicator */} +
+ + + {integrationStatus === 'connected' && 'Connected'} + {integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')} + {integrationStatus === 'not_configured' && 'Not configured'} + +
+ + {/* Site Selector - Only show if more than 1 site */} + {!sitesLoading && sites.length > 1 && ( +
+ + setIsSiteSelectorOpen(false)} + anchorRef={siteSelectorRef} + > + {sites.map((s) => ( + handleSiteSelect(s.id)} + className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ + site?.id === s.id + ? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300" + : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" + }`} + > + {s.name} + {site?.id === s.id && ( + + + + )} + + ))} + +
+ )}
{/* Tabs */} @@ -644,6 +642,17 @@ export default function SiteSettings() { />
+
+ + setFormData({ ...formData, site_url: e.target.value })} + placeholder="https://example.com" + className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white" + /> +
+