UI styling improvements: dashboard headers and navigation

- Restore blue PageHeader on Dashboard (/app-components)
- Update homepage (/) with subtle header design without blue bar
- Add uniform PageHeader styling to application edit page
- Fix Rapporten link on homepage to point to /reports overview
- Improve header descriptions spacing for better readability
This commit is contained in:
2026-01-21 03:24:56 +01:00
parent e276e77fbc
commit cdee0e8819
138 changed files with 24551 additions and 3352 deletions

View File

@@ -0,0 +1,876 @@
/**
* ArchitectureDebugPage - Debug page for testing refactored architecture
*
* Tests:
* A - Level2 recursion: referencedObject with attributes is stored
* B - Disabled type: NOT full sync, but reference-only caching works
* C - Attribute wipe rule: shallow referencedObject doesn't delete existing values
* D - QueryService reconstruction: DB → TS object is correct
* E - Write-through: update to Jira + immediate DB update
*/
import { useState, useEffect } from 'react';
import { getConfiguredObjectTypes } from '../services/api';
import PageHeader from './PageHeader';
interface TestResult {
test: string;
status: 'pending' | 'running' | 'success' | 'error';
message?: string;
data?: any;
sqlQueries?: Array<{ sql: string; result: any }>;
timestamp?: string;
}
export default function ArchitectureDebugPage() {
const [testResults, setTestResults] = useState<Record<string, TestResult>>({});
const [loading, setLoading] = useState<Record<string, boolean>>({});
const [enabledTypes, setEnabledTypes] = useState<string[]>([]);
const [loadingEnabledTypes, setLoadingEnabledTypes] = useState(true);
const [inputs, setInputs] = useState({
typeName: 'ApplicationComponent',
objectKey: '',
referencedObjectKey: '',
disabledTypeName: 'HostingType',
updateField: 'description',
updateValue: 'Updated via Debug Page',
});
// Load enabled object types on mount
useEffect(() => {
const loadEnabledTypes = async () => {
try {
setLoadingEnabledTypes(true);
const config = await getConfiguredObjectTypes();
const enabled = config.schemas.flatMap(s =>
s.objectTypes.filter(ot => ot.enabled).map(ot => ot.objectTypeName)
);
setEnabledTypes(enabled);
} catch (error) {
console.error('Failed to load enabled types:', error);
} finally {
setLoadingEnabledTypes(false);
}
};
loadEnabledTypes();
}, []);
const updateTestResult = (testId: string, updates: Partial<TestResult>) => {
setTestResults(prev => ({
...prev,
[testId]: {
...prev[testId],
...updates,
timestamp: new Date().toISOString(),
} as TestResult,
}));
};
const executeSqlQuery = async (sql: string, params: any[] = []): Promise<any> => {
try {
const response = await fetch('/api/v2/debug/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ sql, params }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
const data = await response.json();
return data.result || [];
} catch (error) {
console.error('SQL query failed:', error);
throw error;
}
};
// Test A: Level2 recursion
const runTestA = async () => {
const testId = 'testA';
setLoading(prev => ({ ...prev, [testId]: true }));
updateTestResult(testId, { test: 'Test A: Level2 Recursion', status: 'running' });
try {
// Step 1: Sync object type
updateTestResult(testId, { message: `Syncing object type "${inputs.typeName}"...` });
const syncResponse = await fetch(`/api/v2/sync/objects/${inputs.typeName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
});
if (!syncResponse.ok) {
const errorData = await syncResponse.json().catch(() => ({ error: `HTTP ${syncResponse.status}: Sync failed` }));
const errorMessage = errorData.error || `HTTP ${syncResponse.status}: Sync failed`;
// Extract enabled types from error details if available
let enabledTypesHint = '';
if (errorData.details?.enabledTypeNames && errorData.details.enabledTypeNames.length > 0) {
enabledTypesHint = ` Currently enabled: ${errorData.details.enabledTypeNames.join(', ')}.`;
}
// Provide helpful hints for common errors
let hint = '';
if (errorMessage.includes('not enabled')) {
hint = ` Tip: Go to Schema Configuration settings and enable "${inputs.typeName}".${enabledTypesHint}`;
} else if (errorMessage.includes('token')) {
hint = ' Tip: Check if JIRA_SERVICE_ACCOUNT_TOKEN is configured in .env or if you have a Personal Access Token configured.';
} else if (errorMessage.includes('404') || errorMessage.includes('not found')) {
hint = ' Tip: The object type may not exist in your Jira Assets instance. Check the type name spelling.';
}
throw new Error(errorMessage + hint);
}
const syncResult = await syncResponse.json();
// Check for errors in sync result
if (syncResult.hasErrors && syncResult.errors && syncResult.errors.length > 0) {
const errorMessages = syncResult.errors.map((e: any) => `${e.objectId}: ${e.error}`).join('; ');
console.warn(`Sync completed with errors: ${errorMessages}`);
}
// Check if sync had errors (even if not marked as hasErrors)
if (syncResult.errors && syncResult.errors.length > 0) {
const errorMessages = syncResult.errors.map((e: any) => e.error || e.message || 'Unknown error').join('; ');
// Check if it's a network/Jira API error
if (errorMessages.includes('Failed to fetch') || errorMessages.includes('fetch failed') || errorMessages.includes('network')) {
throw new Error(`Network error while syncing: ${errorMessages}. This usually means: - Jira host URL is incorrect or unreachable - Network connectivity issues - Jira API authentication failed. Check your Jira PAT token in Profile Settings and verify the JIRA_HOST environment variable.`);
}
throw new Error(`Sync failed: ${errorMessages}. Check backend logs for details.`);
}
if (syncResult.objectsCached === 0 && syncResult.objectsProcessed === 0) {
throw new Error('Sync completed but no objects were processed. This could mean:\n- The object type has no objects in Jira Assets\n- The IQL search returned no results\n- Check if the object type name is correct');
}
// Step 2: Get a sample object to check
updateTestResult(testId, { message: 'Fetching sample objects...' });
const objectsResponse = await fetch(`/api/v2/objects/${inputs.typeName}?limit=10`, {
credentials: 'include',
});
if (!objectsResponse.ok) {
const errorData = await objectsResponse.json().catch(() => ({ error: `HTTP ${objectsResponse.status}: Failed to fetch objects` }));
const errorMessage = errorData.error || `HTTP ${objectsResponse.status}: Failed to fetch objects`;
throw new Error(`${errorMessage}. Sync processed ${syncResult.objectsProcessed || 0} objects. If objects were synced, they may not be queryable yet.`);
}
const objectsData = await objectsResponse.json();
const sampleObject = objectsData.objects?.[0];
if (!sampleObject) {
throw new Error(`No objects found after sync. Sync processed ${syncResult.objectsProcessed || 0} objects, cached ${syncResult.objectsCached || 0}. The objects may not be queryable yet or the object type has no objects in Jira Assets.`);
}
// Step 3: Find a referenced object from the sample
updateTestResult(testId, { message: 'Looking for referenced objects in sample...' });
const referencedObjectKey = inputs.referencedObjectKey || (() => {
// Try to find a reference in the object
const references: string[] = [];
for (const [key, value] of Object.entries(sampleObject)) {
if (value && typeof value === 'object' && ('objectKey' in value || 'key' in value)) {
const objKey = (value as any).objectKey || (value as any).key;
if (objKey) references.push(objKey);
}
if (Array.isArray(value) && value.length > 0) {
for (const item of value) {
if (item && typeof item === 'object' && ('objectKey' in item || 'key' in item)) {
const objKey = item.objectKey || item.key;
if (objKey) references.push(objKey);
}
}
}
}
return references.length > 0 ? references[0] : null;
})();
if (!referencedObjectKey) {
const sampleObjectKeys = Object.keys(sampleObject).join(', ');
throw new Error(`No referenced object found in sample object. Object fields: ${sampleObjectKeys}. Please provide a referencedObjectKey manually if the object has references.`);
}
updateTestResult(testId, { message: `Found referenced object: ${referencedObjectKey}` });
// Step 4: Run SQL checks
updateTestResult(testId, { message: 'Running database checks...' });
const sqlQueries: Array<{ sql: string; result: any }> = [];
// Check 1: Both objects exist
const sampleObjectKey = sampleObject.objectKey || sampleObject.key || sampleObject.id;
if (!sampleObjectKey) {
throw new Error(`Sample object has no objectKey. Object structure: ${JSON.stringify(sampleObject)}`);
}
const check1Sql = `SELECT id, object_key, object_type_name, label FROM objects WHERE object_key IN (?, ?)`;
const check1Result = await executeSqlQuery(check1Sql, [sampleObjectKey, referencedObjectKey]);
sqlQueries.push({ sql: check1Sql, result: check1Result });
// Check 2: Referenced object has attribute values
// Handle both snake_case and camelCase column names
const refObj = check1Result.find((o: any) =>
(o.object_key || o.objectKey) === referencedObjectKey
);
if (!refObj) {
const foundKeys = check1Result.map((o: any) => (o.object_key || o.objectKey)).filter(Boolean);
throw new Error(`Referenced object "${referencedObjectKey}" not found in database. Found objects: ${foundKeys.join(', ') || 'none'}. The referenced object may not have been cached during sync.`);
}
const check2Sql = `SELECT COUNT(*) as count FROM attribute_values WHERE object_id = ?`;
const check2Result = await executeSqlQuery(check2Sql, [refObj.id]);
sqlQueries.push({ sql: check2Sql, result: check2Result });
const attrCount = check2Result[0]?.count || (check2Result[0]?.count === 0 ? 0 : null);
// Check 3: Get sample attribute values to verify they exist
const check3Sql = `SELECT av.*, a.field_name, a.attr_type
FROM attribute_values av
JOIN attributes a ON a.id = av.attribute_id
WHERE av.object_id = ?
LIMIT 5`;
const check3Result = await executeSqlQuery(check3Sql, [refObj.id]);
sqlQueries.push({ sql: check3Sql, result: check3Result });
updateTestResult(testId, {
status: 'success',
message: `✅ Sync completed successfully. Found ${syncResult.objectsCached || 0} objects. Referenced object "${referencedObjectKey}" exists with ${attrCount || 0} attribute values.`,
data: {
syncResult: {
objectsProcessed: syncResult.objectsProcessed,
objectsCached: syncResult.objectsCached,
relationsExtracted: syncResult.relationsExtracted,
},
sampleObject: { objectKey: sampleObjectKey, label: sampleObject.label || sampleObject.name },
referencedObject: {
objectKey: referencedObjectKey,
id: refObj.id,
typeName: refObj.object_type_name || refObj.objectTypeName,
label: refObj.label,
attributeValueCount: attrCount,
},
},
sqlQueries,
});
} catch (error) {
updateTestResult(testId, {
status: 'error',
message: error instanceof Error ? error.message : 'Test failed',
});
} finally {
setLoading(prev => ({ ...prev, [testId]: false }));
}
};
// Test B: Disabled type reference-only caching
const runTestB = async () => {
const testId = 'testB';
setLoading(prev => ({ ...prev, [testId]: true }));
updateTestResult(testId, { test: 'Test B: Disabled Type Reference-Only', status: 'running' });
try {
const disabledType = inputs.disabledTypeName;
// Step 1: Check initial count
updateTestResult(testId, { message: 'Checking initial object count...' });
const initialCountResult = await executeSqlQuery(
`SELECT COUNT(*) as count FROM objects WHERE object_type_name = ?`,
[disabledType]
);
const initialCount = initialCountResult[0]?.count || 0;
// Step 2: Sync an enabled type that references the disabled type
updateTestResult(testId, { message: `Syncing ${inputs.typeName} (should cache ${disabledType} references)...` });
const syncResponse = await fetch(`/api/v2/sync/objects/${inputs.typeName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
});
if (!syncResponse.ok) throw new Error('Sync failed');
const syncResult = await syncResponse.json();
// Step 3: Check final count (should not have increased much)
updateTestResult(testId, { message: 'Checking final object count...' });
const finalCountResult = await executeSqlQuery(
`SELECT COUNT(*) as count FROM objects WHERE object_type_name = ?`,
[disabledType]
);
const finalCount = finalCountResult[0]?.count || 0;
// Step 4: Check relations
const relationsResult = await executeSqlQuery(
`SELECT COUNT(*) as count
FROM object_relations r
JOIN objects t ON t.id = r.target_id
WHERE t.object_type_name = ?`,
[disabledType]
);
const relationCount = relationsResult[0]?.count || 0;
const sqlQueries = [
{ sql: 'Initial count query', result: initialCountResult },
{ sql: 'Final count query', result: finalCountResult },
{ sql: 'Relations query', result: relationsResult },
];
updateTestResult(testId, {
status: 'success',
message: `Disabled type ${disabledType}: ${initialCount}${finalCount} objects. ${relationCount} relations found.`,
data: {
initialCount,
finalCount,
relationCount,
delta: finalCount - initialCount,
},
sqlQueries,
});
} catch (error) {
updateTestResult(testId, {
status: 'error',
message: error instanceof Error ? error.message : 'Test failed',
});
} finally {
setLoading(prev => ({ ...prev, [testId]: false }));
}
};
// Test C: Attribute wipe rule
const runTestC = async () => {
const testId = 'testC';
setLoading(prev => ({ ...prev, [testId]: true }));
updateTestResult(testId, { test: 'Test C: Attribute Wipe Rule', status: 'running' });
try {
if (!inputs.referencedObjectKey) {
throw new Error('Please provide a referencedObjectKey to test');
}
// Step 1: Get initial attribute count
updateTestResult(testId, { message: 'Checking initial attribute values...' });
const objInfoResponse = await fetch(`/api/v2/debug/objects?objectKey=${inputs.referencedObjectKey}`, {
credentials: 'include',
});
if (!objInfoResponse.ok) throw new Error('Failed to get object info');
const objInfo = await objInfoResponse.json();
const initialAttrCount = objInfo.attributeValueCount || 0;
if (initialAttrCount === 0) {
throw new Error('Object has no attributes - sync it first with level2 expansion');
}
// Step 2: Sync a parent object that references this one (should come as shallow)
updateTestResult(testId, { message: 'Syncing parent object (referenced object may come as shallow)...' });
const syncResponse = await fetch(`/api/v2/sync/objects/${inputs.typeName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
});
if (!syncResponse.ok) throw new Error('Sync failed');
// Step 3: Check final attribute count (should be same or greater)
updateTestResult(testId, { message: 'Checking final attribute values...' });
const finalObjInfoResponse = await fetch(`/api/v2/debug/objects?objectKey=${inputs.referencedObjectKey}`, {
credentials: 'include',
});
if (!finalObjInfoResponse.ok) throw new Error('Failed to get final object info');
const finalObjInfo = await finalObjInfoResponse.json();
const finalAttrCount = finalObjInfo.attributeValueCount || 0;
const sqlQueries = [
{ sql: 'Initial attribute count', result: [{ count: initialAttrCount }] },
{ sql: 'Final attribute count', result: [{ count: finalAttrCount }] },
];
const success = finalAttrCount >= initialAttrCount;
updateTestResult(testId, {
status: success ? 'success' : 'error',
message: success
? `✅ Attributes preserved: ${initialAttrCount}${finalAttrCount}`
: `❌ Attributes were wiped: ${initialAttrCount}${finalAttrCount}`,
data: {
initialAttrCount,
finalAttrCount,
preserved: success,
},
sqlQueries,
});
} catch (error) {
updateTestResult(testId, {
status: 'error',
message: error instanceof Error ? error.message : 'Test failed',
});
} finally {
setLoading(prev => ({ ...prev, [testId]: false }));
}
};
// Test D: QueryService reconstruction
const runTestD = async () => {
const testId = 'testD';
setLoading(prev => ({ ...prev, [testId]: true }));
updateTestResult(testId, { test: 'Test D: QueryService Reconstruction', status: 'running' });
try {
if (!inputs.objectKey) {
throw new Error('Please provide an objectKey to test');
}
// Step 1: Get object from DB via QueryService
updateTestResult(testId, { message: 'Fetching object via QueryService...' });
const objectResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, {
credentials: 'include',
});
if (!objectResponse.ok) throw new Error('Failed to fetch object');
const object = await objectResponse.json();
// Step 2: Get raw DB data for comparison
updateTestResult(testId, { message: 'Fetching raw DB data...' });
const objInfoResponse = await fetch(`/api/v2/debug/objects?objectKey=${inputs.objectKey}`, {
credentials: 'include',
});
if (!objInfoResponse.ok) throw new Error('Failed to get object info');
const objInfo = await objInfoResponse.json();
// Step 3: Get attribute values from DB
const attrValuesResult = await executeSqlQuery(
`SELECT av.*, a.field_name, a.attr_type, a.is_multiple
FROM attribute_values av
JOIN attributes a ON a.id = av.attribute_id
WHERE av.object_id = ?
ORDER BY a.field_name, av.array_index`,
[objInfo.object.id]
);
// Step 4: Verify reconstruction
const fieldCount = new Set(attrValuesResult.map((av: any) => av.field_name)).size;
const objectFieldCount = Object.keys(object).filter(k => !k.startsWith('_') && k !== 'id' && k !== 'objectKey' && k !== 'label').length;
const sqlQueries = [
{ sql: 'Attribute values query', result: attrValuesResult },
];
updateTestResult(testId, {
status: 'success',
message: `Object reconstructed: ${fieldCount} fields from DB, ${objectFieldCount} fields in object`,
data: {
object,
dbFields: fieldCount,
objectFields: objectFieldCount,
attributeValues: attrValuesResult.length,
},
sqlQueries,
});
} catch (error) {
updateTestResult(testId, {
status: 'error',
message: error instanceof Error ? error.message : 'Test failed',
});
} finally {
setLoading(prev => ({ ...prev, [testId]: false }));
}
};
// Test E: Write-through
const runTestE = async () => {
const testId = 'testE';
setLoading(prev => ({ ...prev, [testId]: true }));
updateTestResult(testId, { test: 'Test E: Write-Through Update', status: 'running' });
try {
if (!inputs.objectKey) {
throw new Error('Please provide an objectKey to test');
}
// Step 1: Get initial object state
updateTestResult(testId, { message: 'Fetching initial object state...' });
const initialResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, {
credentials: 'include',
});
if (!initialResponse.ok) throw new Error('Failed to fetch initial object');
const initialObject = await initialResponse.json();
// Step 2: Update via write-through
updateTestResult(testId, { message: 'Updating object via write-through...' });
const updatePayload: Record<string, any> = {};
updatePayload[inputs.updateField] = inputs.updateValue;
const updateResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(updatePayload),
});
if (!updateResponse.ok) {
const error = await updateResponse.json();
throw new Error(error.error || 'Update failed');
}
const updateResult = await updateResponse.json();
// Step 3: Immediately check DB (without refresh)
updateTestResult(testId, { message: 'Checking DB state immediately...' });
const dbCheckResponse = await fetch(`/api/v2/objects/${inputs.typeName}/${inputs.objectKey}`, {
credentials: 'include',
});
if (!dbCheckResponse.ok) throw new Error('Failed to fetch updated object');
const dbObject = await dbCheckResponse.json();
// Step 4: Check attribute value in DB
const attrCheckResult = await executeSqlQuery(
`SELECT av.*, a.field_name
FROM attribute_values av
JOIN attributes a ON a.id = av.attribute_id
JOIN objects o ON o.id = av.object_id
WHERE o.object_key = ? AND a.field_name = ?`,
[inputs.objectKey, inputs.updateField]
);
const sqlQueries = [
{ sql: 'Attribute value check', result: attrCheckResult },
];
const dbValue = dbObject[inputs.updateField];
const updated = dbValue === inputs.updateValue || (typeof dbValue === 'object' && dbValue?.label === inputs.updateValue);
updateTestResult(testId, {
status: updated ? 'success' : 'error',
message: updated
? `✅ Write-through successful: DB updated immediately`
: `❌ Write-through failed: DB value doesn't match`,
data: {
initialValue: initialObject[inputs.updateField],
expectedValue: inputs.updateValue,
dbValue,
updated,
},
sqlQueries,
});
} catch (error) {
updateTestResult(testId, {
status: 'error',
message: error instanceof Error ? error.message : 'Test failed',
});
} finally {
setLoading(prev => ({ ...prev, [testId]: false }));
}
};
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<PageHeader
title="Architecture Debug & Test Page"
description="Test the refactored V2 API architecture. All tests require admin permissions."
icon={
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
}
/>
{/* Input Form */}
<div className="bg-white shadow rounded-lg p-6 mb-8">
<h2 className="text-xl font-semibold mb-4">Test Parameters</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Object Type Name
</label>
<div className="flex gap-2">
<input
type="text"
value={inputs.typeName}
onChange={(e) => setInputs({ ...inputs, typeName: e.target.value })}
className="flex-1 border border-gray-300 rounded-md px-3 py-2"
placeholder="ApplicationComponent"
list="enabled-types-list"
/>
<datalist id="enabled-types-list">
{enabledTypes.map(type => (
<option key={type} value={type} />
))}
</datalist>
</div>
{!loadingEnabledTypes && enabledTypes.length > 0 && (
<p className="mt-1 text-xs text-gray-500">
Enabled types: {enabledTypes.join(', ')}
{!enabledTypes.includes(inputs.typeName) && inputs.typeName && (
<span className="ml-2 text-orange-600">
"{inputs.typeName}" is not enabled
</span>
)}
</p>
)}
{!loadingEnabledTypes && enabledTypes.length === 0 && (
<p className="mt-1 text-xs text-orange-600">
No object types are currently enabled. Please enable at least one in Schema Configuration.
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Object Key (for Tests C, D, E)
</label>
<input
type="text"
value={inputs.objectKey}
onChange={(e) => setInputs({ ...inputs, objectKey: e.target.value })}
className="w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="APP-123"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Referenced Object Key (for Tests A, C)
</label>
<input
type="text"
value={inputs.referencedObjectKey}
onChange={(e) => setInputs({ ...inputs, referencedObjectKey: e.target.value })}
className="w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="HOST-123"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Disabled Type Name (for Test B)
</label>
<input
type="text"
value={inputs.disabledTypeName}
onChange={(e) => setInputs({ ...inputs, disabledTypeName: e.target.value })}
className="w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="HostingType"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Update Field (for Test E)
</label>
<input
type="text"
value={inputs.updateField}
onChange={(e) => setInputs({ ...inputs, updateField: e.target.value })}
className="w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="description"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Update Value (for Test E)
</label>
<input
type="text"
value={inputs.updateValue}
onChange={(e) => setInputs({ ...inputs, updateValue: e.target.value })}
className="w-full border border-gray-300 rounded-md px-3 py-2"
placeholder="Updated value"
/>
</div>
</div>
</div>
{/* Utility Actions */}
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-8">
<h2 className="text-lg font-semibold mb-2 text-yellow-900">Utility Actions</h2>
<div className="flex gap-2">
<button
onClick={async () => {
try {
const response = await fetch('/api/v2/debug/fix-missing-type-names', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
});
const result = await response.json();
if (result.success) {
alert(`Fixed ${result.fixed} object types with missing type_name. Errors: ${result.errors || 0}`);
// Reload enabled types
const config = await getConfiguredObjectTypes();
const enabled = config.schemas.flatMap(s =>
s.objectTypes.filter(ot => ot.enabled).map(ot => ot.objectTypeName)
);
setEnabledTypes(enabled);
} else {
alert(`Error: ${result.error || 'Failed to fix'}`);
}
} catch (error) {
alert(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}}
className="bg-yellow-600 text-white px-4 py-2 rounded-md hover:bg-yellow-700"
>
Fix Missing type_name
</button>
<button
onClick={async () => {
const testId = 'debug-all-types';
setLoading(prev => ({ ...prev, [testId]: true }));
updateTestResult(testId, { test: 'Debug: All Object Types', status: 'running', message: 'Fetching all object types...' });
try {
const response = await fetch('/api/v2/debug/all-object-types', {
credentials: 'include',
});
if (!response.ok) {
// Check if it's a 404 - likely means V2 API is not enabled
if (response.status === 404) {
throw new Error('V2 API routes are not enabled. Please set USE_V2_API=true in backend environment variables and restart the server.');
}
const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}: Failed to fetch` }));
const errorMessage = errorData.error || errorData.details || `HTTP ${response.status}: Failed to fetch`;
// Provide more context for common errors
if (response.status === 403) {
throw new Error(`${errorMessage} (Admin permission required)`);
}
throw new Error(errorMessage);
}
const result = await response.json();
// Format the data for display
const enabledWithNullTypeName = result.allTypes.filter((t: any) => t.enabled && !t.hasTypeName);
const enabledWithTypeName = result.allTypes.filter((t: any) => t.enabled && t.hasTypeName);
updateTestResult(testId, {
test: 'Debug: All Object Types',
status: 'success',
message: `Found ${result.summary.total} total types. ${result.summary.enabled} enabled in DB (${result.summary.enabledWithTypeName} with type_name, ${enabledWithNullTypeName.length} missing type_name). ${result.summary.missingTypeName} total missing type_name.`,
data: {
summary: result.summary,
enabledWithNullTypeName: enabledWithNullTypeName.map((t: any) => ({
id: t.id,
displayName: t.displayName,
enabled: t.enabled,
})),
enabledWithTypeName: enabledWithTypeName.map((t: any) => ({
id: t.id,
typeName: t.typeName,
displayName: t.displayName,
})),
allTypes: result.allTypes,
enabledTypesFromService: result.enabledTypes,
},
});
} catch (error) {
updateTestResult(testId, {
test: 'Debug: All Object Types',
status: 'error',
message: error instanceof Error ? error.message : 'Failed to fetch',
});
} finally {
setLoading(prev => ({ ...prev, [testId]: false }));
}
}}
disabled={loading.debugAllTypes}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading.debugAllTypes ? 'Loading...' : 'Show All Object Types (Debug)'}
</button>
</div>
</div>
{/* Test Buttons */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<button
onClick={runTestA}
disabled={loading.testA}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading.testA ? 'Running...' : 'Test A: Level2 Recursion'}
</button>
<button
onClick={runTestB}
disabled={loading.testB}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading.testB ? 'Running...' : 'Test B: Disabled Type'}
</button>
<button
onClick={runTestC}
disabled={loading.testC}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading.testC ? 'Running...' : 'Test C: Attribute Wipe Rule'}
</button>
<button
onClick={runTestD}
disabled={loading.testD}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading.testD ? 'Running...' : 'Test D: QueryService Reconstruction'}
</button>
<button
onClick={runTestE}
disabled={loading.testE}
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
>
{loading.testE ? 'Running...' : 'Test E: Write-Through'}
</button>
</div>
{/* Test Results */}
<div className="space-y-6">
{Object.entries(testResults).map(([testId, result]) => (
<div key={testId} className="bg-white shadow rounded-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">{result.test}</h3>
<span className={`px-3 py-1 rounded-full text-sm ${
result.status === 'success' ? 'bg-green-100 text-green-800' :
result.status === 'error' ? 'bg-red-100 text-red-800' :
result.status === 'running' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{result.status}
</span>
</div>
{result.message && (
<p className="text-gray-700 mb-4">{result.message}</p>
)}
{result.data && (
<div className="mb-4">
<h4 className="font-medium mb-2">Data:</h4>
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-sm">
{JSON.stringify(result.data, null, 2)}
</pre>
</div>
)}
{result.sqlQueries && result.sqlQueries.length > 0 && (
<div className="mb-4">
<h4 className="font-medium mb-2">SQL Queries & Results:</h4>
{result.sqlQueries.map((query, idx) => (
<div key={idx} className="mb-4 border-l-4 border-blue-500 pl-4">
<div className="font-mono text-sm bg-gray-50 p-2 rounded mb-2">
{query.sql}
</div>
<pre className="bg-gray-50 p-4 rounded-md overflow-auto text-xs max-h-64">
{JSON.stringify(query.result, null, 2)}
</pre>
</div>
))}
</div>
)}
{result.timestamp && (
<p className="text-xs text-gray-500">Ran at: {new Date(result.timestamp).toLocaleString()}</p>
)}
</div>
))}
</div>
{Object.keys(testResults).length === 0 && (
<div className="bg-gray-50 rounded-lg p-8 text-center text-gray-500">
No tests run yet. Use the buttons above to start testing.
</div>
)}
</div>
);
}