Spaces:
Paused
Paused
Upload 14 files
Browse files- app/static/dev/api-test.html +274 -0
- app/static/dev/comprehensive-test.html +767 -0
- app/static/dev/functional-test.html +885 -0
- app/static/dev/integration-test.html +385 -0
- app/static/dev/real-api-test.html +674 -0
- app/static/dev/test_integration.html +164 -0
- app/static/js/api-client.js +422 -0
- app/static/js/api-connection-test.js +380 -0
- app/static/js/chart.js +340 -0
- app/static/js/core.js +388 -0
- app/static/js/document-crud.js +386 -0
- app/static/js/file-upload-handler.js +328 -0
- app/static/js/notifications.js +617 -0
- app/static/js/scraping-control.js +368 -0
app/static/dev/api-test.html
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>API Connection Test - Legal Dashboard</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Arial', sans-serif;
|
| 10 |
+
max-width: 1200px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.test-section {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 20px;
|
| 18 |
+
margin: 20px 0;
|
| 19 |
+
border-radius: 8px;
|
| 20 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 21 |
+
}
|
| 22 |
+
.success { color: #10b981; }
|
| 23 |
+
.error { color: #ef4444; }
|
| 24 |
+
.info { color: #3b82f6; }
|
| 25 |
+
.warning { color: #f59e0b; }
|
| 26 |
+
button {
|
| 27 |
+
background: #007bff;
|
| 28 |
+
color: white;
|
| 29 |
+
border: none;
|
| 30 |
+
padding: 10px 20px;
|
| 31 |
+
border-radius: 4px;
|
| 32 |
+
cursor: pointer;
|
| 33 |
+
margin: 5px;
|
| 34 |
+
}
|
| 35 |
+
button:hover {
|
| 36 |
+
background: #0056b3;
|
| 37 |
+
}
|
| 38 |
+
pre {
|
| 39 |
+
background: #f8f9fa;
|
| 40 |
+
padding: 10px;
|
| 41 |
+
border-radius: 4px;
|
| 42 |
+
overflow-x: auto;
|
| 43 |
+
max-height: 300px;
|
| 44 |
+
overflow-y: auto;
|
| 45 |
+
}
|
| 46 |
+
.endpoint-grid {
|
| 47 |
+
display: grid;
|
| 48 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 49 |
+
gap: 15px;
|
| 50 |
+
margin-top: 20px;
|
| 51 |
+
}
|
| 52 |
+
.endpoint-card {
|
| 53 |
+
border: 1px solid #ddd;
|
| 54 |
+
border-radius: 8px;
|
| 55 |
+
padding: 15px;
|
| 56 |
+
background: white;
|
| 57 |
+
}
|
| 58 |
+
.endpoint-card.success {
|
| 59 |
+
border-color: #10b981;
|
| 60 |
+
background: #f0fdf4;
|
| 61 |
+
}
|
| 62 |
+
.endpoint-card.error {
|
| 63 |
+
border-color: #ef4444;
|
| 64 |
+
background: #fef2f2;
|
| 65 |
+
}
|
| 66 |
+
.endpoint-card.warning {
|
| 67 |
+
border-color: #f59e0b;
|
| 68 |
+
background: #fffbeb;
|
| 69 |
+
}
|
| 70 |
+
.status-indicator {
|
| 71 |
+
display: inline-block;
|
| 72 |
+
width: 12px;
|
| 73 |
+
height: 12px;
|
| 74 |
+
border-radius: 50%;
|
| 75 |
+
margin-right: 8px;
|
| 76 |
+
}
|
| 77 |
+
.status-indicator.success { background: #10b981; }
|
| 78 |
+
.status-indicator.error { background: #ef4444; }
|
| 79 |
+
.status-indicator.warning { background: #f59e0b; }
|
| 80 |
+
.summary-stats {
|
| 81 |
+
display: grid;
|
| 82 |
+
grid-template-columns: repeat(4, 1fr);
|
| 83 |
+
gap: 15px;
|
| 84 |
+
margin-bottom: 20px;
|
| 85 |
+
}
|
| 86 |
+
.stat-card {
|
| 87 |
+
background: white;
|
| 88 |
+
padding: 15px;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
text-align: center;
|
| 91 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 92 |
+
}
|
| 93 |
+
.stat-number {
|
| 94 |
+
font-size: 2rem;
|
| 95 |
+
font-weight: bold;
|
| 96 |
+
margin-bottom: 5px;
|
| 97 |
+
}
|
| 98 |
+
.stat-label {
|
| 99 |
+
color: #666;
|
| 100 |
+
font-size: 0.9rem;
|
| 101 |
+
}
|
| 102 |
+
</style>
|
| 103 |
+
</head>
|
| 104 |
+
<body>
|
| 105 |
+
<h1>🔧 API Connection Test - Legal Dashboard</h1>
|
| 106 |
+
|
| 107 |
+
<div class="test-section">
|
| 108 |
+
<h2>📊 Test Summary</h2>
|
| 109 |
+
<div class="summary-stats" id="summaryStats">
|
| 110 |
+
<div class="stat-card">
|
| 111 |
+
<div class="stat-number" id="totalTests">0</div>
|
| 112 |
+
<div class="stat-label">Total Tests</div>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="stat-card">
|
| 115 |
+
<div class="stat-number success" id="passedTests">0</div>
|
| 116 |
+
<div class="stat-label">Passed</div>
|
| 117 |
+
</div>
|
| 118 |
+
<div class="stat-card">
|
| 119 |
+
<div class="stat-number error" id="failedTests">0</div>
|
| 120 |
+
<div class="stat-label">Failed</div>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="stat-card">
|
| 123 |
+
<div class="stat-number info" id="successRate">0%</div>
|
| 124 |
+
<div class="stat-label">Success Rate</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<button type="button" onclick="runAllTests()">Run All API Tests</button>
|
| 129 |
+
<button type="button" onclick="testEndpointPatterns()">Test Endpoint Patterns</button>
|
| 130 |
+
<button type="button" onclick="clearResults()">Clear Results</button>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<div class="test-section">
|
| 134 |
+
<h2>🔍 Endpoint Test Results</h2>
|
| 135 |
+
<div class="endpoint-grid" id="endpointResults">
|
| 136 |
+
<!-- Results will be populated here -->
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div class="test-section">
|
| 141 |
+
<h2>📋 Detailed Results</h2>
|
| 142 |
+
<div id="detailedResults">
|
| 143 |
+
<p class="info">Click "Run All API Tests" to start testing...</p>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<script src="js/api-connection-test.js"></script>
|
| 148 |
+
<script>
|
| 149 |
+
let testResults = [];
|
| 150 |
+
|
| 151 |
+
async function runAllTests() {
|
| 152 |
+
console.log('Starting comprehensive API tests...');
|
| 153 |
+
|
| 154 |
+
// Clear previous results
|
| 155 |
+
document.getElementById('endpointResults').innerHTML = '';
|
| 156 |
+
document.getElementById('detailedResults').innerHTML = '<p class="info">Running tests...</p>';
|
| 157 |
+
|
| 158 |
+
// Run the API tests
|
| 159 |
+
const results = await window.apiTester.runAllTests();
|
| 160 |
+
testResults = results;
|
| 161 |
+
|
| 162 |
+
// Update summary
|
| 163 |
+
updateSummary(results);
|
| 164 |
+
|
| 165 |
+
// Display detailed results
|
| 166 |
+
displayDetailedResults(results);
|
| 167 |
+
|
| 168 |
+
console.log('API tests completed');
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
async function testEndpointPatterns() {
|
| 172 |
+
console.log('Testing endpoint patterns...');
|
| 173 |
+
await window.apiTester.testEndpointPatterns();
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
function clearResults() {
|
| 177 |
+
document.getElementById('endpointResults').innerHTML = '';
|
| 178 |
+
document.getElementById('detailedResults').innerHTML = '<p class="info">Results cleared</p>';
|
| 179 |
+
updateSummary([]);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
function updateSummary(results) {
|
| 183 |
+
const total = results.length;
|
| 184 |
+
const passed = results.filter(r => r.success).length;
|
| 185 |
+
const failed = total - passed;
|
| 186 |
+
const successRate = total > 0 ? ((passed / total) * 100).toFixed(1) : 0;
|
| 187 |
+
|
| 188 |
+
document.getElementById('totalTests').textContent = total;
|
| 189 |
+
document.getElementById('passedTests').textContent = passed;
|
| 190 |
+
document.getElementById('failedTests').textContent = failed;
|
| 191 |
+
document.getElementById('successRate').textContent = successRate + '%';
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
function displayDetailedResults(results) {
|
| 195 |
+
const container = document.getElementById('endpointResults');
|
| 196 |
+
const detailedContainer = document.getElementById('detailedResults');
|
| 197 |
+
|
| 198 |
+
// Clear containers
|
| 199 |
+
container.innerHTML = '';
|
| 200 |
+
detailedContainer.innerHTML = '';
|
| 201 |
+
|
| 202 |
+
// Group results by category
|
| 203 |
+
const categories = {};
|
| 204 |
+
results.forEach(result => {
|
| 205 |
+
if (!categories[result.category]) {
|
| 206 |
+
categories[result.category] = [];
|
| 207 |
+
}
|
| 208 |
+
categories[result.category].push(result);
|
| 209 |
+
});
|
| 210 |
+
|
| 211 |
+
// Create endpoint cards
|
| 212 |
+
results.forEach(result => {
|
| 213 |
+
const card = document.createElement('div');
|
| 214 |
+
card.className = `endpoint-card ${result.success ? 'success' : 'error'}`;
|
| 215 |
+
|
| 216 |
+
const statusClass = result.success ? 'success' : 'error';
|
| 217 |
+
const statusText = result.success ? 'PASS' : 'FAIL';
|
| 218 |
+
|
| 219 |
+
card.innerHTML = `
|
| 220 |
+
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
| 221 |
+
<span class="status-indicator ${statusClass}"></span>
|
| 222 |
+
<strong>${result.name}</strong>
|
| 223 |
+
<span style="margin-left: auto; font-size: 0.8rem; color: #666;">
|
| 224 |
+
${result.responseTime}ms
|
| 225 |
+
</span>
|
| 226 |
+
</div>
|
| 227 |
+
<div style="font-size: 0.9rem; color: #666;">
|
| 228 |
+
<div>URL: ${result.url}</div>
|
| 229 |
+
<div>Method: ${result.method}</div>
|
| 230 |
+
<div>Status: ${result.status}</div>
|
| 231 |
+
${result.error ? `<div style="color: #ef4444;">Error: ${result.error}</div>` : ''}
|
| 232 |
+
</div>
|
| 233 |
+
`;
|
| 234 |
+
|
| 235 |
+
container.appendChild(card);
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
// Create detailed results
|
| 239 |
+
let detailedHTML = '<h3>Test Results by Category</h3>';
|
| 240 |
+
|
| 241 |
+
Object.entries(categories).forEach(([category, categoryResults]) => {
|
| 242 |
+
const passed = categoryResults.filter(r => r.success).length;
|
| 243 |
+
const total = categoryResults.length;
|
| 244 |
+
const rate = ((passed / total) * 100).toFixed(1);
|
| 245 |
+
|
| 246 |
+
detailedHTML += `
|
| 247 |
+
<div style="margin-bottom: 20px;">
|
| 248 |
+
<h4>${category} (${passed}/${total} - ${rate}%)</h4>
|
| 249 |
+
<ul>
|
| 250 |
+
${categoryResults.map(result => `
|
| 251 |
+
<li class="${result.success ? 'success' : 'error'}">
|
| 252 |
+
${result.name}: ${result.success ? 'PASS' : 'FAIL'}
|
| 253 |
+
(${result.responseTime}ms)
|
| 254 |
+
${result.error ? ` - ${result.error}` : ''}
|
| 255 |
+
</li>
|
| 256 |
+
`).join('')}
|
| 257 |
+
</ul>
|
| 258 |
+
</div>
|
| 259 |
+
`;
|
| 260 |
+
});
|
| 261 |
+
|
| 262 |
+
detailedContainer.innerHTML = detailedHTML;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
// Auto-run tests when page loads
|
| 266 |
+
window.addEventListener('load', () => {
|
| 267 |
+
setTimeout(() => {
|
| 268 |
+
console.log('Auto-running API tests...');
|
| 269 |
+
runAllTests();
|
| 270 |
+
}, 1000);
|
| 271 |
+
});
|
| 272 |
+
</script>
|
| 273 |
+
</body>
|
| 274 |
+
</html>
|
app/static/dev/comprehensive-test.html
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Comprehensive Frontend Test - Legal Dashboard</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Arial', sans-serif;
|
| 10 |
+
max-inline-size: 1400px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.test-section {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 20px;
|
| 18 |
+
margin: 20px 0;
|
| 19 |
+
border-radius: 8px;
|
| 20 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 21 |
+
}
|
| 22 |
+
.success { color: #10b981; }
|
| 23 |
+
.error { color: #ef4444; }
|
| 24 |
+
.info { color: #3b82f6; }
|
| 25 |
+
.warning { color: #f59e0b; }
|
| 26 |
+
button {
|
| 27 |
+
background: #007bff;
|
| 28 |
+
color: white;
|
| 29 |
+
border: none;
|
| 30 |
+
padding: 10px 20px;
|
| 31 |
+
border-radius: 4px;
|
| 32 |
+
cursor: pointer;
|
| 33 |
+
margin: 5px;
|
| 34 |
+
}
|
| 35 |
+
button:hover {
|
| 36 |
+
background: #0056b3;
|
| 37 |
+
}
|
| 38 |
+
button:disabled {
|
| 39 |
+
background: #ccc;
|
| 40 |
+
cursor: not-allowed;
|
| 41 |
+
}
|
| 42 |
+
.page-test {
|
| 43 |
+
border: 1px solid #ddd;
|
| 44 |
+
border-radius: 8px;
|
| 45 |
+
padding: 15px;
|
| 46 |
+
margin: 10px 0;
|
| 47 |
+
background: white;
|
| 48 |
+
}
|
| 49 |
+
.page-test.success {
|
| 50 |
+
border-color: #10b981;
|
| 51 |
+
background: #f0fdf4;
|
| 52 |
+
}
|
| 53 |
+
.page-test.error {
|
| 54 |
+
border-color: #ef4444;
|
| 55 |
+
background: #fef2f2;
|
| 56 |
+
}
|
| 57 |
+
.page-test.testing {
|
| 58 |
+
border-color: #3b82f6;
|
| 59 |
+
background: #eff6ff;
|
| 60 |
+
}
|
| 61 |
+
.status-indicator {
|
| 62 |
+
display: inline-block;
|
| 63 |
+
inline-size: 12px;
|
| 64 |
+
block-size: 12px;
|
| 65 |
+
border-radius: 50%;
|
| 66 |
+
margin-inline-end: 8px;
|
| 67 |
+
}
|
| 68 |
+
.status-indicator.success { background: #10b981; }
|
| 69 |
+
.status-indicator.error { background: #ef4444; }
|
| 70 |
+
.status-indicator.warning { background: #f59e0b; }
|
| 71 |
+
.status-indicator.info { background: #3b82f6; }
|
| 72 |
+
.status-indicator.testing {
|
| 73 |
+
background: #3b82f6;
|
| 74 |
+
animation: pulse 1s infinite;
|
| 75 |
+
}
|
| 76 |
+
@keyframes pulse {
|
| 77 |
+
0% { opacity: 1; }
|
| 78 |
+
50% { opacity: 0.5; }
|
| 79 |
+
100% { opacity: 1; }
|
| 80 |
+
}
|
| 81 |
+
.test-results {
|
| 82 |
+
max-block-size: 400px;
|
| 83 |
+
overflow-y: auto;
|
| 84 |
+
border: 1px solid #ddd;
|
| 85 |
+
border-radius: 4px;
|
| 86 |
+
padding: 10px;
|
| 87 |
+
background: #f8f9fa;
|
| 88 |
+
font-family: 'Courier New', monospace;
|
| 89 |
+
font-size: 12px;
|
| 90 |
+
}
|
| 91 |
+
.summary-stats {
|
| 92 |
+
display: grid;
|
| 93 |
+
grid-template-columns: repeat(4, 1fr);
|
| 94 |
+
gap: 15px;
|
| 95 |
+
margin-block-end: 20px;
|
| 96 |
+
}
|
| 97 |
+
.stat-card {
|
| 98 |
+
background: white;
|
| 99 |
+
padding: 15px;
|
| 100 |
+
border-radius: 8px;
|
| 101 |
+
text-align: center;
|
| 102 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 103 |
+
}
|
| 104 |
+
.stat-number {
|
| 105 |
+
font-size: 2rem;
|
| 106 |
+
font-weight: bold;
|
| 107 |
+
margin-block-end: 5px;
|
| 108 |
+
}
|
| 109 |
+
.stat-label {
|
| 110 |
+
color: #666;
|
| 111 |
+
font-size: 0.9rem;
|
| 112 |
+
}
|
| 113 |
+
.progress-bar {
|
| 114 |
+
inline-size: 100%;
|
| 115 |
+
block-size: 4px;
|
| 116 |
+
background: #e5e7eb;
|
| 117 |
+
border-radius: 2px;
|
| 118 |
+
overflow: hidden;
|
| 119 |
+
margin: 10px 0;
|
| 120 |
+
}
|
| 121 |
+
.progress-fill {
|
| 122 |
+
block-size: 100%;
|
| 123 |
+
background: #3b82f6;
|
| 124 |
+
transition: width 0.3s ease;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.progress-fill.initial {
|
| 128 |
+
inline-size: 0%;
|
| 129 |
+
}
|
| 130 |
+
</style>
|
| 131 |
+
</head>
|
| 132 |
+
<body>
|
| 133 |
+
<h1>🔍 Comprehensive Frontend Test - Legal Dashboard</h1>
|
| 134 |
+
<div class="test-section">
|
| 135 |
+
<h2>📊 Test Summary</h2>
|
| 136 |
+
<div class="summary-stats">
|
| 137 |
+
<div class="stat-card">
|
| 138 |
+
<div class="stat-number" id="totalPages">0</div>
|
| 139 |
+
<div class="stat-label">Total Pages</div>
|
| 140 |
+
</div>
|
| 141 |
+
<div class="stat-card">
|
| 142 |
+
<div class="stat-number" id="passedPages">0</div>
|
| 143 |
+
<div class="stat-label">Passed</div>
|
| 144 |
+
</div>
|
| 145 |
+
<div class="stat-card">
|
| 146 |
+
<div class="stat-number" id="failedPages">0</div>
|
| 147 |
+
<div class="stat-label">Failed</div>
|
| 148 |
+
</div>
|
| 149 |
+
<div class="stat-card">
|
| 150 |
+
<div class="stat-number" id="successRate">0%</div>
|
| 151 |
+
<div class="stat-label">Success Rate</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
<div class="progress-bar">
|
| 155 |
+
<div class="progress-fill initial" id="progressBar"></div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
<div class="test-section">
|
| 160 |
+
<h2>🎛️ Test Controls</h2>
|
| 161 |
+
<button type="button" onclick="runAllTests()" id="runAllBtn">Run All Tests</button>
|
| 162 |
+
<button type="button" onclick="testCoreSystem()">Test Core System</button>
|
| 163 |
+
<button type="button" onclick="testAPIConnectivity()">Test API Connectivity</button>
|
| 164 |
+
<button type="button" onclick="testPageIntegration()">Test Page Integration</button>
|
| 165 |
+
<button type="button" onclick="clearResults()">Clear Results</button>
|
| 166 |
+
<button type="button" onclick="exportResults()">Export Results</button>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div class="test-section">
|
| 170 |
+
<h2>📄 Page Tests</h2>
|
| 171 |
+
<div id="pageTests">
|
| 172 |
+
<!-- Page tests will be generated here -->
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<div class="test-section">
|
| 177 |
+
<h2>📋 Test Results</h2>
|
| 178 |
+
<div class="test-results" id="testResults">
|
| 179 |
+
<!-- Test results will be displayed here -->
|
| 180 |
+
</div>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<script src="../js/api-client.js"></script>
|
| 184 |
+
<script src="../js/core.js"></script>
|
| 185 |
+
<script src="../js/notifications.js"></script>
|
| 186 |
+
<script>
|
| 187 |
+
class ComprehensiveTester {
|
| 188 |
+
constructor() {
|
| 189 |
+
this.baseURL = window.location.origin;
|
| 190 |
+
this.results = [];
|
| 191 |
+
this.testStats = {
|
| 192 |
+
total: 0,
|
| 193 |
+
passed: 0,
|
| 194 |
+
failed: 0,
|
| 195 |
+
successRate: 0
|
| 196 |
+
};
|
| 197 |
+
this.isRunning = false;
|
| 198 |
+
|
| 199 |
+
this.pages = [
|
| 200 |
+
{
|
| 201 |
+
name: 'Main Dashboard',
|
| 202 |
+
url: 'improved_legal_dashboard.html',
|
| 203 |
+
description: 'Main dashboard with analytics and charts',
|
| 204 |
+
tests: ['load', 'api', 'core', 'charts']
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
name: 'Documents Page',
|
| 208 |
+
url: 'documents.html',
|
| 209 |
+
description: 'Document management and CRUD operations',
|
| 210 |
+
tests: ['load', 'api', 'core', 'crud']
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
name: 'Upload Page',
|
| 214 |
+
url: 'upload.html',
|
| 215 |
+
description: 'File upload and OCR processing',
|
| 216 |
+
tests: ['load', 'api', 'core', 'upload']
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
name: 'Scraping Page',
|
| 220 |
+
url: 'scraping.html',
|
| 221 |
+
description: 'Web scraping and content extraction',
|
| 222 |
+
tests: ['load', 'api', 'core', 'scraping']
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
name: 'Scraping Dashboard',
|
| 226 |
+
url: 'scraping_dashboard.html',
|
| 227 |
+
description: 'Scraping statistics and monitoring',
|
| 228 |
+
tests: ['load', 'api', 'core', 'stats']
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
name: 'Reports Page',
|
| 232 |
+
url: 'reports.html',
|
| 233 |
+
description: 'Analytics reports and insights',
|
| 234 |
+
tests: ['load', 'api', 'core', 'reports']
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
name: 'Index Page',
|
| 238 |
+
url: 'index.html',
|
| 239 |
+
description: 'Landing page and navigation',
|
| 240 |
+
tests: ['load', 'api', 'core', 'navigation']
|
| 241 |
+
}
|
| 242 |
+
];
|
| 243 |
+
|
| 244 |
+
this.initialize();
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
initialize() {
|
| 248 |
+
this.createPageTests();
|
| 249 |
+
this.updateStats();
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
createPageTests() {
|
| 253 |
+
const container = document.getElementById('pageTests');
|
| 254 |
+
container.innerHTML = '';
|
| 255 |
+
|
| 256 |
+
this.pages.forEach((page, index) => {
|
| 257 |
+
const testDiv = document.createElement('div');
|
| 258 |
+
testDiv.className = 'page-test';
|
| 259 |
+
testDiv.id = `page-${index}`;
|
| 260 |
+
|
| 261 |
+
testDiv.innerHTML = `
|
| 262 |
+
<div class="status-indicator"></div>
|
| 263 |
+
<h3>${page.name}</h3>
|
| 264 |
+
<p>${page.description}</p>
|
| 265 |
+
<div style="font-size: 0.8rem; color: #666; margin: 5px 0;">
|
| 266 |
+
File: ${page.url}
|
| 267 |
+
</div>
|
| 268 |
+
<div class="tests" id="tests-${index}">
|
| 269 |
+
${page.tests.map((test, testIndex) => `
|
| 270 |
+
<div class="test" id="test-${index}-${testIndex}">
|
| 271 |
+
<span class="status-indicator"></span>
|
| 272 |
+
${test.charAt(0).toUpperCase() + test.slice(1)} Test
|
| 273 |
+
</div>
|
| 274 |
+
`).join('')}
|
| 275 |
+
</div>
|
| 276 |
+
<button type="button" onclick="tester.testSinglePage(${index})" class="test-page-btn">
|
| 277 |
+
Test Page
|
| 278 |
+
</button>
|
| 279 |
+
`;
|
| 280 |
+
|
| 281 |
+
container.appendChild(testDiv);
|
| 282 |
+
});
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
async testSinglePage(pageIndex) {
|
| 286 |
+
const page = this.pages[pageIndex];
|
| 287 |
+
const testDiv = document.getElementById(`page-${pageIndex}`);
|
| 288 |
+
|
| 289 |
+
// Set testing state
|
| 290 |
+
testDiv.className = 'page-test testing';
|
| 291 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
|
| 292 |
+
testDiv.querySelector('.test-page-btn').disabled = true;
|
| 293 |
+
|
| 294 |
+
this.logResult({
|
| 295 |
+
page: page.name,
|
| 296 |
+
status: 'started',
|
| 297 |
+
message: `Starting tests for ${page.name}`
|
| 298 |
+
});
|
| 299 |
+
|
| 300 |
+
let allTestsPassed = true;
|
| 301 |
+
|
| 302 |
+
for (let testIndex = 0; testIndex < page.tests.length; testIndex++) {
|
| 303 |
+
const test = page.tests[testIndex];
|
| 304 |
+
const testDiv = document.getElementById(`test-${pageIndex}-${testIndex}`);
|
| 305 |
+
|
| 306 |
+
// Set test testing state
|
| 307 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
|
| 308 |
+
|
| 309 |
+
try {
|
| 310 |
+
const result = await this.executeTest(test, page);
|
| 311 |
+
|
| 312 |
+
if (result.success) {
|
| 313 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator success';
|
| 314 |
+
this.logResult({
|
| 315 |
+
page: page.name,
|
| 316 |
+
test: test,
|
| 317 |
+
status: 'success',
|
| 318 |
+
message: `${test} test passed for ${page.name}`
|
| 319 |
+
});
|
| 320 |
+
} else {
|
| 321 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator error';
|
| 322 |
+
allTestsPassed = false;
|
| 323 |
+
this.logResult({
|
| 324 |
+
page: page.name,
|
| 325 |
+
test: test,
|
| 326 |
+
status: 'error',
|
| 327 |
+
message: `${test} test failed for ${page.name}: ${result.error}`
|
| 328 |
+
});
|
| 329 |
+
}
|
| 330 |
+
} catch (error) {
|
| 331 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator error';
|
| 332 |
+
allTestsPassed = false;
|
| 333 |
+
this.logResult({
|
| 334 |
+
page: page.name,
|
| 335 |
+
test: test,
|
| 336 |
+
status: 'error',
|
| 337 |
+
message: `${test} test failed for ${page.name}: ${error.message}`
|
| 338 |
+
});
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
await this.delay(200); // Small delay between tests
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
// Update page status
|
| 345 |
+
testDiv.className = `page-test ${allTestsPassed ? 'success' : 'error'}`;
|
| 346 |
+
testDiv.querySelector('.status-indicator').className = `status-indicator ${allTestsPassed ? 'success' : 'error'}`;
|
| 347 |
+
testDiv.querySelector('.test-page-btn').disabled = false;
|
| 348 |
+
|
| 349 |
+
this.logResult({
|
| 350 |
+
page: page.name,
|
| 351 |
+
status: allTestsPassed ? 'completed' : 'failed',
|
| 352 |
+
message: `${page.name} ${allTestsPassed ? 'completed successfully' : 'failed'}`
|
| 353 |
+
});
|
| 354 |
+
|
| 355 |
+
this.updateStats();
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
async executeTest(test, page) {
|
| 359 |
+
switch (test) {
|
| 360 |
+
case 'load':
|
| 361 |
+
return await this.testPageLoad(page);
|
| 362 |
+
case 'api':
|
| 363 |
+
return await this.testAPIConnectivity(page);
|
| 364 |
+
case 'core':
|
| 365 |
+
return await this.testCoreIntegration(page);
|
| 366 |
+
case 'charts':
|
| 367 |
+
return await this.testChartsFunctionality(page);
|
| 368 |
+
case 'crud':
|
| 369 |
+
return await this.testCRUDOperations(page);
|
| 370 |
+
case 'upload':
|
| 371 |
+
return await this.testUploadFunctionality(page);
|
| 372 |
+
case 'scraping':
|
| 373 |
+
return await this.testScrapingFunctionality(page);
|
| 374 |
+
case 'stats':
|
| 375 |
+
return await this.testStatisticsFunctionality(page);
|
| 376 |
+
case 'reports':
|
| 377 |
+
return await this.testReportsFunctionality(page);
|
| 378 |
+
case 'navigation':
|
| 379 |
+
return await this.testNavigationFunctionality(page);
|
| 380 |
+
default:
|
| 381 |
+
return { success: false, error: 'Unknown test' };
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
async testPageLoad(page) {
|
| 386 |
+
try {
|
| 387 |
+
const response = await fetch(`${this.baseURL}/${page.url}`);
|
| 388 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 389 |
+
} catch (error) {
|
| 390 |
+
return { success: false, error: error.message };
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
async testAPIConnectivity(page) {
|
| 395 |
+
try {
|
| 396 |
+
const response = await fetch(`${this.baseURL}/api/health`);
|
| 397 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 398 |
+
} catch (error) {
|
| 399 |
+
return { success: false, error: error.message };
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
async testCoreIntegration(page) {
|
| 404 |
+
try {
|
| 405 |
+
// Check if core.js is loaded
|
| 406 |
+
if (typeof dashboardCore === 'undefined') {
|
| 407 |
+
return { success: false, error: 'Core module not loaded' };
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Check if core is initialized
|
| 411 |
+
if (!dashboardCore.isInitialized) {
|
| 412 |
+
return { success: false, error: 'Core module not initialized' };
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
return { success: true, error: null };
|
| 416 |
+
} catch (error) {
|
| 417 |
+
return { success: false, error: error.message };
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
async testChartsFunctionality(page) {
|
| 422 |
+
try {
|
| 423 |
+
// Check if Chart.js is available
|
| 424 |
+
if (typeof Chart === 'undefined') {
|
| 425 |
+
return { success: false, error: 'Chart.js not loaded' };
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
return { success: true, error: null };
|
| 429 |
+
} catch (error) {
|
| 430 |
+
return { success: false, error: error.message };
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
async testCRUDOperations(page) {
|
| 435 |
+
try {
|
| 436 |
+
const response = await fetch(`${this.baseURL}/api/documents`);
|
| 437 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 438 |
+
} catch (error) {
|
| 439 |
+
return { success: false, error: error.message };
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
async testUploadFunctionality(page) {
|
| 444 |
+
try {
|
| 445 |
+
const response = await fetch(`${this.baseURL}/api/ocr/status`);
|
| 446 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 447 |
+
} catch (error) {
|
| 448 |
+
return { success: false, error: error.message };
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
async testScrapingFunctionality(page) {
|
| 453 |
+
try {
|
| 454 |
+
const response = await fetch(`${this.baseURL}/api/scraping/health`);
|
| 455 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 456 |
+
} catch (error) {
|
| 457 |
+
return { success: false, error: error.message };
|
| 458 |
+
}
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
async testStatisticsFunctionality(page) {
|
| 462 |
+
try {
|
| 463 |
+
const response = await fetch(`${this.baseURL}/api/scraping/scrape/statistics`);
|
| 464 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 465 |
+
} catch (error) {
|
| 466 |
+
return { success: false, error: error.message };
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
async testReportsFunctionality(page) {
|
| 471 |
+
try {
|
| 472 |
+
const response = await fetch(`${this.baseURL}/api/analytics/overview`);
|
| 473 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 474 |
+
} catch (error) {
|
| 475 |
+
return { success: false, error: error.message };
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
async testNavigationFunctionality(page) {
|
| 480 |
+
try {
|
| 481 |
+
// Check if navigation elements exist
|
| 482 |
+
const response = await fetch(`${this.baseURL}/${page.url}`);
|
| 483 |
+
const html = await response.text();
|
| 484 |
+
|
| 485 |
+
// Check for navigation elements
|
| 486 |
+
const hasNavigation = html.includes('nav') || html.includes('sidebar') || html.includes('menu');
|
| 487 |
+
|
| 488 |
+
return { success: hasNavigation, error: hasNavigation ? null : 'No navigation found' };
|
| 489 |
+
} catch (error) {
|
| 490 |
+
return { success: false, error: error.message };
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
async runAllTests() {
|
| 495 |
+
if (this.isRunning) return;
|
| 496 |
+
|
| 497 |
+
this.isRunning = true;
|
| 498 |
+
document.getElementById('runAllBtn').disabled = true;
|
| 499 |
+
document.getElementById('runAllBtn').textContent = 'Running...';
|
| 500 |
+
|
| 501 |
+
this.clearResults();
|
| 502 |
+
|
| 503 |
+
for (let i = 0; i < this.pages.length; i++) {
|
| 504 |
+
await this.testSinglePage(i);
|
| 505 |
+
await this.delay(500); // Delay between pages
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
this.isRunning = false;
|
| 509 |
+
document.getElementById('runAllBtn').disabled = false;
|
| 510 |
+
document.getElementById('runAllBtn').textContent = 'Run All Tests';
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
async testCoreSystem() {
|
| 514 |
+
this.logResult({
|
| 515 |
+
test: 'Core System',
|
| 516 |
+
status: 'started',
|
| 517 |
+
message: 'Testing core system integration'
|
| 518 |
+
});
|
| 519 |
+
|
| 520 |
+
try {
|
| 521 |
+
// Test core module loading
|
| 522 |
+
if (typeof dashboardCore === 'undefined') {
|
| 523 |
+
throw new Error('Core module not loaded');
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
// Test core initialization
|
| 527 |
+
if (!dashboardCore.isInitialized) {
|
| 528 |
+
throw new Error('Core module not initialized');
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
// Test API client
|
| 532 |
+
if (!dashboardCore.apiClient) {
|
| 533 |
+
throw new Error('API client not available');
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
this.logResult({
|
| 537 |
+
test: 'Core System',
|
| 538 |
+
status: 'success',
|
| 539 |
+
message: 'Core system integration working correctly'
|
| 540 |
+
});
|
| 541 |
+
|
| 542 |
+
} catch (error) {
|
| 543 |
+
this.logResult({
|
| 544 |
+
test: 'Core System',
|
| 545 |
+
status: 'error',
|
| 546 |
+
message: `Core system test failed: ${error.message}`
|
| 547 |
+
});
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
this.updateStats();
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
async testAPIConnectivity() {
|
| 554 |
+
this.logResult({
|
| 555 |
+
test: 'API Connectivity',
|
| 556 |
+
status: 'started',
|
| 557 |
+
message: 'Testing API connectivity'
|
| 558 |
+
});
|
| 559 |
+
|
| 560 |
+
const endpoints = [
|
| 561 |
+
'/api/health',
|
| 562 |
+
'/api/dashboard/summary',
|
| 563 |
+
'/api/documents',
|
| 564 |
+
'/api/ocr/status',
|
| 565 |
+
'/api/scraping/health',
|
| 566 |
+
'/api/analytics/overview'
|
| 567 |
+
];
|
| 568 |
+
|
| 569 |
+
let successCount = 0;
|
| 570 |
+
let totalCount = endpoints.length;
|
| 571 |
+
|
| 572 |
+
for (const endpoint of endpoints) {
|
| 573 |
+
try {
|
| 574 |
+
const response = await fetch(`${this.baseURL}${endpoint}`);
|
| 575 |
+
if (response.ok) {
|
| 576 |
+
successCount++;
|
| 577 |
+
this.logResult({
|
| 578 |
+
test: 'API Connectivity',
|
| 579 |
+
endpoint: endpoint,
|
| 580 |
+
status: 'success',
|
| 581 |
+
message: `${endpoint} - OK`
|
| 582 |
+
});
|
| 583 |
+
} else {
|
| 584 |
+
this.logResult({
|
| 585 |
+
test: 'API Connectivity',
|
| 586 |
+
endpoint: endpoint,
|
| 587 |
+
status: 'error',
|
| 588 |
+
message: `${endpoint} - HTTP ${response.status}`
|
| 589 |
+
});
|
| 590 |
+
}
|
| 591 |
+
} catch (error) {
|
| 592 |
+
this.logResult({
|
| 593 |
+
test: 'API Connectivity',
|
| 594 |
+
endpoint: endpoint,
|
| 595 |
+
status: 'error',
|
| 596 |
+
message: `${endpoint} - ${error.message}`
|
| 597 |
+
});
|
| 598 |
+
}
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
const successRate = Math.round((successCount / totalCount) * 100);
|
| 602 |
+
this.logResult({
|
| 603 |
+
test: 'API Connectivity',
|
| 604 |
+
status: 'completed',
|
| 605 |
+
message: `API connectivity test completed: ${successCount}/${totalCount} endpoints working (${successRate}%)`
|
| 606 |
+
});
|
| 607 |
+
|
| 608 |
+
this.updateStats();
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
async testPageIntegration() {
|
| 612 |
+
this.logResult({
|
| 613 |
+
test: 'Page Integration',
|
| 614 |
+
status: 'started',
|
| 615 |
+
message: 'Testing page integration with core system'
|
| 616 |
+
});
|
| 617 |
+
|
| 618 |
+
try {
|
| 619 |
+
// Test if pages can communicate with core
|
| 620 |
+
if (typeof dashboardCore !== 'undefined') {
|
| 621 |
+
// Test event broadcasting
|
| 622 |
+
dashboardCore.broadcast('testIntegration', { test: true });
|
| 623 |
+
|
| 624 |
+
// Test event listening
|
| 625 |
+
let eventReceived = false;
|
| 626 |
+
const unsubscribe = dashboardCore.listen('testIntegration', (data) => {
|
| 627 |
+
eventReceived = true;
|
| 628 |
+
});
|
| 629 |
+
|
| 630 |
+
// Broadcast again to trigger the listener
|
| 631 |
+
dashboardCore.broadcast('testIntegration', { test: true });
|
| 632 |
+
|
| 633 |
+
// Clean up
|
| 634 |
+
if (unsubscribe) unsubscribe();
|
| 635 |
+
|
| 636 |
+
this.logResult({
|
| 637 |
+
test: 'Page Integration',
|
| 638 |
+
status: 'success',
|
| 639 |
+
message: 'Page integration with core system working correctly'
|
| 640 |
+
});
|
| 641 |
+
} else {
|
| 642 |
+
throw new Error('Core system not available');
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
} catch (error) {
|
| 646 |
+
this.logResult({
|
| 647 |
+
test: 'Page Integration',
|
| 648 |
+
status: 'error',
|
| 649 |
+
message: `Page integration test failed: ${error.message}`
|
| 650 |
+
});
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
this.updateStats();
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
logResult(result) {
|
| 657 |
+
this.results.push({
|
| 658 |
+
...result,
|
| 659 |
+
timestamp: new Date().toISOString()
|
| 660 |
+
});
|
| 661 |
+
|
| 662 |
+
const resultsDiv = document.getElementById('testResults');
|
| 663 |
+
const resultEntry = document.createElement('div');
|
| 664 |
+
resultEntry.className = `test-result ${result.status === 'success' || result.status === 'completed' ? 'success' : 'error'}`;
|
| 665 |
+
resultEntry.innerHTML = `
|
| 666 |
+
<strong>${result.page || result.test}</strong>${result.test && result.page ? ` - ${result.test}` : ''} -
|
| 667 |
+
${result.status.toUpperCase()} -
|
| 668 |
+
${result.message}
|
| 669 |
+
<br><small>${new Date().toLocaleTimeString()}</small>
|
| 670 |
+
`;
|
| 671 |
+
|
| 672 |
+
resultsDiv.appendChild(resultEntry);
|
| 673 |
+
resultsDiv.scrollTop = resultsDiv.scrollHeight;
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
updateStats() {
|
| 677 |
+
const total = this.results.length;
|
| 678 |
+
const passed = this.results.filter(r =>
|
| 679 |
+
r.status === 'success' || r.status === 'completed'
|
| 680 |
+
).length;
|
| 681 |
+
const failed = total - passed;
|
| 682 |
+
const successRate = total > 0 ? Math.round((passed / total) * 100) : 0;
|
| 683 |
+
|
| 684 |
+
this.testStats = { total, passed, failed, successRate };
|
| 685 |
+
|
| 686 |
+
document.getElementById('totalPages').textContent = total;
|
| 687 |
+
document.getElementById('passedPages').textContent = passed;
|
| 688 |
+
document.getElementById('failedPages').textContent = failed;
|
| 689 |
+
document.getElementById('successRate').textContent = successRate + '%';
|
| 690 |
+
|
| 691 |
+
const progressBar = document.getElementById('progressBar');
|
| 692 |
+
progressBar.style.width = successRate + '%';
|
| 693 |
+
progressBar.style.background = successRate >= 80 ? '#10b981' : successRate >= 60 ? '#f59e0b' : '#ef4444';
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
clearResults() {
|
| 697 |
+
this.results = [];
|
| 698 |
+
document.getElementById('testResults').innerHTML = '';
|
| 699 |
+
this.updateStats();
|
| 700 |
+
|
| 701 |
+
// Reset all page tests
|
| 702 |
+
this.pages.forEach((page, index) => {
|
| 703 |
+
const testDiv = document.getElementById(`page-${index}`);
|
| 704 |
+
testDiv.className = 'page-test';
|
| 705 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator';
|
| 706 |
+
testDiv.querySelector('.test-page-btn').disabled = false;
|
| 707 |
+
|
| 708 |
+
page.tests.forEach((test, testIndex) => {
|
| 709 |
+
const testDiv = document.getElementById(`test-${index}-${testIndex}`);
|
| 710 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator';
|
| 711 |
+
});
|
| 712 |
+
});
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
exportResults() {
|
| 716 |
+
const data = {
|
| 717 |
+
timestamp: new Date().toISOString(),
|
| 718 |
+
stats: this.testStats,
|
| 719 |
+
results: this.results
|
| 720 |
+
};
|
| 721 |
+
|
| 722 |
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
| 723 |
+
const url = URL.createObjectURL(blob);
|
| 724 |
+
const a = document.createElement('a');
|
| 725 |
+
a.href = url;
|
| 726 |
+
a.download = `comprehensive-test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
| 727 |
+
a.click();
|
| 728 |
+
URL.revokeObjectURL(url);
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
delay(ms) {
|
| 732 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
| 733 |
+
}
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
// Global tester instance
|
| 737 |
+
const tester = new ComprehensiveTester();
|
| 738 |
+
|
| 739 |
+
// Global functions for button clicks
|
| 740 |
+
function runAllTests() {
|
| 741 |
+
tester.runAllTests();
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
function testCoreSystem() {
|
| 745 |
+
tester.testCoreSystem();
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
function testAPIConnectivity() {
|
| 749 |
+
tester.testAPIConnectivity();
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
+
function testPageIntegration() {
|
| 753 |
+
tester.testPageIntegration();
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
function clearResults() {
|
| 757 |
+
tester.clearResults();
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
function exportResults() {
|
| 761 |
+
tester.exportResults();
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
console.log('🔍 Comprehensive Tester initialized');
|
| 765 |
+
</script>
|
| 766 |
+
</body>
|
| 767 |
+
</html>
|
app/static/dev/functional-test.html
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Functional Testing - Legal Dashboard</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Arial', sans-serif;
|
| 10 |
+
max-width: 1400px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.test-section {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 20px;
|
| 18 |
+
margin: 20px 0;
|
| 19 |
+
border-radius: 8px;
|
| 20 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 21 |
+
}
|
| 22 |
+
.success { color: #10b981; }
|
| 23 |
+
.error { color: #ef4444; }
|
| 24 |
+
.info { color: #3b82f6; }
|
| 25 |
+
.warning { color: #f59e0b; }
|
| 26 |
+
button {
|
| 27 |
+
background: #007bff;
|
| 28 |
+
color: white;
|
| 29 |
+
border: none;
|
| 30 |
+
padding: 10px 20px;
|
| 31 |
+
border-radius: 4px;
|
| 32 |
+
cursor: pointer;
|
| 33 |
+
margin: 5px;
|
| 34 |
+
font-size: 14px;
|
| 35 |
+
}
|
| 36 |
+
button:hover {
|
| 37 |
+
background: #0056b3;
|
| 38 |
+
}
|
| 39 |
+
button:disabled {
|
| 40 |
+
background: #ccc;
|
| 41 |
+
cursor: not-allowed;
|
| 42 |
+
}
|
| 43 |
+
.workflow-test {
|
| 44 |
+
border: 1px solid #ddd;
|
| 45 |
+
border-radius: 8px;
|
| 46 |
+
padding: 15px;
|
| 47 |
+
margin: 10px 0;
|
| 48 |
+
background: white;
|
| 49 |
+
}
|
| 50 |
+
.workflow-test.success {
|
| 51 |
+
border-color: #10b981;
|
| 52 |
+
background: #f0fdf4;
|
| 53 |
+
}
|
| 54 |
+
.workflow-test.error {
|
| 55 |
+
border-color: #ef4444;
|
| 56 |
+
background: #fef2f2;
|
| 57 |
+
}
|
| 58 |
+
.workflow-test.testing {
|
| 59 |
+
border-color: #3b82f6;
|
| 60 |
+
background: #eff6ff;
|
| 61 |
+
}
|
| 62 |
+
.test-results {
|
| 63 |
+
max-height: 400px;
|
| 64 |
+
overflow-y: auto;
|
| 65 |
+
border: 1px solid #ddd;
|
| 66 |
+
border-radius: 4px;
|
| 67 |
+
padding: 10px;
|
| 68 |
+
background: #f8f9fa;
|
| 69 |
+
font-family: 'Courier New', monospace;
|
| 70 |
+
font-size: 12px;
|
| 71 |
+
}
|
| 72 |
+
.progress-bar {
|
| 73 |
+
width: 100%;
|
| 74 |
+
height: 6px;
|
| 75 |
+
background: #e5e7eb;
|
| 76 |
+
border-radius: 3px;
|
| 77 |
+
overflow: hidden;
|
| 78 |
+
margin: 10px 0;
|
| 79 |
+
}
|
| 80 |
+
.progress-fill {
|
| 81 |
+
height: 100%;
|
| 82 |
+
background: #3b82f6;
|
| 83 |
+
transition: width 0.3s ease;
|
| 84 |
+
}
|
| 85 |
+
.file-upload-area {
|
| 86 |
+
border: 2px dashed #ddd;
|
| 87 |
+
padding: 30px;
|
| 88 |
+
text-align: center;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
margin: 20px 0;
|
| 91 |
+
background: #fafafa;
|
| 92 |
+
}
|
| 93 |
+
.file-upload-area.dragover {
|
| 94 |
+
border-color: #3b82f6;
|
| 95 |
+
background: #eff6ff;
|
| 96 |
+
}
|
| 97 |
+
.status-indicator {
|
| 98 |
+
display: inline-block;
|
| 99 |
+
width: 12px;
|
| 100 |
+
height: 12px;
|
| 101 |
+
border-radius: 50%;
|
| 102 |
+
margin-right: 8px;
|
| 103 |
+
}
|
| 104 |
+
.status-indicator.success { background: #10b981; }
|
| 105 |
+
.status-indicator.error { background: #ef4444; }
|
| 106 |
+
.status-indicator.warning { background: #f59e0b; }
|
| 107 |
+
.status-indicator.info { background: #3b82f6; }
|
| 108 |
+
.status-indicator.testing {
|
| 109 |
+
background: #3b82f6;
|
| 110 |
+
animation: pulse 1s infinite;
|
| 111 |
+
}
|
| 112 |
+
@keyframes pulse {
|
| 113 |
+
0% { opacity: 1; }
|
| 114 |
+
50% { opacity: 0.5; }
|
| 115 |
+
100% { opacity: 1; }
|
| 116 |
+
}
|
| 117 |
+
.summary-stats {
|
| 118 |
+
display: grid;
|
| 119 |
+
grid-template-columns: repeat(4, 1fr);
|
| 120 |
+
gap: 15px;
|
| 121 |
+
margin-bottom: 20px;
|
| 122 |
+
}
|
| 123 |
+
.stat-card {
|
| 124 |
+
background: white;
|
| 125 |
+
padding: 15px;
|
| 126 |
+
border-radius: 8px;
|
| 127 |
+
text-align: center;
|
| 128 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 129 |
+
}
|
| 130 |
+
.stat-number {
|
| 131 |
+
font-size: 2rem;
|
| 132 |
+
font-weight: bold;
|
| 133 |
+
margin-bottom: 5px;
|
| 134 |
+
}
|
| 135 |
+
.stat-label {
|
| 136 |
+
color: #666;
|
| 137 |
+
font-size: 0.9rem;
|
| 138 |
+
}
|
| 139 |
+
</style>
|
| 140 |
+
</head>
|
| 141 |
+
<body>
|
| 142 |
+
<h1>🔧 Functional Testing - Legal Dashboard</h1>
|
| 143 |
+
|
| 144 |
+
<div class="test-section">
|
| 145 |
+
<h2>📊 Test Summary</h2>
|
| 146 |
+
<div class="summary-stats">
|
| 147 |
+
<div class="stat-card">
|
| 148 |
+
<div class="stat-number" id="totalWorkflows">0</div>
|
| 149 |
+
<div class="stat-label">Total Workflows</div>
|
| 150 |
+
</div>
|
| 151 |
+
<div class="stat-card">
|
| 152 |
+
<div class="stat-number" id="passedWorkflows">0</div>
|
| 153 |
+
<div class="stat-label">Passed</div>
|
| 154 |
+
</div>
|
| 155 |
+
<div class="stat-card">
|
| 156 |
+
<div class="stat-number" id="failedWorkflows">0</div>
|
| 157 |
+
<div class="stat-label">Failed</div>
|
| 158 |
+
</div>
|
| 159 |
+
<div class="stat-card">
|
| 160 |
+
<div class="stat-number" id="successRate">0%</div>
|
| 161 |
+
<div class="stat-label">Success Rate</div>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="progress-bar">
|
| 165 |
+
<div class="progress-fill" id="progressBar" style="width: 0%"></div>
|
| 166 |
+
</div>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div class="test-section">
|
| 170 |
+
<h2>🎛️ Test Controls</h2>
|
| 171 |
+
<button type="button" onclick="runAllWorkflows()" id="runAllBtn">Run All Workflows</button>
|
| 172 |
+
<button type="button" onclick="testDocumentWorkflow()">Document Workflow</button>
|
| 173 |
+
<button type="button" onclick="testUploadWorkflow()">Upload Workflow</button>
|
| 174 |
+
<button type="button" onclick="testScrapingWorkflow()">Scraping Workflow</button>
|
| 175 |
+
<button type="button" onclick="testAnalyticsWorkflow()">Analytics Workflow</button>
|
| 176 |
+
<button type="button" onclick="clearResults()">Clear Results</button>
|
| 177 |
+
<button type="button" onclick="exportResults()">Export Results</button>
|
| 178 |
+
</div>
|
| 179 |
+
|
| 180 |
+
<div class="test-section">
|
| 181 |
+
<h2>📁 File Upload Test</h2>
|
| 182 |
+
<div class="file-upload-area" id="uploadZone">
|
| 183 |
+
<p><strong>Drag and drop a file here or click to select</strong></p>
|
| 184 |
+
<p>Supported formats: PDF, JPG, JPEG, PNG, TIFF</p>
|
| 185 |
+
<input type="file" id="testFileInput" accept=".pdf,.jpg,.jpeg,.png,.tiff" style="display: none;">
|
| 186 |
+
<button type="button" onclick="document.getElementById('testFileInput').click()">Select File</button>
|
| 187 |
+
</div>
|
| 188 |
+
<div id="uploadResults"></div>
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
<div class="test-section">
|
| 192 |
+
<h2>🔄 Workflow Tests</h2>
|
| 193 |
+
<div id="workflowTests">
|
| 194 |
+
<!-- Workflow tests will be generated here -->
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
<div class="test-section">
|
| 199 |
+
<h2>📋 Test Results</h2>
|
| 200 |
+
<div class="test-results" id="testResults">
|
| 201 |
+
<!-- Test results will be displayed here -->
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<script src="../js/api-client.js"></script>
|
| 206 |
+
<script>
|
| 207 |
+
class FunctionalTester {
|
| 208 |
+
constructor() {
|
| 209 |
+
this.baseURL = window.location.origin;
|
| 210 |
+
this.results = [];
|
| 211 |
+
this.testStats = {
|
| 212 |
+
total: 0,
|
| 213 |
+
passed: 0,
|
| 214 |
+
failed: 0,
|
| 215 |
+
successRate: 0
|
| 216 |
+
};
|
| 217 |
+
this.isRunning = false;
|
| 218 |
+
|
| 219 |
+
this.workflows = [
|
| 220 |
+
{
|
| 221 |
+
name: 'Document Management Workflow',
|
| 222 |
+
description: 'Test complete document CRUD operations',
|
| 223 |
+
steps: [
|
| 224 |
+
{ name: 'Get Documents List', action: 'getDocuments' },
|
| 225 |
+
{ name: 'Create Test Document', action: 'createDocument' },
|
| 226 |
+
{ name: 'Update Document', action: 'updateDocument' },
|
| 227 |
+
{ name: 'Search Documents', action: 'searchDocuments' },
|
| 228 |
+
{ name: 'Delete Test Document', action: 'deleteDocument' }
|
| 229 |
+
]
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
name: 'File Upload & OCR Workflow',
|
| 233 |
+
description: 'Test file upload and OCR processing',
|
| 234 |
+
steps: [
|
| 235 |
+
{ name: 'Upload Test File', action: 'uploadFile' },
|
| 236 |
+
{ name: 'Process OCR', action: 'processOCR' },
|
| 237 |
+
{ name: 'Get OCR Status', action: 'getOCRStatus' },
|
| 238 |
+
{ name: 'Extract Text', action: 'extractText' }
|
| 239 |
+
]
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
name: 'Dashboard Analytics Workflow',
|
| 243 |
+
description: 'Test dashboard and analytics functionality',
|
| 244 |
+
steps: [
|
| 245 |
+
{ name: 'Get Dashboard Summary', action: 'getDashboardSummary' },
|
| 246 |
+
{ name: 'Get Charts Data', action: 'getChartsData' },
|
| 247 |
+
{ name: 'Get AI Suggestions', action: 'getAISuggestions' },
|
| 248 |
+
{ name: 'Get Performance Metrics', action: 'getPerformanceMetrics' }
|
| 249 |
+
]
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
name: 'Scraping & Rating Workflow',
|
| 253 |
+
description: 'Test web scraping and content rating',
|
| 254 |
+
steps: [
|
| 255 |
+
{ name: 'Get Scraping Status', action: 'getScrapingStatus' },
|
| 256 |
+
{ name: 'Get Scraping Statistics', action: 'getScrapingStatistics' },
|
| 257 |
+
{ name: 'Get Rating Summary', action: 'getRatingSummary' },
|
| 258 |
+
{ name: 'Check Scraping Health', action: 'getScrapingHealth' }
|
| 259 |
+
]
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
name: 'Analytics & Reporting Workflow',
|
| 263 |
+
description: 'Test advanced analytics and reporting',
|
| 264 |
+
steps: [
|
| 265 |
+
{ name: 'Get Analytics Overview', action: 'getAnalyticsOverview' },
|
| 266 |
+
{ name: 'Get Performance Analytics', action: 'getPerformanceAnalytics' },
|
| 267 |
+
{ name: 'Get Entity Analysis', action: 'getEntityAnalysis' },
|
| 268 |
+
{ name: 'Get Quality Analysis', action: 'getQualityAnalysis' }
|
| 269 |
+
]
|
| 270 |
+
}
|
| 271 |
+
];
|
| 272 |
+
|
| 273 |
+
this.initialize();
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
initialize() {
|
| 277 |
+
this.createWorkflowTests();
|
| 278 |
+
this.setupFileUpload();
|
| 279 |
+
this.updateStats();
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
createWorkflowTests() {
|
| 283 |
+
const container = document.getElementById('workflowTests');
|
| 284 |
+
container.innerHTML = '';
|
| 285 |
+
|
| 286 |
+
this.workflows.forEach((workflow, index) => {
|
| 287 |
+
const testDiv = document.createElement('div');
|
| 288 |
+
testDiv.className = 'workflow-test';
|
| 289 |
+
testDiv.id = `workflow-${index}`;
|
| 290 |
+
|
| 291 |
+
testDiv.innerHTML = `
|
| 292 |
+
<div class="status-indicator"></div>
|
| 293 |
+
<h3>${workflow.name}</h3>
|
| 294 |
+
<p>${workflow.description}</p>
|
| 295 |
+
<div class="steps" id="steps-${index}">
|
| 296 |
+
${workflow.steps.map((step, stepIndex) => `
|
| 297 |
+
<div class="step" id="step-${index}-${stepIndex}">
|
| 298 |
+
<span class="status-indicator"></span>
|
| 299 |
+
${step.name}
|
| 300 |
+
</div>
|
| 301 |
+
`).join('')}
|
| 302 |
+
</div>
|
| 303 |
+
<button type="button" onclick="tester.runWorkflow(${index})" class="run-workflow-btn">
|
| 304 |
+
Run Workflow
|
| 305 |
+
</button>
|
| 306 |
+
`;
|
| 307 |
+
|
| 308 |
+
container.appendChild(testDiv);
|
| 309 |
+
});
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
setupFileUpload() {
|
| 313 |
+
const uploadZone = document.getElementById('uploadZone');
|
| 314 |
+
const fileInput = document.getElementById('testFileInput');
|
| 315 |
+
|
| 316 |
+
uploadZone.addEventListener('dragover', (e) => {
|
| 317 |
+
e.preventDefault();
|
| 318 |
+
uploadZone.classList.add('dragover');
|
| 319 |
+
});
|
| 320 |
+
|
| 321 |
+
uploadZone.addEventListener('dragleave', () => {
|
| 322 |
+
uploadZone.classList.remove('dragover');
|
| 323 |
+
});
|
| 324 |
+
|
| 325 |
+
uploadZone.addEventListener('drop', (e) => {
|
| 326 |
+
e.preventDefault();
|
| 327 |
+
uploadZone.classList.remove('dragover');
|
| 328 |
+
const files = e.dataTransfer.files;
|
| 329 |
+
if (files.length > 0) {
|
| 330 |
+
this.testFileUpload(files[0]);
|
| 331 |
+
}
|
| 332 |
+
});
|
| 333 |
+
|
| 334 |
+
fileInput.addEventListener('change', (e) => {
|
| 335 |
+
if (e.target.files.length > 0) {
|
| 336 |
+
this.testFileUpload(e.target.files[0]);
|
| 337 |
+
}
|
| 338 |
+
});
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
async runWorkflow(workflowIndex) {
|
| 342 |
+
const workflow = this.workflows[workflowIndex];
|
| 343 |
+
const testDiv = document.getElementById(`workflow-${workflowIndex}`);
|
| 344 |
+
|
| 345 |
+
// Set testing state
|
| 346 |
+
testDiv.className = 'workflow-test testing';
|
| 347 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
|
| 348 |
+
testDiv.querySelector('.run-workflow-btn').disabled = true;
|
| 349 |
+
|
| 350 |
+
this.logResult({
|
| 351 |
+
workflow: workflow.name,
|
| 352 |
+
status: 'started',
|
| 353 |
+
message: `Starting ${workflow.name}`
|
| 354 |
+
});
|
| 355 |
+
|
| 356 |
+
let allStepsPassed = true;
|
| 357 |
+
|
| 358 |
+
for (let stepIndex = 0; stepIndex < workflow.steps.length; stepIndex++) {
|
| 359 |
+
const step = workflow.steps[stepIndex];
|
| 360 |
+
const stepDiv = document.getElementById(`step-${workflowIndex}-${stepIndex}`);
|
| 361 |
+
|
| 362 |
+
// Set step testing state
|
| 363 |
+
stepDiv.querySelector('.status-indicator').className = 'status-indicator testing';
|
| 364 |
+
|
| 365 |
+
try {
|
| 366 |
+
const result = await this.executeStep(step.action);
|
| 367 |
+
|
| 368 |
+
if (result.success) {
|
| 369 |
+
stepDiv.querySelector('.status-indicator').className = 'status-indicator success';
|
| 370 |
+
this.logResult({
|
| 371 |
+
workflow: workflow.name,
|
| 372 |
+
step: step.name,
|
| 373 |
+
status: 'success',
|
| 374 |
+
message: `${step.name} completed successfully`
|
| 375 |
+
});
|
| 376 |
+
} else {
|
| 377 |
+
stepDiv.querySelector('.status-indicator').className = 'status-indicator error';
|
| 378 |
+
allStepsPassed = false;
|
| 379 |
+
this.logResult({
|
| 380 |
+
workflow: workflow.name,
|
| 381 |
+
step: step.name,
|
| 382 |
+
status: 'error',
|
| 383 |
+
message: `${step.name} failed: ${result.error}`
|
| 384 |
+
});
|
| 385 |
+
}
|
| 386 |
+
} catch (error) {
|
| 387 |
+
stepDiv.querySelector('.status-indicator').className = 'status-indicator error';
|
| 388 |
+
allStepsPassed = false;
|
| 389 |
+
this.logResult({
|
| 390 |
+
workflow: workflow.name,
|
| 391 |
+
step: step.name,
|
| 392 |
+
status: 'error',
|
| 393 |
+
message: `${step.name} failed: ${error.message}`
|
| 394 |
+
});
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
await this.delay(200); // Small delay between steps
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
// Update workflow status
|
| 401 |
+
testDiv.className = `workflow-test ${allStepsPassed ? 'success' : 'error'}`;
|
| 402 |
+
testDiv.querySelector('.status-indicator').className = `status-indicator ${allStepsPassed ? 'success' : 'error'}`;
|
| 403 |
+
testDiv.querySelector('.run-workflow-btn').disabled = false;
|
| 404 |
+
|
| 405 |
+
this.logResult({
|
| 406 |
+
workflow: workflow.name,
|
| 407 |
+
status: allStepsPassed ? 'completed' : 'failed',
|
| 408 |
+
message: `${workflow.name} ${allStepsPassed ? 'completed successfully' : 'failed'}`
|
| 409 |
+
});
|
| 410 |
+
|
| 411 |
+
this.updateStats();
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
async executeStep(action) {
|
| 415 |
+
switch (action) {
|
| 416 |
+
case 'getDocuments':
|
| 417 |
+
return await this.testGetDocuments();
|
| 418 |
+
case 'createDocument':
|
| 419 |
+
return await this.testCreateDocument();
|
| 420 |
+
case 'updateDocument':
|
| 421 |
+
return await this.testUpdateDocument();
|
| 422 |
+
case 'searchDocuments':
|
| 423 |
+
return await this.testSearchDocuments();
|
| 424 |
+
case 'deleteDocument':
|
| 425 |
+
return await this.testDeleteDocument();
|
| 426 |
+
case 'uploadFile':
|
| 427 |
+
return await this.testUploadFile();
|
| 428 |
+
case 'processOCR':
|
| 429 |
+
return await this.testProcessOCR();
|
| 430 |
+
case 'getOCRStatus':
|
| 431 |
+
return await this.testGetOCRStatus();
|
| 432 |
+
case 'extractText':
|
| 433 |
+
return await this.testExtractText();
|
| 434 |
+
case 'getDashboardSummary':
|
| 435 |
+
return await this.testGetDashboardSummary();
|
| 436 |
+
case 'getChartsData':
|
| 437 |
+
return await this.testGetChartsData();
|
| 438 |
+
case 'getAISuggestions':
|
| 439 |
+
return await this.testGetAISuggestions();
|
| 440 |
+
case 'getPerformanceMetrics':
|
| 441 |
+
return await this.testGetPerformanceMetrics();
|
| 442 |
+
case 'getScrapingStatus':
|
| 443 |
+
return await this.testGetScrapingStatus();
|
| 444 |
+
case 'getScrapingStatistics':
|
| 445 |
+
return await this.testGetScrapingStatistics();
|
| 446 |
+
case 'getRatingSummary':
|
| 447 |
+
return await this.testGetRatingSummary();
|
| 448 |
+
case 'getScrapingHealth':
|
| 449 |
+
return await this.testGetScrapingHealth();
|
| 450 |
+
case 'getAnalyticsOverview':
|
| 451 |
+
return await this.testGetAnalyticsOverview();
|
| 452 |
+
case 'getPerformanceAnalytics':
|
| 453 |
+
return await this.testGetPerformanceAnalytics();
|
| 454 |
+
case 'getEntityAnalysis':
|
| 455 |
+
return await this.testGetEntityAnalysis();
|
| 456 |
+
case 'getQualityAnalysis':
|
| 457 |
+
return await this.testGetQualityAnalysis();
|
| 458 |
+
default:
|
| 459 |
+
return { success: false, error: 'Unknown action' };
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// Individual step implementations
|
| 464 |
+
async testGetDocuments() {
|
| 465 |
+
try {
|
| 466 |
+
const response = await fetch(`${this.baseURL}/api/documents`);
|
| 467 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 468 |
+
} catch (error) {
|
| 469 |
+
return { success: false, error: error.message };
|
| 470 |
+
}
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
async testCreateDocument() {
|
| 474 |
+
try {
|
| 475 |
+
const testDoc = {
|
| 476 |
+
title: `Test Document ${Date.now()}`,
|
| 477 |
+
content: 'This is a test document for functional testing',
|
| 478 |
+
category: 'test',
|
| 479 |
+
source: 'functional_test'
|
| 480 |
+
};
|
| 481 |
+
|
| 482 |
+
const response = await fetch(`${this.baseURL}/api/documents`, {
|
| 483 |
+
method: 'POST',
|
| 484 |
+
headers: { 'Content-Type': 'application/json' },
|
| 485 |
+
body: JSON.stringify(testDoc)
|
| 486 |
+
});
|
| 487 |
+
|
| 488 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 489 |
+
} catch (error) {
|
| 490 |
+
return { success: false, error: error.message };
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
async testUpdateDocument() {
|
| 495 |
+
try {
|
| 496 |
+
const response = await fetch(`${this.baseURL}/api/documents/1`, {
|
| 497 |
+
method: 'PUT',
|
| 498 |
+
headers: { 'Content-Type': 'application/json' },
|
| 499 |
+
body: JSON.stringify({ title: 'Updated Test Document' })
|
| 500 |
+
});
|
| 501 |
+
|
| 502 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 503 |
+
} catch (error) {
|
| 504 |
+
return { success: false, error: error.message };
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
async testSearchDocuments() {
|
| 509 |
+
try {
|
| 510 |
+
const response = await fetch(`${this.baseURL}/api/documents/search?q=test`);
|
| 511 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 512 |
+
} catch (error) {
|
| 513 |
+
return { success: false, error: error.message };
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
async testDeleteDocument() {
|
| 518 |
+
try {
|
| 519 |
+
const response = await fetch(`${this.baseURL}/api/documents/1`, {
|
| 520 |
+
method: 'DELETE'
|
| 521 |
+
});
|
| 522 |
+
|
| 523 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 524 |
+
} catch (error) {
|
| 525 |
+
return { success: false, error: error.message };
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
async testUploadFile() {
|
| 530 |
+
try {
|
| 531 |
+
// Create a test file
|
| 532 |
+
const testContent = 'This is a test file for functional testing';
|
| 533 |
+
const blob = new Blob([testContent], { type: 'text/plain' });
|
| 534 |
+
const file = new File([blob], 'test.txt', { type: 'text/plain' });
|
| 535 |
+
|
| 536 |
+
const formData = new FormData();
|
| 537 |
+
formData.append('file', file);
|
| 538 |
+
|
| 539 |
+
const response = await fetch(`${this.baseURL}/api/ocr/upload`, {
|
| 540 |
+
method: 'POST',
|
| 541 |
+
body: formData
|
| 542 |
+
});
|
| 543 |
+
|
| 544 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 545 |
+
} catch (error) {
|
| 546 |
+
return { success: false, error: error.message };
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
async testProcessOCR() {
|
| 551 |
+
try {
|
| 552 |
+
const response = await fetch(`${this.baseURL}/api/ocr/process`, {
|
| 553 |
+
method: 'POST',
|
| 554 |
+
headers: { 'Content-Type': 'application/json' },
|
| 555 |
+
body: JSON.stringify({ file_id: 'test_file' })
|
| 556 |
+
});
|
| 557 |
+
|
| 558 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 559 |
+
} catch (error) {
|
| 560 |
+
return { success: false, error: error.message };
|
| 561 |
+
}
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
async testGetOCRStatus() {
|
| 565 |
+
try {
|
| 566 |
+
const response = await fetch(`${this.baseURL}/api/ocr/status`);
|
| 567 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 568 |
+
} catch (error) {
|
| 569 |
+
return { success: false, error: error.message };
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
async testExtractText() {
|
| 574 |
+
try {
|
| 575 |
+
const response = await fetch(`${this.baseURL}/api/ocr/extract`, {
|
| 576 |
+
method: 'POST',
|
| 577 |
+
headers: { 'Content-Type': 'application/json' },
|
| 578 |
+
body: JSON.stringify({ file_id: 'test_file' })
|
| 579 |
+
});
|
| 580 |
+
|
| 581 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 582 |
+
} catch (error) {
|
| 583 |
+
return { success: false, error: error.message };
|
| 584 |
+
}
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
async testGetDashboardSummary() {
|
| 588 |
+
try {
|
| 589 |
+
const response = await fetch(`${this.baseURL}/api/dashboard/summary`);
|
| 590 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 591 |
+
} catch (error) {
|
| 592 |
+
return { success: false, error: error.message };
|
| 593 |
+
}
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
async testGetChartsData() {
|
| 597 |
+
try {
|
| 598 |
+
const response = await fetch(`${this.baseURL}/api/dashboard/charts-data`);
|
| 599 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 600 |
+
} catch (error) {
|
| 601 |
+
return { success: false, error: error.message };
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
async testGetAISuggestions() {
|
| 606 |
+
try {
|
| 607 |
+
const response = await fetch(`${this.baseURL}/api/dashboard/ai-suggestions`);
|
| 608 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 609 |
+
} catch (error) {
|
| 610 |
+
return { success: false, error: error.message };
|
| 611 |
+
}
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
async testGetPerformanceMetrics() {
|
| 615 |
+
try {
|
| 616 |
+
const response = await fetch(`${this.baseURL}/api/dashboard/performance-metrics`);
|
| 617 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 618 |
+
} catch (error) {
|
| 619 |
+
return { success: false, error: error.message };
|
| 620 |
+
}
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
async testGetScrapingStatus() {
|
| 624 |
+
try {
|
| 625 |
+
const response = await fetch(`${this.baseURL}/api/scraping/scrape/status`);
|
| 626 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 627 |
+
} catch (error) {
|
| 628 |
+
return { success: false, error: error.message };
|
| 629 |
+
}
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
async testGetScrapingStatistics() {
|
| 633 |
+
try {
|
| 634 |
+
const response = await fetch(`${this.baseURL}/api/scraping/scrape/statistics`);
|
| 635 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 636 |
+
} catch (error) {
|
| 637 |
+
return { success: false, error: error.message };
|
| 638 |
+
}
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
async testGetRatingSummary() {
|
| 642 |
+
try {
|
| 643 |
+
const response = await fetch(`${this.baseURL}/api/scraping/rating/summary`);
|
| 644 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 645 |
+
} catch (error) {
|
| 646 |
+
return { success: false, error: error.message };
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
async testGetScrapingHealth() {
|
| 651 |
+
try {
|
| 652 |
+
const response = await fetch(`${this.baseURL}/api/scraping/health`);
|
| 653 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 654 |
+
} catch (error) {
|
| 655 |
+
return { success: false, error: error.message };
|
| 656 |
+
}
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
async testGetAnalyticsOverview() {
|
| 660 |
+
try {
|
| 661 |
+
const response = await fetch(`${this.baseURL}/api/analytics/overview`);
|
| 662 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 663 |
+
} catch (error) {
|
| 664 |
+
return { success: false, error: error.message };
|
| 665 |
+
}
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
async testGetPerformanceAnalytics() {
|
| 669 |
+
try {
|
| 670 |
+
const response = await fetch(`${this.baseURL}/api/analytics/performance`);
|
| 671 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 672 |
+
} catch (error) {
|
| 673 |
+
return { success: false, error: error.message };
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
async testGetEntityAnalysis() {
|
| 678 |
+
try {
|
| 679 |
+
const response = await fetch(`${this.baseURL}/api/analytics/entities`);
|
| 680 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 681 |
+
} catch (error) {
|
| 682 |
+
return { success: false, error: error.message };
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
async testGetQualityAnalysis() {
|
| 687 |
+
try {
|
| 688 |
+
const response = await fetch(`${this.baseURL}/api/analytics/quality-analysis`);
|
| 689 |
+
return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
|
| 690 |
+
} catch (error) {
|
| 691 |
+
return { success: false, error: error.message };
|
| 692 |
+
}
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
async testFileUpload(file) {
|
| 696 |
+
const resultsDiv = document.getElementById('uploadResults');
|
| 697 |
+
resultsDiv.innerHTML = `<p>Testing file upload: ${file.name} (${file.size} bytes)</p>`;
|
| 698 |
+
|
| 699 |
+
try {
|
| 700 |
+
const formData = new FormData();
|
| 701 |
+
formData.append('file', file);
|
| 702 |
+
|
| 703 |
+
const startTime = Date.now();
|
| 704 |
+
const response = await fetch(`${this.baseURL}/api/ocr/upload`, {
|
| 705 |
+
method: 'POST',
|
| 706 |
+
body: formData
|
| 707 |
+
});
|
| 708 |
+
|
| 709 |
+
const responseTime = Date.now() - startTime;
|
| 710 |
+
const responseData = await response.json();
|
| 711 |
+
|
| 712 |
+
const success = response.ok;
|
| 713 |
+
|
| 714 |
+
resultsDiv.innerHTML = `
|
| 715 |
+
<div class="${success ? 'success' : 'error'}">
|
| 716 |
+
<h4>File Upload Test Results</h4>
|
| 717 |
+
<p><strong>File:</strong> ${file.name}</p>
|
| 718 |
+
<p><strong>Size:</strong> ${file.size} bytes</p>
|
| 719 |
+
<p><strong>Status:</strong> ${response.status} ${response.statusText}</p>
|
| 720 |
+
<p><strong>Response Time:</strong> ${responseTime}ms</p>
|
| 721 |
+
<div class="response-data">
|
| 722 |
+
${JSON.stringify(responseData, null, 2)}
|
| 723 |
+
</div>
|
| 724 |
+
</div>
|
| 725 |
+
`;
|
| 726 |
+
|
| 727 |
+
this.logResult({
|
| 728 |
+
workflow: 'File Upload',
|
| 729 |
+
status: success ? 'success' : 'error',
|
| 730 |
+
message: `File upload ${success ? 'succeeded' : 'failed'}: ${file.name}`
|
| 731 |
+
});
|
| 732 |
+
|
| 733 |
+
} catch (error) {
|
| 734 |
+
resultsDiv.innerHTML = `
|
| 735 |
+
<div class="error">
|
| 736 |
+
<h4>File Upload Test Failed</h4>
|
| 737 |
+
<p>Error: ${error.message}</p>
|
| 738 |
+
</div>
|
| 739 |
+
`;
|
| 740 |
+
|
| 741 |
+
this.logResult({
|
| 742 |
+
workflow: 'File Upload',
|
| 743 |
+
status: 'error',
|
| 744 |
+
message: `File upload failed: ${error.message}`
|
| 745 |
+
});
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
this.updateStats();
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
async runAllWorkflows() {
|
| 752 |
+
if (this.isRunning) return;
|
| 753 |
+
|
| 754 |
+
this.isRunning = true;
|
| 755 |
+
document.getElementById('runAllBtn').disabled = true;
|
| 756 |
+
document.getElementById('runAllBtn').textContent = 'Running...';
|
| 757 |
+
|
| 758 |
+
this.clearResults();
|
| 759 |
+
|
| 760 |
+
for (let i = 0; i < this.workflows.length; i++) {
|
| 761 |
+
await this.runWorkflow(i);
|
| 762 |
+
await this.delay(500); // Delay between workflows
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
this.isRunning = false;
|
| 766 |
+
document.getElementById('runAllBtn').disabled = false;
|
| 767 |
+
document.getElementById('runAllBtn').textContent = 'Run All Workflows';
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
logResult(result) {
|
| 771 |
+
this.results.push({
|
| 772 |
+
...result,
|
| 773 |
+
timestamp: new Date().toISOString()
|
| 774 |
+
});
|
| 775 |
+
|
| 776 |
+
const resultsDiv = document.getElementById('testResults');
|
| 777 |
+
const resultEntry = document.createElement('div');
|
| 778 |
+
resultEntry.className = `test-result ${result.status === 'success' || result.status === 'completed' ? 'success' : 'error'}`;
|
| 779 |
+
resultEntry.innerHTML = `
|
| 780 |
+
<strong>${result.workflow}</strong>${result.step ? ` - ${result.step}` : ''} -
|
| 781 |
+
${result.status.toUpperCase()} -
|
| 782 |
+
${result.message}
|
| 783 |
+
<br><small>${new Date().toLocaleTimeString()}</small>
|
| 784 |
+
`;
|
| 785 |
+
|
| 786 |
+
resultsDiv.appendChild(resultEntry);
|
| 787 |
+
resultsDiv.scrollTop = resultsDiv.scrollHeight;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
updateStats() {
|
| 791 |
+
const total = this.results.length;
|
| 792 |
+
const passed = this.results.filter(r =>
|
| 793 |
+
r.status === 'success' || r.status === 'completed'
|
| 794 |
+
).length;
|
| 795 |
+
const failed = total - passed;
|
| 796 |
+
const successRate = total > 0 ? Math.round((passed / total) * 100) : 0;
|
| 797 |
+
|
| 798 |
+
this.testStats = { total, passed, failed, successRate };
|
| 799 |
+
|
| 800 |
+
document.getElementById('totalWorkflows').textContent = total;
|
| 801 |
+
document.getElementById('passedWorkflows').textContent = passed;
|
| 802 |
+
document.getElementById('failedWorkflows').textContent = failed;
|
| 803 |
+
document.getElementById('successRate').textContent = successRate + '%';
|
| 804 |
+
|
| 805 |
+
const progressBar = document.getElementById('progressBar');
|
| 806 |
+
progressBar.style.width = successRate + '%';
|
| 807 |
+
progressBar.style.background = successRate >= 80 ? '#10b981' : successRate >= 60 ? '#f59e0b' : '#ef4444';
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
clearResults() {
|
| 811 |
+
this.results = [];
|
| 812 |
+
document.getElementById('testResults').innerHTML = '';
|
| 813 |
+
this.updateStats();
|
| 814 |
+
|
| 815 |
+
// Reset all workflow tests
|
| 816 |
+
this.workflows.forEach((workflow, index) => {
|
| 817 |
+
const testDiv = document.getElementById(`workflow-${index}`);
|
| 818 |
+
testDiv.className = 'workflow-test';
|
| 819 |
+
testDiv.querySelector('.status-indicator').className = 'status-indicator';
|
| 820 |
+
testDiv.querySelector('.run-workflow-btn').disabled = false;
|
| 821 |
+
|
| 822 |
+
workflow.steps.forEach((step, stepIndex) => {
|
| 823 |
+
const stepDiv = document.getElementById(`step-${index}-${stepIndex}`);
|
| 824 |
+
stepDiv.querySelector('.status-indicator').className = 'status-indicator';
|
| 825 |
+
});
|
| 826 |
+
});
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
exportResults() {
|
| 830 |
+
const data = {
|
| 831 |
+
timestamp: new Date().toISOString(),
|
| 832 |
+
stats: this.testStats,
|
| 833 |
+
results: this.results
|
| 834 |
+
};
|
| 835 |
+
|
| 836 |
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
| 837 |
+
const url = URL.createObjectURL(blob);
|
| 838 |
+
const a = document.createElement('a');
|
| 839 |
+
a.href = url;
|
| 840 |
+
a.download = `functional-test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
| 841 |
+
a.click();
|
| 842 |
+
URL.revokeObjectURL(url);
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
delay(ms) {
|
| 846 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
| 847 |
+
}
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
// Global tester instance
|
| 851 |
+
const tester = new FunctionalTester();
|
| 852 |
+
|
| 853 |
+
// Global functions for button clicks
|
| 854 |
+
function runAllWorkflows() {
|
| 855 |
+
tester.runAllWorkflows();
|
| 856 |
+
}
|
| 857 |
+
|
| 858 |
+
function testDocumentWorkflow() {
|
| 859 |
+
tester.runWorkflow(0);
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
function testUploadWorkflow() {
|
| 863 |
+
tester.runWorkflow(1);
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
function testScrapingWorkflow() {
|
| 867 |
+
tester.runWorkflow(3);
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
function testAnalyticsWorkflow() {
|
| 871 |
+
tester.runWorkflow(4);
|
| 872 |
+
}
|
| 873 |
+
|
| 874 |
+
function clearResults() {
|
| 875 |
+
tester.clearResults();
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
function exportResults() {
|
| 879 |
+
tester.exportResults();
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
console.log('🔧 Functional Tester initialized');
|
| 883 |
+
</script>
|
| 884 |
+
</body>
|
| 885 |
+
</html>
|
app/static/dev/integration-test.html
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Integration Test - Legal Dashboard</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Arial', sans-serif;
|
| 10 |
+
max-inline-size: 1200px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.test-section {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 20px;
|
| 18 |
+
margin: 20px 0;
|
| 19 |
+
border-radius: 8px;
|
| 20 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 21 |
+
}
|
| 22 |
+
.success { color: #10b981; }
|
| 23 |
+
.error { color: #ef4444; }
|
| 24 |
+
.info { color: #3b82f6; }
|
| 25 |
+
.warning { color: #f59e0b; }
|
| 26 |
+
button {
|
| 27 |
+
background: #007bff;
|
| 28 |
+
color: white;
|
| 29 |
+
border: none;
|
| 30 |
+
padding: 10px 20px;
|
| 31 |
+
border-radius: 4px;
|
| 32 |
+
cursor: pointer;
|
| 33 |
+
margin: 5px;
|
| 34 |
+
}
|
| 35 |
+
button:hover {
|
| 36 |
+
background: #0056b3;
|
| 37 |
+
}
|
| 38 |
+
pre {
|
| 39 |
+
background: #f8f9fa;
|
| 40 |
+
padding: 10px;
|
| 41 |
+
border-radius: 4px;
|
| 42 |
+
overflow-x: auto;
|
| 43 |
+
max-block-size: 300px;
|
| 44 |
+
overflow-y: auto;
|
| 45 |
+
}
|
| 46 |
+
.event-log {
|
| 47 |
+
background: #1a1a1a;
|
| 48 |
+
color: #00ff00;
|
| 49 |
+
padding: 15px;
|
| 50 |
+
border-radius: 8px;
|
| 51 |
+
font-family: 'Courier New', monospace;
|
| 52 |
+
max-block-size: 400px;
|
| 53 |
+
overflow-y: auto;
|
| 54 |
+
}
|
| 55 |
+
.status-indicator {
|
| 56 |
+
display: inline-block;
|
| 57 |
+
inline-size: 12px;
|
| 58 |
+
block-size: 12px;
|
| 59 |
+
border-radius: 50%;
|
| 60 |
+
margin-inline-end: 8px;
|
| 61 |
+
}
|
| 62 |
+
.status-indicator.success { background: #10b981; }
|
| 63 |
+
.status-indicator.error { background: #ef4444; }
|
| 64 |
+
.status-indicator.warning { background: #f59e0b; }
|
| 65 |
+
.status-indicator.info { background: #3b82f6; }
|
| 66 |
+
</style>
|
| 67 |
+
</head>
|
| 68 |
+
<body>
|
| 69 |
+
<h1>🔍 Integration Test - Legal Dashboard</h1>
|
| 70 |
+
|
| 71 |
+
<div class="test-section">
|
| 72 |
+
<h2>📦 Core Module Test</h2>
|
| 73 |
+
<button type="button" onclick="testCoreModule()">Test Core Module</button>
|
| 74 |
+
<div id="coreTestResult"></div>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<div class="test-section">
|
| 78 |
+
<h2>🔌 API Connectivity Test</h2>
|
| 79 |
+
<button type="button" onclick="testAPIConnectivity()">Test API Connectivity</button>
|
| 80 |
+
<div id="apiTestResult"></div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div class="test-section">
|
| 84 |
+
<h2>📡 Cross-Page Communication Test</h2>
|
| 85 |
+
<button type="button" onclick="testCrossPageCommunication()">Test Cross-Page Events</button>
|
| 86 |
+
<div id="communicationTestResult"></div>
|
| 87 |
+
</div>
|
| 88 |
+
|
| 89 |
+
<div class="test-section">
|
| 90 |
+
<h2>📊 Event Log</h2>
|
| 91 |
+
<button type="button" onclick="clearEventLog()">Clear Log</button>
|
| 92 |
+
<div id="eventLog" class="event-log"></div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div class="test-section">
|
| 96 |
+
<h2>🔄 Real-time Updates Test</h2>
|
| 97 |
+
<button type="button" onclick="simulateDocumentUpload()">Simulate Document Upload</button>
|
| 98 |
+
<button type="button" onclick="simulateDocumentUpdate()">Simulate Document Update</button>
|
| 99 |
+
<button type="button" onclick="simulateDocumentDelete()">Simulate Document Delete</button>
|
| 100 |
+
<div id="realtimeTestResult"></div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
<script src="../js/api-client.js"></script>
|
| 104 |
+
<script src="../js/core.js"></script>
|
| 105 |
+
<script>
|
| 106 |
+
let eventLog = [];
|
| 107 |
+
|
| 108 |
+
function logEvent(message, type = 'info') {
|
| 109 |
+
const timestamp = new Date().toLocaleTimeString();
|
| 110 |
+
const logEntry = `[${timestamp}] ${message}`;
|
| 111 |
+
eventLog.push({ message: logEntry, type });
|
| 112 |
+
|
| 113 |
+
const eventLogElement = document.getElementById('eventLog');
|
| 114 |
+
eventLogElement.innerHTML = eventLog.map(entry =>
|
| 115 |
+
`<div class="${entry.type}">${entry.message}</div>`
|
| 116 |
+
).join('');
|
| 117 |
+
|
| 118 |
+
eventLogElement.scrollTop = eventLogElement.scrollHeight;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function clearEventLog() {
|
| 122 |
+
eventLog = [];
|
| 123 |
+
document.getElementById('eventLog').innerHTML = '';
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
async function testCoreModule() {
|
| 127 |
+
const resultDiv = document.getElementById('coreTestResult');
|
| 128 |
+
resultDiv.innerHTML = '<p>Testing core module...</p>';
|
| 129 |
+
|
| 130 |
+
try {
|
| 131 |
+
// Test if core module is loaded
|
| 132 |
+
if (typeof dashboardCore === 'undefined') {
|
| 133 |
+
throw new Error('Dashboard Core module not loaded');
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
// Test initialization
|
| 137 |
+
if (!dashboardCore.isInitialized) {
|
| 138 |
+
throw new Error('Dashboard Core not initialized');
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Test API client
|
| 142 |
+
if (!dashboardCore.apiClient) {
|
| 143 |
+
throw new Error('API client not initialized');
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// Test event system
|
| 147 |
+
let eventReceived = false;
|
| 148 |
+
const unsubscribe = dashboardCore.listen('testEvent', (data) => {
|
| 149 |
+
eventReceived = true;
|
| 150 |
+
logEvent('✅ Test event received: ' + JSON.stringify(data), 'success');
|
| 151 |
+
});
|
| 152 |
+
|
| 153 |
+
dashboardCore.broadcast('testEvent', { test: true, timestamp: Date.now() });
|
| 154 |
+
|
| 155 |
+
setTimeout(() => {
|
| 156 |
+
unsubscribe();
|
| 157 |
+
if (eventReceived) {
|
| 158 |
+
resultDiv.innerHTML = `
|
| 159 |
+
<div class="success">
|
| 160 |
+
<span class="status-indicator success"></span>
|
| 161 |
+
✅ Core module working correctly
|
| 162 |
+
<ul>
|
| 163 |
+
<li>Module loaded: ✅</li>
|
| 164 |
+
<li>Initialized: ✅</li>
|
| 165 |
+
<li>API client: ✅</li>
|
| 166 |
+
<li>Event system: ✅</li>
|
| 167 |
+
</ul>
|
| 168 |
+
</div>
|
| 169 |
+
`;
|
| 170 |
+
} else {
|
| 171 |
+
throw new Error('Event system not working');
|
| 172 |
+
}
|
| 173 |
+
}, 100);
|
| 174 |
+
|
| 175 |
+
} catch (error) {
|
| 176 |
+
resultDiv.innerHTML = `
|
| 177 |
+
<div class="error">
|
| 178 |
+
<span class="status-indicator error"></span>
|
| 179 |
+
❌ Core module test failed: ${error.message}
|
| 180 |
+
</div>
|
| 181 |
+
`;
|
| 182 |
+
logEvent('❌ Core module test failed: ' + error.message, 'error');
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
async function testAPIConnectivity() {
|
| 187 |
+
const resultDiv = document.getElementById('apiTestResult');
|
| 188 |
+
resultDiv.innerHTML = '<p>Testing API connectivity...</p>';
|
| 189 |
+
|
| 190 |
+
const endpoints = [
|
| 191 |
+
'/api/health',
|
| 192 |
+
'/api/dashboard/summary',
|
| 193 |
+
'/api/documents',
|
| 194 |
+
'/api/ocr/status'
|
| 195 |
+
];
|
| 196 |
+
|
| 197 |
+
const results = [];
|
| 198 |
+
|
| 199 |
+
for (const endpoint of endpoints) {
|
| 200 |
+
try {
|
| 201 |
+
const response = await fetch(endpoint);
|
| 202 |
+
const success = response.ok;
|
| 203 |
+
results.push({
|
| 204 |
+
endpoint,
|
| 205 |
+
success,
|
| 206 |
+
status: response.status,
|
| 207 |
+
statusText: response.statusText
|
| 208 |
+
});
|
| 209 |
+
|
| 210 |
+
logEvent(`${success ? '✅' : '❌'} ${endpoint}: ${response.status}`, success ? 'success' : 'error');
|
| 211 |
+
} catch (error) {
|
| 212 |
+
results.push({
|
| 213 |
+
endpoint,
|
| 214 |
+
success: false,
|
| 215 |
+
error: error.message
|
| 216 |
+
});
|
| 217 |
+
logEvent(`❌ ${endpoint}: ${error.message}`, 'error');
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
const successCount = results.filter(r => r.success).length;
|
| 222 |
+
const totalCount = results.length;
|
| 223 |
+
const successRate = Math.round((successCount / totalCount) * 100);
|
| 224 |
+
|
| 225 |
+
resultDiv.innerHTML = `
|
| 226 |
+
<div class="${successRate >= 75 ? 'success' : successRate >= 50 ? 'warning' : 'error'}">
|
| 227 |
+
<span class="status-indicator ${successRate >= 75 ? 'success' : successRate >= 50 ? 'warning' : 'error'}"></span>
|
| 228 |
+
API Connectivity: ${successCount}/${totalCount} (${successRate}%)
|
| 229 |
+
<ul>
|
| 230 |
+
${results.map(r => `
|
| 231 |
+
<li class="${r.success ? 'success' : 'error'}">
|
| 232 |
+
${r.success ? '✅' : '❌'} ${r.endpoint}: ${r.status || r.error}
|
| 233 |
+
</li>
|
| 234 |
+
`).join('')}
|
| 235 |
+
</ul>
|
| 236 |
+
</div>
|
| 237 |
+
`;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
function testCrossPageCommunication() {
|
| 241 |
+
const resultDiv = document.getElementById('communicationTestResult');
|
| 242 |
+
resultDiv.innerHTML = '<p>Testing cross-page communication...</p>';
|
| 243 |
+
|
| 244 |
+
try {
|
| 245 |
+
// Test localStorage synchronization
|
| 246 |
+
const testData = { test: true, timestamp: Date.now() };
|
| 247 |
+
dashboardCore.storeEvent('testStorageEvent', testData);
|
| 248 |
+
|
| 249 |
+
// Verify event was stored
|
| 250 |
+
const events = JSON.parse(localStorage.getItem('dashboard_events') || '[]');
|
| 251 |
+
const lastEvent = events[events.length - 1];
|
| 252 |
+
|
| 253 |
+
if (lastEvent && lastEvent.name === 'testStorageEvent') {
|
| 254 |
+
logEvent('✅ localStorage synchronization working', 'success');
|
| 255 |
+
} else {
|
| 256 |
+
throw new Error('localStorage synchronization failed');
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// Test event broadcasting
|
| 260 |
+
let eventReceived = false;
|
| 261 |
+
const unsubscribe = dashboardCore.listen('testCommunicationEvent', (data) => {
|
| 262 |
+
eventReceived = true;
|
| 263 |
+
logEvent('✅ Cross-page event received: ' + JSON.stringify(data), 'success');
|
| 264 |
+
});
|
| 265 |
+
|
| 266 |
+
dashboardCore.broadcast('testCommunicationEvent', {
|
| 267 |
+
message: 'Test cross-page communication',
|
| 268 |
+
timestamp: Date.now()
|
| 269 |
+
});
|
| 270 |
+
|
| 271 |
+
setTimeout(() => {
|
| 272 |
+
unsubscribe();
|
| 273 |
+
if (eventReceived) {
|
| 274 |
+
resultDiv.innerHTML = `
|
| 275 |
+
<div class="success">
|
| 276 |
+
<span class="status-indicator success"></span>
|
| 277 |
+
✅ Cross-page communication working
|
| 278 |
+
<ul>
|
| 279 |
+
<li>Event broadcasting: ✅</li>
|
| 280 |
+
<li>Event listening: ✅</li>
|
| 281 |
+
<li>localStorage sync: ✅</li>
|
| 282 |
+
</ul>
|
| 283 |
+
</div>
|
| 284 |
+
`;
|
| 285 |
+
} else {
|
| 286 |
+
throw new Error('Event communication failed');
|
| 287 |
+
}
|
| 288 |
+
}, 100);
|
| 289 |
+
|
| 290 |
+
} catch (error) {
|
| 291 |
+
resultDiv.innerHTML = `
|
| 292 |
+
<div class="error">
|
| 293 |
+
<span class="status-indicator error"></span>
|
| 294 |
+
❌ Cross-page communication test failed: ${error.message}
|
| 295 |
+
</div>
|
| 296 |
+
`;
|
| 297 |
+
logEvent('❌ Cross-page communication test failed: ' + error.message, 'error');
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
function simulateDocumentUpload() {
|
| 302 |
+
const testData = {
|
| 303 |
+
fileId: 'test_' + Date.now(),
|
| 304 |
+
fileName: 'test_document.pdf',
|
| 305 |
+
fileSize: 1024000,
|
| 306 |
+
status: 'uploaded'
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
dashboardCore.broadcast('documentUploaded', testData);
|
| 310 |
+
logEvent('📄 Simulated document upload: ' + testData.fileName, 'info');
|
| 311 |
+
|
| 312 |
+
document.getElementById('realtimeTestResult').innerHTML = `
|
| 313 |
+
<div class="success">
|
| 314 |
+
✅ Document upload event broadcasted
|
| 315 |
+
<pre>${JSON.stringify(testData, null, 2)}</pre>
|
| 316 |
+
</div>
|
| 317 |
+
`;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
function simulateDocumentUpdate() {
|
| 321 |
+
const testData = {
|
| 322 |
+
documentId: 'doc_' + Date.now(),
|
| 323 |
+
fileName: 'updated_document.pdf',
|
| 324 |
+
status: 'updated',
|
| 325 |
+
updatedAt: new Date().toISOString()
|
| 326 |
+
};
|
| 327 |
+
|
| 328 |
+
dashboardCore.broadcast('documentUpdated', testData);
|
| 329 |
+
logEvent('📝 Simulated document update: ' + testData.fileName, 'info');
|
| 330 |
+
|
| 331 |
+
document.getElementById('realtimeTestResult').innerHTML = `
|
| 332 |
+
<div class="success">
|
| 333 |
+
✅ Document update event broadcasted
|
| 334 |
+
<pre>${JSON.stringify(testData, null, 2)}</pre>
|
| 335 |
+
</div>
|
| 336 |
+
`;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
function simulateDocumentDelete() {
|
| 340 |
+
const testData = {
|
| 341 |
+
documentId: 'doc_' + Date.now(),
|
| 342 |
+
fileName: 'deleted_document.pdf',
|
| 343 |
+
status: 'deleted'
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
dashboardCore.broadcast('documentDeleted', testData);
|
| 347 |
+
logEvent('🗑️ Simulated document delete: ' + testData.fileName, 'info');
|
| 348 |
+
|
| 349 |
+
document.getElementById('realtimeTestResult').innerHTML = `
|
| 350 |
+
<div class="success">
|
| 351 |
+
✅ Document delete event broadcasted
|
| 352 |
+
<pre>${JSON.stringify(testData, null, 2)}</pre>
|
| 353 |
+
</div>
|
| 354 |
+
`;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// Listen for all dashboard events
|
| 358 |
+
dashboardCore.listen('documentUploaded', (data) => {
|
| 359 |
+
logEvent('📄 Document upload event received: ' + data.fileName, 'success');
|
| 360 |
+
});
|
| 361 |
+
|
| 362 |
+
dashboardCore.listen('documentUpdated', (data) => {
|
| 363 |
+
logEvent('📝 Document update event received: ' + data.fileName, 'success');
|
| 364 |
+
});
|
| 365 |
+
|
| 366 |
+
dashboardCore.listen('documentDeleted', (data) => {
|
| 367 |
+
logEvent('🗑️ Document delete event received: ' + data.fileName, 'success');
|
| 368 |
+
});
|
| 369 |
+
|
| 370 |
+
dashboardCore.listen('healthUpdate', (data) => {
|
| 371 |
+
logEvent('💓 Health update: ' + data.status, 'info');
|
| 372 |
+
});
|
| 373 |
+
|
| 374 |
+
dashboardCore.listen('dashboardStatsUpdated', (data) => {
|
| 375 |
+
logEvent('📊 Dashboard stats updated', 'info');
|
| 376 |
+
});
|
| 377 |
+
|
| 378 |
+
// Initialize test page
|
| 379 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 380 |
+
logEvent('🚀 Integration test page loaded', 'info');
|
| 381 |
+
logEvent('📦 Dashboard Core module: ' + (typeof dashboardCore !== 'undefined' ? 'Loaded' : 'Not loaded'), 'info');
|
| 382 |
+
});
|
| 383 |
+
</script>
|
| 384 |
+
</body>
|
| 385 |
+
</html>
|
app/static/dev/real-api-test.html
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Real API Testing - Legal Dashboard</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Arial', sans-serif;
|
| 10 |
+
max-width: 1400px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.test-section {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 20px;
|
| 18 |
+
margin: 20px 0;
|
| 19 |
+
border-radius: 8px;
|
| 20 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 21 |
+
}
|
| 22 |
+
.success { color: #10b981; }
|
| 23 |
+
.error { color: #ef4444; }
|
| 24 |
+
.info { color: #3b82f6; }
|
| 25 |
+
.warning { color: #f59e0b; }
|
| 26 |
+
button {
|
| 27 |
+
background: #007bff;
|
| 28 |
+
color: white;
|
| 29 |
+
border: none;
|
| 30 |
+
padding: 10px 20px;
|
| 31 |
+
border-radius: 4px;
|
| 32 |
+
cursor: pointer;
|
| 33 |
+
margin: 5px;
|
| 34 |
+
}
|
| 35 |
+
button:hover {
|
| 36 |
+
background: #0056b3;
|
| 37 |
+
}
|
| 38 |
+
button:disabled {
|
| 39 |
+
background: #ccc;
|
| 40 |
+
cursor: not-allowed;
|
| 41 |
+
}
|
| 42 |
+
.endpoint-grid {
|
| 43 |
+
display: grid;
|
| 44 |
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
| 45 |
+
gap: 15px;
|
| 46 |
+
margin-top: 20px;
|
| 47 |
+
}
|
| 48 |
+
.endpoint-card {
|
| 49 |
+
border: 1px solid #ddd;
|
| 50 |
+
border-radius: 8px;
|
| 51 |
+
padding: 15px;
|
| 52 |
+
background: white;
|
| 53 |
+
position: relative;
|
| 54 |
+
}
|
| 55 |
+
.endpoint-card.success {
|
| 56 |
+
border-color: #10b981;
|
| 57 |
+
background: #f0fdf4;
|
| 58 |
+
}
|
| 59 |
+
.endpoint-card.error {
|
| 60 |
+
border-color: #ef4444;
|
| 61 |
+
background: #fef2f2;
|
| 62 |
+
}
|
| 63 |
+
.endpoint-card.warning {
|
| 64 |
+
border-color: #f59e0b;
|
| 65 |
+
background: #fffbeb;
|
| 66 |
+
}
|
| 67 |
+
.endpoint-card.testing {
|
| 68 |
+
border-color: #3b82f6;
|
| 69 |
+
background: #eff6ff;
|
| 70 |
+
}
|
| 71 |
+
.status-indicator {
|
| 72 |
+
display: inline-block;
|
| 73 |
+
width: 12px;
|
| 74 |
+
height: 12px;
|
| 75 |
+
border-radius: 50%;
|
| 76 |
+
margin-right: 8px;
|
| 77 |
+
}
|
| 78 |
+
.status-indicator.success { background: #10b981; }
|
| 79 |
+
.status-indicator.error { background: #ef4444; }
|
| 80 |
+
.status-indicator.warning { background: #f59e0b; }
|
| 81 |
+
.status-indicator.info { background: #3b82f6; }
|
| 82 |
+
.status-indicator.testing {
|
| 83 |
+
background: #3b82f6;
|
| 84 |
+
animation: pulse 1s infinite;
|
| 85 |
+
}
|
| 86 |
+
@keyframes pulse {
|
| 87 |
+
0% { opacity: 1; }
|
| 88 |
+
50% { opacity: 0.5; }
|
| 89 |
+
100% { opacity: 1; }
|
| 90 |
+
}
|
| 91 |
+
.response-data {
|
| 92 |
+
background: #f8f9fa;
|
| 93 |
+
padding: 10px;
|
| 94 |
+
border-radius: 4px;
|
| 95 |
+
margin-top: 10px;
|
| 96 |
+
font-family: 'Courier New', monospace;
|
| 97 |
+
font-size: 12px;
|
| 98 |
+
max-height: 200px;
|
| 99 |
+
overflow-y: auto;
|
| 100 |
+
white-space: pre-wrap;
|
| 101 |
+
}
|
| 102 |
+
.test-controls {
|
| 103 |
+
display: flex;
|
| 104 |
+
gap: 10px;
|
| 105 |
+
margin-bottom: 20px;
|
| 106 |
+
flex-wrap: wrap;
|
| 107 |
+
}
|
| 108 |
+
.summary-stats {
|
| 109 |
+
display: grid;
|
| 110 |
+
grid-template-columns: repeat(4, 1fr);
|
| 111 |
+
gap: 15px;
|
| 112 |
+
margin-bottom: 20px;
|
| 113 |
+
}
|
| 114 |
+
.stat-card {
|
| 115 |
+
background: white;
|
| 116 |
+
padding: 15px;
|
| 117 |
+
border-radius: 8px;
|
| 118 |
+
text-align: center;
|
| 119 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 120 |
+
}
|
| 121 |
+
.stat-number {
|
| 122 |
+
font-size: 2rem;
|
| 123 |
+
font-weight: bold;
|
| 124 |
+
margin-bottom: 5px;
|
| 125 |
+
}
|
| 126 |
+
.stat-label {
|
| 127 |
+
color: #666;
|
| 128 |
+
font-size: 0.9rem;
|
| 129 |
+
}
|
| 130 |
+
.progress-bar {
|
| 131 |
+
width: 100%;
|
| 132 |
+
height: 4px;
|
| 133 |
+
background: #e5e7eb;
|
| 134 |
+
border-radius: 2px;
|
| 135 |
+
overflow: hidden;
|
| 136 |
+
margin: 10px 0;
|
| 137 |
+
}
|
| 138 |
+
.progress-fill {
|
| 139 |
+
height: 100%;
|
| 140 |
+
background: #3b82f6;
|
| 141 |
+
transition: width 0.3s ease;
|
| 142 |
+
}
|
| 143 |
+
.file-upload-test {
|
| 144 |
+
border: 2px dashed #ddd;
|
| 145 |
+
padding: 20px;
|
| 146 |
+
text-align: center;
|
| 147 |
+
border-radius: 8px;
|
| 148 |
+
margin: 20px 0;
|
| 149 |
+
}
|
| 150 |
+
.file-upload-test.dragover {
|
| 151 |
+
border-color: #3b82f6;
|
| 152 |
+
background: #eff6ff;
|
| 153 |
+
}
|
| 154 |
+
.test-results {
|
| 155 |
+
max-height: 400px;
|
| 156 |
+
overflow-y: auto;
|
| 157 |
+
border: 1px solid #ddd;
|
| 158 |
+
border-radius: 4px;
|
| 159 |
+
padding: 10px;
|
| 160 |
+
background: #f8f9fa;
|
| 161 |
+
}
|
| 162 |
+
</style>
|
| 163 |
+
</head>
|
| 164 |
+
<body>
|
| 165 |
+
<h1>🔍 Real API Testing - Legal Dashboard</h1>
|
| 166 |
+
|
| 167 |
+
<div class="test-section">
|
| 168 |
+
<h2>📊 Test Summary</h2>
|
| 169 |
+
<div class="summary-stats">
|
| 170 |
+
<div class="stat-card">
|
| 171 |
+
<div class="stat-number" id="totalTests">0</div>
|
| 172 |
+
<div class="stat-label">Total Tests</div>
|
| 173 |
+
</div>
|
| 174 |
+
<div class="stat-card">
|
| 175 |
+
<div class="stat-number" id="passedTests">0</div>
|
| 176 |
+
<div class="stat-label">Passed</div>
|
| 177 |
+
</div>
|
| 178 |
+
<div class="stat-card">
|
| 179 |
+
<div class="stat-number" id="failedTests">0</div>
|
| 180 |
+
<div class="stat-label">Failed</div>
|
| 181 |
+
</div>
|
| 182 |
+
<div class="stat-card">
|
| 183 |
+
<div class="stat-number" id="successRate">0%</div>
|
| 184 |
+
<div class="stat-label">Success Rate</div>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
<div class="progress-bar">
|
| 188 |
+
<div class="progress-fill" id="progressBar"></div>
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
|
| 192 |
+
<div class="test-section">
|
| 193 |
+
<h2>🎛️ Test Controls</h2>
|
| 194 |
+
<div class="test-controls">
|
| 195 |
+
<button type="button" onclick="runAllTests()" id="runAllBtn">Run All Tests</button>
|
| 196 |
+
<button type="button" onclick="runHealthTests()">Health Tests Only</button>
|
| 197 |
+
<button type="button" onclick="runDashboardTests()">Dashboard Tests</button>
|
| 198 |
+
<button type="button" onclick="runDocumentTests()">Document Tests</button>
|
| 199 |
+
<button type="button" onclick="runOCRTests()">OCR Tests</button>
|
| 200 |
+
<button type="button" onclick="runScrapingTests()">Scraping Tests</button>
|
| 201 |
+
<button type="button" onclick="clearResults()">Clear Results</button>
|
| 202 |
+
<button type="button" onclick="exportResults()">Export Results</button>
|
| 203 |
+
</div>
|
| 204 |
+
</div>
|
| 205 |
+
|
| 206 |
+
<div class="test-section">
|
| 207 |
+
<h2>📁 File Upload Test</h2>
|
| 208 |
+
<div class="file-upload-test" id="uploadZone">
|
| 209 |
+
<p>Drag and drop a file here or click to select</p>
|
| 210 |
+
<input type="file" id="testFileInput" accept=".pdf,.jpg,.jpeg,.png,.tiff" style="display: none;">
|
| 211 |
+
<button type="button" onclick="document.getElementById('testFileInput').click()">Select File</button>
|
| 212 |
+
</div>
|
| 213 |
+
<div id="uploadResults"></div>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<div class="test-section">
|
| 217 |
+
<h2>🔌 API Endpoint Tests</h2>
|
| 218 |
+
<div class="endpoint-grid" id="endpointGrid">
|
| 219 |
+
<!-- Endpoint cards will be generated here -->
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
<div class="test-section">
|
| 224 |
+
<h2>📋 Test Results</h2>
|
| 225 |
+
<div class="test-results" id="testResults">
|
| 226 |
+
<!-- Test results will be displayed here -->
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<script src="../js/api-client.js"></script>
|
| 231 |
+
<script>
|
| 232 |
+
class RealAPITester {
|
| 233 |
+
constructor() {
|
| 234 |
+
this.baseURL = window.location.origin;
|
| 235 |
+
this.results = [];
|
| 236 |
+
this.testStats = {
|
| 237 |
+
total: 0,
|
| 238 |
+
passed: 0,
|
| 239 |
+
failed: 0,
|
| 240 |
+
successRate: 0
|
| 241 |
+
};
|
| 242 |
+
this.isRunning = false;
|
| 243 |
+
|
| 244 |
+
this.endpoints = [
|
| 245 |
+
// Health & System Tests
|
| 246 |
+
{ name: 'Health Check', url: '/api/health', method: 'GET', category: 'Health', expectedStatus: 200 },
|
| 247 |
+
{ name: 'API Docs', url: '/api/docs', method: 'GET', category: 'Health', expectedStatus: 200 },
|
| 248 |
+
|
| 249 |
+
// Dashboard Tests
|
| 250 |
+
{ name: 'Dashboard Summary', url: '/api/dashboard/summary', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
|
| 251 |
+
{ name: 'Charts Data', url: '/api/dashboard/charts-data', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
|
| 252 |
+
{ name: 'AI Suggestions', url: '/api/dashboard/ai-suggestions', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
|
| 253 |
+
{ name: 'Performance Metrics', url: '/api/dashboard/performance-metrics', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
|
| 254 |
+
{ name: 'Trends', url: '/api/dashboard/trends', method: 'GET', category: 'Dashboard', expectedStatus: 200 },
|
| 255 |
+
|
| 256 |
+
// Documents Tests
|
| 257 |
+
{ name: 'Documents List', url: '/api/documents', method: 'GET', category: 'Documents', expectedStatus: 200 },
|
| 258 |
+
{ name: 'Document Categories', url: '/api/documents/categories', method: 'GET', category: 'Documents', expectedStatus: 200 },
|
| 259 |
+
{ name: 'Document Sources', url: '/api/documents/sources', method: 'GET', category: 'Documents', expectedStatus: 200 },
|
| 260 |
+
{ name: 'Document Search', url: '/api/documents/search?q=test', method: 'GET', category: 'Documents', expectedStatus: 200 },
|
| 261 |
+
|
| 262 |
+
// OCR Tests
|
| 263 |
+
{ name: 'OCR Status', url: '/api/ocr/status', method: 'GET', category: 'OCR', expectedStatus: 200 },
|
| 264 |
+
{ name: 'OCR Models', url: '/api/ocr/models', method: 'GET', category: 'OCR', expectedStatus: 200 },
|
| 265 |
+
|
| 266 |
+
// Scraping Tests
|
| 267 |
+
{ name: 'Scraping Statistics', url: '/api/scraping/scrape/statistics', method: 'GET', category: 'Scraping', expectedStatus: 200 },
|
| 268 |
+
{ name: 'Scraping Status', url: '/api/scraping/scrape/status', method: 'GET', category: 'Scraping', expectedStatus: 200 },
|
| 269 |
+
{ name: 'Rating Summary', url: '/api/scraping/rating/summary', method: 'GET', category: 'Scraping', expectedStatus: 200 },
|
| 270 |
+
{ name: 'Scraping Health', url: '/api/scraping/health', method: 'GET', category: 'Scraping', expectedStatus: 200 },
|
| 271 |
+
|
| 272 |
+
// Analytics Tests (Expected to fail)
|
| 273 |
+
{ name: 'Analytics Overview', url: '/api/analytics/overview', method: 'GET', category: 'Analytics', expectedStatus: 404 },
|
| 274 |
+
{ name: 'Analytics Performance', url: '/api/analytics/performance', method: 'GET', category: 'Analytics', expectedStatus: 404 },
|
| 275 |
+
{ name: 'Analytics Entities', url: '/api/analytics/entities', method: 'GET', category: 'Analytics', expectedStatus: 404 },
|
| 276 |
+
{ name: 'Analytics Quality', url: '/api/analytics/quality-analysis', method: 'GET', category: 'Analytics', expectedStatus: 404 }
|
| 277 |
+
];
|
| 278 |
+
|
| 279 |
+
this.initialize();
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
initialize() {
|
| 283 |
+
this.createEndpointCards();
|
| 284 |
+
this.setupFileUpload();
|
| 285 |
+
this.updateStats();
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
createEndpointCards() {
|
| 289 |
+
const grid = document.getElementById('endpointGrid');
|
| 290 |
+
grid.innerHTML = '';
|
| 291 |
+
|
| 292 |
+
this.endpoints.forEach((endpoint, index) => {
|
| 293 |
+
const card = document.createElement('div');
|
| 294 |
+
card.className = 'endpoint-card';
|
| 295 |
+
card.id = `endpoint-${index}`;
|
| 296 |
+
|
| 297 |
+
card.innerHTML = `
|
| 298 |
+
<div class="status-indicator"></div>
|
| 299 |
+
<strong>${endpoint.name}</strong>
|
| 300 |
+
<div style="font-size: 0.8rem; color: #666; margin: 5px 0;">
|
| 301 |
+
${endpoint.method} ${endpoint.url}
|
| 302 |
+
</div>
|
| 303 |
+
<div class="response-data" id="response-${index}" style="display: none;"></div>
|
| 304 |
+
<button onclick="tester.testSingleEndpoint(${index})" class="test-btn">
|
| 305 |
+
Test
|
| 306 |
+
</button>
|
| 307 |
+
`;
|
| 308 |
+
|
| 309 |
+
grid.appendChild(card);
|
| 310 |
+
});
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
setupFileUpload() {
|
| 314 |
+
const uploadZone = document.getElementById('uploadZone');
|
| 315 |
+
const fileInput = document.getElementById('testFileInput');
|
| 316 |
+
|
| 317 |
+
uploadZone.addEventListener('dragover', (e) => {
|
| 318 |
+
e.preventDefault();
|
| 319 |
+
uploadZone.classList.add('dragover');
|
| 320 |
+
});
|
| 321 |
+
|
| 322 |
+
uploadZone.addEventListener('dragleave', () => {
|
| 323 |
+
uploadZone.classList.remove('dragover');
|
| 324 |
+
});
|
| 325 |
+
|
| 326 |
+
uploadZone.addEventListener('drop', (e) => {
|
| 327 |
+
e.preventDefault();
|
| 328 |
+
uploadZone.classList.remove('dragover');
|
| 329 |
+
const files = e.dataTransfer.files;
|
| 330 |
+
if (files.length > 0) {
|
| 331 |
+
this.testFileUpload(files[0]);
|
| 332 |
+
}
|
| 333 |
+
});
|
| 334 |
+
|
| 335 |
+
fileInput.addEventListener('change', (e) => {
|
| 336 |
+
if (e.target.files.length > 0) {
|
| 337 |
+
this.testFileUpload(e.target.files[0]);
|
| 338 |
+
}
|
| 339 |
+
});
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
async testSingleEndpoint(index) {
|
| 343 |
+
const endpoint = this.endpoints[index];
|
| 344 |
+
const card = document.getElementById(`endpoint-${index}`);
|
| 345 |
+
const responseDiv = document.getElementById(`response-${index}`);
|
| 346 |
+
|
| 347 |
+
// Set testing state
|
| 348 |
+
card.className = 'endpoint-card testing';
|
| 349 |
+
card.querySelector('.status-indicator').className = 'status-indicator testing';
|
| 350 |
+
card.querySelector('.test-btn').disabled = true;
|
| 351 |
+
|
| 352 |
+
const startTime = Date.now();
|
| 353 |
+
|
| 354 |
+
try {
|
| 355 |
+
const response = await fetch(`${this.baseURL}${endpoint.url}`, {
|
| 356 |
+
method: endpoint.method,
|
| 357 |
+
headers: {
|
| 358 |
+
'Content-Type': 'application/json'
|
| 359 |
+
}
|
| 360 |
+
});
|
| 361 |
+
|
| 362 |
+
const responseTime = Date.now() - startTime;
|
| 363 |
+
const responseText = await response.text();
|
| 364 |
+
let responseData;
|
| 365 |
+
|
| 366 |
+
try {
|
| 367 |
+
responseData = JSON.parse(responseText);
|
| 368 |
+
} catch {
|
| 369 |
+
responseData = responseText;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
const success = response.status === endpoint.expectedStatus;
|
| 373 |
+
|
| 374 |
+
// Update card
|
| 375 |
+
card.className = `endpoint-card ${success ? 'success' : 'error'}`;
|
| 376 |
+
card.querySelector('.status-indicator').className = `status-indicator ${success ? 'success' : 'error'}`;
|
| 377 |
+
card.querySelector('.test-btn').disabled = false;
|
| 378 |
+
|
| 379 |
+
// Show response data
|
| 380 |
+
responseDiv.style.display = 'block';
|
| 381 |
+
responseDiv.innerHTML = `
|
| 382 |
+
Status: ${response.status} ${response.statusText}
|
| 383 |
+
Time: ${responseTime}ms
|
| 384 |
+
Size: ${responseText.length} bytes
|
| 385 |
+
|
| 386 |
+
Response:
|
| 387 |
+
${JSON.stringify(responseData, null, 2)}
|
| 388 |
+
`;
|
| 389 |
+
|
| 390 |
+
// Log result
|
| 391 |
+
this.logResult({
|
| 392 |
+
endpoint: endpoint.name,
|
| 393 |
+
url: endpoint.url,
|
| 394 |
+
method: endpoint.method,
|
| 395 |
+
status: response.status,
|
| 396 |
+
expectedStatus: endpoint.expectedStatus,
|
| 397 |
+
success: success,
|
| 398 |
+
responseTime: responseTime,
|
| 399 |
+
responseSize: responseText.length,
|
| 400 |
+
responseData: responseData
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
} catch (error) {
|
| 404 |
+
card.className = 'endpoint-card error';
|
| 405 |
+
card.querySelector('.status-indicator').className = 'status-indicator error';
|
| 406 |
+
card.querySelector('.test-btn').disabled = false;
|
| 407 |
+
|
| 408 |
+
responseDiv.style.display = 'block';
|
| 409 |
+
responseDiv.innerHTML = `Error: ${error.message}`;
|
| 410 |
+
|
| 411 |
+
this.logResult({
|
| 412 |
+
endpoint: endpoint.name,
|
| 413 |
+
url: endpoint.url,
|
| 414 |
+
method: endpoint.method,
|
| 415 |
+
status: 0,
|
| 416 |
+
expectedStatus: endpoint.expectedStatus,
|
| 417 |
+
success: false,
|
| 418 |
+
error: error.message
|
| 419 |
+
});
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
this.updateStats();
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
async testFileUpload(file) {
|
| 426 |
+
const resultsDiv = document.getElementById('uploadResults');
|
| 427 |
+
resultsDiv.innerHTML = `<p>Testing file upload: ${file.name} (${file.size} bytes)</p>`;
|
| 428 |
+
|
| 429 |
+
try {
|
| 430 |
+
const formData = new FormData();
|
| 431 |
+
formData.append('file', file);
|
| 432 |
+
|
| 433 |
+
const startTime = Date.now();
|
| 434 |
+
const response = await fetch(`${this.baseURL}/api/ocr/upload`, {
|
| 435 |
+
method: 'POST',
|
| 436 |
+
body: formData
|
| 437 |
+
});
|
| 438 |
+
|
| 439 |
+
const responseTime = Date.now() - startTime;
|
| 440 |
+
const responseData = await response.json();
|
| 441 |
+
|
| 442 |
+
const success = response.ok;
|
| 443 |
+
|
| 444 |
+
resultsDiv.innerHTML = `
|
| 445 |
+
<div class="${success ? 'success' : 'error'}">
|
| 446 |
+
<h4>File Upload Test Results</h4>
|
| 447 |
+
<p><strong>File:</strong> ${file.name}</p>
|
| 448 |
+
<p><strong>Size:</strong> ${file.size} bytes</p>
|
| 449 |
+
<p><strong>Status:</strong> ${response.status} ${response.statusText}</p>
|
| 450 |
+
<p><strong>Response Time:</strong> ${responseTime}ms</p>
|
| 451 |
+
<div class="response-data">
|
| 452 |
+
${JSON.stringify(responseData, null, 2)}
|
| 453 |
+
</div>
|
| 454 |
+
</div>
|
| 455 |
+
`;
|
| 456 |
+
|
| 457 |
+
this.logResult({
|
| 458 |
+
endpoint: 'File Upload',
|
| 459 |
+
url: '/api/ocr/upload',
|
| 460 |
+
method: 'POST',
|
| 461 |
+
status: response.status,
|
| 462 |
+
success: success,
|
| 463 |
+
responseTime: responseTime,
|
| 464 |
+
fileSize: file.size,
|
| 465 |
+
fileName: file.name,
|
| 466 |
+
responseData: responseData
|
| 467 |
+
});
|
| 468 |
+
|
| 469 |
+
} catch (error) {
|
| 470 |
+
resultsDiv.innerHTML = `
|
| 471 |
+
<div class="error">
|
| 472 |
+
<h4>File Upload Test Failed</h4>
|
| 473 |
+
<p>Error: ${error.message}</p>
|
| 474 |
+
</div>
|
| 475 |
+
`;
|
| 476 |
+
|
| 477 |
+
this.logResult({
|
| 478 |
+
endpoint: 'File Upload',
|
| 479 |
+
url: '/api/ocr/upload',
|
| 480 |
+
method: 'POST',
|
| 481 |
+
status: 0,
|
| 482 |
+
success: false,
|
| 483 |
+
error: error.message,
|
| 484 |
+
fileName: file.name
|
| 485 |
+
});
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
this.updateStats();
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
async runAllTests() {
|
| 492 |
+
if (this.isRunning) return;
|
| 493 |
+
|
| 494 |
+
this.isRunning = true;
|
| 495 |
+
document.getElementById('runAllBtn').disabled = true;
|
| 496 |
+
document.getElementById('runAllBtn').textContent = 'Running...';
|
| 497 |
+
|
| 498 |
+
this.clearResults();
|
| 499 |
+
|
| 500 |
+
for (let i = 0; i < this.endpoints.length; i++) {
|
| 501 |
+
await this.testSingleEndpoint(i);
|
| 502 |
+
await this.delay(100); // Small delay between tests
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
this.isRunning = false;
|
| 506 |
+
document.getElementById('runAllBtn').disabled = false;
|
| 507 |
+
document.getElementById('runAllBtn').textContent = 'Run All Tests';
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
async runHealthTests() {
|
| 511 |
+
const healthEndpoints = this.endpoints.filter(e => e.category === 'Health');
|
| 512 |
+
for (let i = 0; i < this.endpoints.length; i++) {
|
| 513 |
+
if (this.endpoints[i].category === 'Health') {
|
| 514 |
+
await this.testSingleEndpoint(i);
|
| 515 |
+
await this.delay(100);
|
| 516 |
+
}
|
| 517 |
+
}
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
async runDashboardTests() {
|
| 521 |
+
const dashboardEndpoints = this.endpoints.filter(e => e.category === 'Dashboard');
|
| 522 |
+
for (let i = 0; i < this.endpoints.length; i++) {
|
| 523 |
+
if (this.endpoints[i].category === 'Dashboard') {
|
| 524 |
+
await this.testSingleEndpoint(i);
|
| 525 |
+
await this.delay(100);
|
| 526 |
+
}
|
| 527 |
+
}
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
async runDocumentTests() {
|
| 531 |
+
const documentEndpoints = this.endpoints.filter(e => e.category === 'Documents');
|
| 532 |
+
for (let i = 0; i < this.endpoints.length; i++) {
|
| 533 |
+
if (this.endpoints[i].category === 'Documents') {
|
| 534 |
+
await this.testSingleEndpoint(i);
|
| 535 |
+
await this.delay(100);
|
| 536 |
+
}
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
async runOCRTests() {
|
| 541 |
+
const ocrEndpoints = this.endpoints.filter(e => e.category === 'OCR');
|
| 542 |
+
for (let i = 0; i < this.endpoints.length; i++) {
|
| 543 |
+
if (this.endpoints[i].category === 'OCR') {
|
| 544 |
+
await this.testSingleEndpoint(i);
|
| 545 |
+
await this.delay(100);
|
| 546 |
+
}
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
async runScrapingTests() {
|
| 551 |
+
const scrapingEndpoints = this.endpoints.filter(e => e.category === 'Scraping');
|
| 552 |
+
for (let i = 0; i < this.endpoints.length; i++) {
|
| 553 |
+
if (this.endpoints[i].category === 'Scraping') {
|
| 554 |
+
await this.testSingleEndpoint(i);
|
| 555 |
+
await this.delay(100);
|
| 556 |
+
}
|
| 557 |
+
}
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
logResult(result) {
|
| 561 |
+
this.results.push({
|
| 562 |
+
...result,
|
| 563 |
+
timestamp: new Date().toISOString()
|
| 564 |
+
});
|
| 565 |
+
|
| 566 |
+
const resultsDiv = document.getElementById('testResults');
|
| 567 |
+
const resultEntry = document.createElement('div');
|
| 568 |
+
resultEntry.className = `test-result ${result.success ? 'success' : 'error'}`;
|
| 569 |
+
resultEntry.innerHTML = `
|
| 570 |
+
<strong>${result.endpoint}</strong> -
|
| 571 |
+
${result.success ? '✅ PASS' : '❌ FAIL'}
|
| 572 |
+
(${result.status || 'ERROR'}) -
|
| 573 |
+
${result.responseTime ? result.responseTime + 'ms' : 'N/A'}
|
| 574 |
+
<br><small>${new Date().toLocaleTimeString()}</small>
|
| 575 |
+
`;
|
| 576 |
+
|
| 577 |
+
resultsDiv.appendChild(resultEntry);
|
| 578 |
+
resultsDiv.scrollTop = resultsDiv.scrollHeight;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
updateStats() {
|
| 582 |
+
const total = this.results.length;
|
| 583 |
+
const passed = this.results.filter(r => r.success).length;
|
| 584 |
+
const failed = total - passed;
|
| 585 |
+
const successRate = total > 0 ? Math.round((passed / total) * 100) : 0;
|
| 586 |
+
|
| 587 |
+
this.testStats = { total, passed, failed, successRate };
|
| 588 |
+
|
| 589 |
+
document.getElementById('totalTests').textContent = total;
|
| 590 |
+
document.getElementById('passedTests').textContent = passed;
|
| 591 |
+
document.getElementById('failedTests').textContent = failed;
|
| 592 |
+
document.getElementById('successRate').textContent = successRate + '%';
|
| 593 |
+
|
| 594 |
+
const progressBar = document.getElementById('progressBar');
|
| 595 |
+
progressBar.style.width = successRate + '%';
|
| 596 |
+
progressBar.style.background = successRate >= 80 ? '#10b981' : successRate >= 60 ? '#f59e0b' : '#ef4444';
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
clearResults() {
|
| 600 |
+
this.results = [];
|
| 601 |
+
document.getElementById('testResults').innerHTML = '';
|
| 602 |
+
this.updateStats();
|
| 603 |
+
|
| 604 |
+
// Reset all endpoint cards
|
| 605 |
+
this.endpoints.forEach((endpoint, index) => {
|
| 606 |
+
const card = document.getElementById(`endpoint-${index}`);
|
| 607 |
+
card.className = 'endpoint-card';
|
| 608 |
+
card.querySelector('.status-indicator').className = 'status-indicator';
|
| 609 |
+
card.querySelector('.test-btn').disabled = false;
|
| 610 |
+
document.getElementById(`response-${index}`).style.display = 'none';
|
| 611 |
+
});
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
exportResults() {
|
| 615 |
+
const data = {
|
| 616 |
+
timestamp: new Date().toISOString(),
|
| 617 |
+
stats: this.testStats,
|
| 618 |
+
results: this.results
|
| 619 |
+
};
|
| 620 |
+
|
| 621 |
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
| 622 |
+
const url = URL.createObjectURL(blob);
|
| 623 |
+
const a = document.createElement('a');
|
| 624 |
+
a.href = url;
|
| 625 |
+
a.download = `api-test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
| 626 |
+
a.click();
|
| 627 |
+
URL.revokeObjectURL(url);
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
delay(ms) {
|
| 631 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
| 632 |
+
}
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
// Global tester instance
|
| 636 |
+
const tester = new RealAPITester();
|
| 637 |
+
|
| 638 |
+
// Global functions for button clicks
|
| 639 |
+
function runAllTests() {
|
| 640 |
+
tester.runAllTests();
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
function runHealthTests() {
|
| 644 |
+
tester.runHealthTests();
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
function runDashboardTests() {
|
| 648 |
+
tester.runDashboardTests();
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
function runDocumentTests() {
|
| 652 |
+
tester.runDocumentTests();
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
function runOCRTests() {
|
| 656 |
+
tester.runOCRTests();
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
function runScrapingTests() {
|
| 660 |
+
tester.runScrapingTests();
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
function clearResults() {
|
| 664 |
+
tester.clearResults();
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
function exportResults() {
|
| 668 |
+
tester.exportResults();
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
console.log('🔍 Real API Tester initialized');
|
| 672 |
+
</script>
|
| 673 |
+
</body>
|
| 674 |
+
</html>
|
app/static/dev/test_integration.html
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fa" dir="rtl">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>تست اتصال فرانتاند و بکاند</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Arial', sans-serif;
|
| 10 |
+
max-width: 800px;
|
| 11 |
+
margin: 0 auto;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
background: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
.test-section {
|
| 16 |
+
background: white;
|
| 17 |
+
padding: 20px;
|
| 18 |
+
margin: 20px 0;
|
| 19 |
+
border-radius: 8px;
|
| 20 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 21 |
+
}
|
| 22 |
+
.success { color: green; }
|
| 23 |
+
.error { color: red; }
|
| 24 |
+
.info { color: blue; }
|
| 25 |
+
button {
|
| 26 |
+
background: #007bff;
|
| 27 |
+
color: white;
|
| 28 |
+
border: none;
|
| 29 |
+
padding: 10px 20px;
|
| 30 |
+
border-radius: 4px;
|
| 31 |
+
cursor: pointer;
|
| 32 |
+
margin: 5px;
|
| 33 |
+
}
|
| 34 |
+
button:hover {
|
| 35 |
+
background: #0056b3;
|
| 36 |
+
}
|
| 37 |
+
pre {
|
| 38 |
+
background: #f8f9fa;
|
| 39 |
+
padding: 10px;
|
| 40 |
+
border-radius: 4px;
|
| 41 |
+
overflow-x: auto;
|
| 42 |
+
}
|
| 43 |
+
</style>
|
| 44 |
+
</head>
|
| 45 |
+
<body>
|
| 46 |
+
<h1>تست اتصال فرانتاند و بکاند</h1>
|
| 47 |
+
|
| 48 |
+
<div class="test-section">
|
| 49 |
+
<h2>تست اتصال API</h2>
|
| 50 |
+
<button onclick="testConnection()">تست اتصال</button>
|
| 51 |
+
<div id="connectionResult"></div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="test-section">
|
| 55 |
+
<h2>تست دریافت آمار داشبورد</h2>
|
| 56 |
+
<button onclick="testDashboardSummary()">دریافت آمار</button>
|
| 57 |
+
<div id="dashboardResult"></div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div class="test-section">
|
| 61 |
+
<h2>تست دریافت اسناد</h2>
|
| 62 |
+
<button onclick="testDocuments()">دریافت اسناد</button>
|
| 63 |
+
<div id="documentsResult"></div>
|
| 64 |
+
</div>
|
| 65 |
+
|
| 66 |
+
<div class="test-section">
|
| 67 |
+
<h2>تست شروع جمعآوری</h2>
|
| 68 |
+
<button onclick="testScraping()">شروع جمعآوری</button>
|
| 69 |
+
<div id="scrapingResult"></div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<script>
|
| 73 |
+
const API_BASE = 'http://localhost:8000';
|
| 74 |
+
|
| 75 |
+
async function testConnection() {
|
| 76 |
+
const resultDiv = document.getElementById('connectionResult');
|
| 77 |
+
resultDiv.innerHTML = '<p class="info">در حال تست اتصال...</p>';
|
| 78 |
+
|
| 79 |
+
try {
|
| 80 |
+
const response = await fetch(`${API_BASE}/api/dashboard-summary`);
|
| 81 |
+
if (response.ok) {
|
| 82 |
+
resultDiv.innerHTML = '<p class="success">✅ اتصال موفق! سرور در دسترس است.</p>';
|
| 83 |
+
} else {
|
| 84 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در اتصال: ${response.status} ${response.statusText}</p>`;
|
| 85 |
+
}
|
| 86 |
+
} catch (error) {
|
| 87 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در اتصال: ${error.message}</p>`;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
async function testDashboardSummary() {
|
| 92 |
+
const resultDiv = document.getElementById('dashboardResult');
|
| 93 |
+
resultDiv.innerHTML = '<p class="info">در حال دریافت آمار...</p>';
|
| 94 |
+
|
| 95 |
+
try {
|
| 96 |
+
const response = await fetch(`${API_BASE}/api/dashboard-summary`);
|
| 97 |
+
if (response.ok) {
|
| 98 |
+
const data = await response.json();
|
| 99 |
+
resultDiv.innerHTML = `
|
| 100 |
+
<p class="success">✅ آمار دریافت شد:</p>
|
| 101 |
+
<pre>${JSON.stringify(data, null, 2)}</pre>
|
| 102 |
+
`;
|
| 103 |
+
} else {
|
| 104 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت آمار: ${response.status}</p>`;
|
| 105 |
+
}
|
| 106 |
+
} catch (error) {
|
| 107 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت آمار: ${error.message}</p>`;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
async function testDocuments() {
|
| 112 |
+
const resultDiv = document.getElementById('documentsResult');
|
| 113 |
+
resultDiv.innerHTML = '<p class="info">در حال دریافت اسناد...</p>';
|
| 114 |
+
|
| 115 |
+
try {
|
| 116 |
+
const response = await fetch(`${API_BASE}/api/documents?limit=5`);
|
| 117 |
+
if (response.ok) {
|
| 118 |
+
const data = await response.json();
|
| 119 |
+
resultDiv.innerHTML = `
|
| 120 |
+
<p class="success">✅ اسناد دریافت شد (${data.length} سند):</p>
|
| 121 |
+
<pre>${JSON.stringify(data, null, 2)}</pre>
|
| 122 |
+
`;
|
| 123 |
+
} else {
|
| 124 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت اسناد: ${response.status}</p>`;
|
| 125 |
+
}
|
| 126 |
+
} catch (error) {
|
| 127 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در دریافت اسناد: ${error.message}</p>`;
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
async function testScraping() {
|
| 132 |
+
const resultDiv = document.getElementById('scrapingResult');
|
| 133 |
+
resultDiv.innerHTML = '<p class="info">در حال شروع جمعآوری...</p>';
|
| 134 |
+
|
| 135 |
+
try {
|
| 136 |
+
const response = await fetch(`${API_BASE}/api/scrape-trigger`, {
|
| 137 |
+
method: 'POST',
|
| 138 |
+
headers: {
|
| 139 |
+
'Content-Type': 'application/json',
|
| 140 |
+
},
|
| 141 |
+
body: JSON.stringify({ manual_trigger: true })
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
if (response.ok) {
|
| 145 |
+
const data = await response.json();
|
| 146 |
+
resultDiv.innerHTML = `
|
| 147 |
+
<p class="success">✅ جمعآوری شروع شد:</p>
|
| 148 |
+
<pre>${JSON.stringify(data, null, 2)}</pre>
|
| 149 |
+
`;
|
| 150 |
+
} else {
|
| 151 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در شروع جمعآوری: ${response.status}</p>`;
|
| 152 |
+
}
|
| 153 |
+
} catch (error) {
|
| 154 |
+
resultDiv.innerHTML = `<p class="error">❌ خطا در شروع جمعآوری: ${error.message}</p>`;
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Auto-test on page load
|
| 159 |
+
window.addEventListener('load', () => {
|
| 160 |
+
setTimeout(testConnection, 1000);
|
| 161 |
+
});
|
| 162 |
+
</script>
|
| 163 |
+
</body>
|
| 164 |
+
</html>
|
app/static/js/api-client.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Legal Dashboard API Client
|
| 3 |
+
* =========================
|
| 4 |
+
*
|
| 5 |
+
* JavaScript client for communicating with the Legal Dashboard backend API.
|
| 6 |
+
* Handles all HTTP requests and data transformation between frontend and backend.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
class LegalDashboardAPI {
|
| 10 |
+
constructor(baseUrl = '') {
|
| 11 |
+
this.baseUrl = baseUrl || window.location.origin;
|
| 12 |
+
this.apiBase = `${this.baseUrl}/api`;
|
| 13 |
+
|
| 14 |
+
// Request interceptor for common headers
|
| 15 |
+
this.defaultHeaders = {
|
| 16 |
+
'Content-Type': 'application/json',
|
| 17 |
+
};
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/**
|
| 21 |
+
* Generic HTTP request handler with error handling
|
| 22 |
+
*/
|
| 23 |
+
async request(endpoint, options = {}) {
|
| 24 |
+
const url = `${this.apiBase}${endpoint}`;
|
| 25 |
+
|
| 26 |
+
const config = {
|
| 27 |
+
...options,
|
| 28 |
+
headers: {
|
| 29 |
+
...this.defaultHeaders,
|
| 30 |
+
...options.headers
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
const response = await fetch(url, config);
|
| 36 |
+
|
| 37 |
+
if (!response.ok) {
|
| 38 |
+
const errorData = await response.json().catch(() => ({}));
|
| 39 |
+
throw new Error(errorData.detail || `HTTP ${response.status}: ${response.statusText}`);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
const contentType = response.headers.get('content-type');
|
| 43 |
+
if (contentType && contentType.includes('application/json')) {
|
| 44 |
+
return await response.json();
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
return await response.text();
|
| 48 |
+
|
| 49 |
+
} catch (error) {
|
| 50 |
+
console.error(`API Request failed: ${endpoint}`, error);
|
| 51 |
+
throw error;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Health check - verify backend is running
|
| 57 |
+
*/
|
| 58 |
+
async healthCheck() {
|
| 59 |
+
return this.request('/health');
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// ==================== DASHBOARD API ====================
|
| 63 |
+
|
| 64 |
+
/**
|
| 65 |
+
* Get dashboard summary statistics
|
| 66 |
+
*/
|
| 67 |
+
async getDashboardSummary() {
|
| 68 |
+
const response = await this.request('/dashboard/summary');
|
| 69 |
+
return response.data;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/**
|
| 73 |
+
* Get processing trends for charts
|
| 74 |
+
*/
|
| 75 |
+
async getProcessingTrends(period = 'weekly') {
|
| 76 |
+
const response = await this.request(`/dashboard/charts/processing-trends?period=${period}`);
|
| 77 |
+
return response.data;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* Get status distribution for pie chart
|
| 82 |
+
*/
|
| 83 |
+
async getStatusDistribution() {
|
| 84 |
+
const response = await this.request('/dashboard/charts/status-distribution');
|
| 85 |
+
return response.data;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
/**
|
| 89 |
+
* Get category distribution for pie chart
|
| 90 |
+
*/
|
| 91 |
+
async getCategoryDistribution() {
|
| 92 |
+
const response = await this.request('/dashboard/charts/category-distribution');
|
| 93 |
+
return response.data;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// ==================== DOCUMENTS API ====================
|
| 97 |
+
|
| 98 |
+
/**
|
| 99 |
+
* Get paginated list of documents with filtering
|
| 100 |
+
*/
|
| 101 |
+
async getDocuments(params = {}) {
|
| 102 |
+
const searchParams = new URLSearchParams();
|
| 103 |
+
|
| 104 |
+
Object.entries(params).forEach(([key, value]) => {
|
| 105 |
+
if (value !== null && value !== undefined && value !== '') {
|
| 106 |
+
searchParams.append(key, value);
|
| 107 |
+
}
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
const endpoint = `/documents/?${searchParams.toString()}`;
|
| 111 |
+
return this.request(endpoint);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/**
|
| 115 |
+
* Get single document by ID
|
| 116 |
+
*/
|
| 117 |
+
async getDocument(documentId) {
|
| 118 |
+
return this.request(`/documents/${documentId}`);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/**
|
| 122 |
+
* Delete document by ID
|
| 123 |
+
*/
|
| 124 |
+
async deleteDocument(documentId) {
|
| 125 |
+
return this.request(`/documents/${documentId}`, {
|
| 126 |
+
method: 'DELETE'
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// ==================== OCR API ====================
|
| 131 |
+
|
| 132 |
+
/**
|
| 133 |
+
* Upload files for OCR processing
|
| 134 |
+
*/
|
| 135 |
+
async uploadFiles(files) {
|
| 136 |
+
const formData = new FormData();
|
| 137 |
+
|
| 138 |
+
// Add files to form data
|
| 139 |
+
files.forEach(file => {
|
| 140 |
+
formData.append('files', file);
|
| 141 |
+
});
|
| 142 |
+
|
| 143 |
+
return this.request('/ocr/upload', {
|
| 144 |
+
method: 'POST',
|
| 145 |
+
headers: {}, // Remove Content-Type to let browser set it for FormData
|
| 146 |
+
body: formData
|
| 147 |
+
});
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Extract text from PDF immediately (for testing)
|
| 152 |
+
*/
|
| 153 |
+
async extractTextImmediate(file) {
|
| 154 |
+
const formData = new FormData();
|
| 155 |
+
formData.append('file', file);
|
| 156 |
+
|
| 157 |
+
return this.request('/ocr/extract', {
|
| 158 |
+
method: 'POST',
|
| 159 |
+
headers: {},
|
| 160 |
+
body: formData
|
| 161 |
+
});
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// ==================== SCRAPING API ====================
|
| 165 |
+
|
| 166 |
+
/**
|
| 167 |
+
* Scrape content from website
|
| 168 |
+
*/
|
| 169 |
+
async scrapeWebsite(scrapingRequest) {
|
| 170 |
+
return this.request('/scraping/scrape', {
|
| 171 |
+
method: 'POST',
|
| 172 |
+
body: JSON.stringify(scrapingRequest)
|
| 173 |
+
});
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/**
|
| 177 |
+
* Get scraping history
|
| 178 |
+
*/
|
| 179 |
+
async getScrapingHistory(skip = 0, limit = 50) {
|
| 180 |
+
const response = await this.request(`/scraping/scrape/history?skip=${skip}&limit=${limit}`);
|
| 181 |
+
return response.data;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// ==================== ANALYTICS API ====================
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* Calculate similarity between two documents
|
| 188 |
+
*/
|
| 189 |
+
async calculateSimilarity(doc1Id, doc2Id) {
|
| 190 |
+
const response = await this.request(`/analytics/similarity?doc1_id=${doc1Id}&doc2_id=${doc2Id}`);
|
| 191 |
+
return response.data;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/**
|
| 195 |
+
* Get quality analysis
|
| 196 |
+
*/
|
| 197 |
+
async getQualityAnalysis() {
|
| 198 |
+
const response = await this.request('/analytics/quality-analysis');
|
| 199 |
+
return response.data;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/**
|
| 203 |
+
* Get performance metrics
|
| 204 |
+
*/
|
| 205 |
+
async getPerformanceMetrics() {
|
| 206 |
+
const response = await this.request('/analytics/performance-metrics');
|
| 207 |
+
return response.data;
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
// ==================== DATA MODELS ====================
|
| 212 |
+
|
| 213 |
+
/**
|
| 214 |
+
* Document model to match backend structure
|
| 215 |
+
*/
|
| 216 |
+
class DocumentModel {
|
| 217 |
+
constructor(backendData) {
|
| 218 |
+
this.id = backendData.id;
|
| 219 |
+
this.filename = backendData.filename;
|
| 220 |
+
this.original_filename = backendData.original_filename;
|
| 221 |
+
this.file_size = backendData.file_size;
|
| 222 |
+
this.file_path = backendData.file_path;
|
| 223 |
+
this.category = backendData.category;
|
| 224 |
+
this.quality_score = backendData.quality_score || 0;
|
| 225 |
+
this.confidence_score = backendData.confidence_score || 0;
|
| 226 |
+
this.status = backendData.status;
|
| 227 |
+
this.created_at = backendData.created_at;
|
| 228 |
+
this.processed_at = backendData.processed_at;
|
| 229 |
+
this.ocr_text = backendData.ocr_text;
|
| 230 |
+
this.summary = backendData.summary;
|
| 231 |
+
this.keywords = backendData.keywords || [];
|
| 232 |
+
this.extracted_entities = backendData.extracted_entities || [];
|
| 233 |
+
this.processing_time = backendData.processing_time || 0;
|
| 234 |
+
this.importance_score = backendData.importance_score || 0;
|
| 235 |
+
this.similarity_scores = backendData.similarity_scores || {};
|
| 236 |
+
this.legal_references = backendData.legal_references || [];
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/**
|
| 240 |
+
* Format file size for display
|
| 241 |
+
*/
|
| 242 |
+
getFormattedFileSize() {
|
| 243 |
+
return formatFileSize(this.file_size);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/**
|
| 247 |
+
* Get creation date formatted for Persian locale
|
| 248 |
+
*/
|
| 249 |
+
getFormattedDate() {
|
| 250 |
+
return formatDate(this.created_at);
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/**
|
| 254 |
+
* Get quality class for styling
|
| 255 |
+
*/
|
| 256 |
+
getQualityClass() {
|
| 257 |
+
if (this.quality_score >= 8.5) return 'quality-excellent';
|
| 258 |
+
if (this.quality_score >= 6.5) return 'quality-good';
|
| 259 |
+
if (this.quality_score >= 4.5) return 'quality-average';
|
| 260 |
+
return 'quality-poor';
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/**
|
| 264 |
+
* Get status text in Persian
|
| 265 |
+
*/
|
| 266 |
+
getStatusText() {
|
| 267 |
+
const statusMap = {
|
| 268 |
+
'processed': 'پردازش شده',
|
| 269 |
+
'processing': 'در حال پردازش',
|
| 270 |
+
'uploaded': 'آپلود شده',
|
| 271 |
+
'pending': 'در انتظار',
|
| 272 |
+
'error': 'خطا'
|
| 273 |
+
};
|
| 274 |
+
return statusMap[this.status] || this.status;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/**
|
| 278 |
+
* Get status icon
|
| 279 |
+
*/
|
| 280 |
+
getStatusIcon() {
|
| 281 |
+
const iconMap = {
|
| 282 |
+
'processed': 'check-circle',
|
| 283 |
+
'processing': 'spinner fa-spin',
|
| 284 |
+
'uploaded': 'cloud-upload-alt',
|
| 285 |
+
'pending': 'clock',
|
| 286 |
+
'error': 'exclamation-triangle'
|
| 287 |
+
};
|
| 288 |
+
return iconMap[this.status] || 'question-circle';
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
/**
|
| 293 |
+
* Scraping result model
|
| 294 |
+
*/
|
| 295 |
+
class ScrapingResultModel {
|
| 296 |
+
constructor(backendData) {
|
| 297 |
+
this.success = backendData.success;
|
| 298 |
+
this.url = backendData.url;
|
| 299 |
+
this.title = backendData.title;
|
| 300 |
+
this.content_length = backendData.content_length;
|
| 301 |
+
this.processing_time = backendData.processing_time;
|
| 302 |
+
this.data = backendData.data;
|
| 303 |
+
this.error = backendData.error;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
isSuccessful() {
|
| 307 |
+
return this.success && !this.error;
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
getFormattedProcessingTime() {
|
| 311 |
+
return `${this.processing_time.toFixed(2)} ثانیه`;
|
| 312 |
+
}
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// ==================== UTILITY FUNCTIONS ====================
|
| 316 |
+
|
| 317 |
+
/**
|
| 318 |
+
* Format file size in bytes to human readable format
|
| 319 |
+
*/
|
| 320 |
+
function formatFileSize(bytes) {
|
| 321 |
+
if (bytes === 0) return '0 بایت';
|
| 322 |
+
const k = 1024;
|
| 323 |
+
const sizes = ['بایت', 'کیلوبایت', 'مگابایت', 'گیگابایت'];
|
| 324 |
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
| 325 |
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
/**
|
| 329 |
+
* Format date for Persian locale
|
| 330 |
+
*/
|
| 331 |
+
function formatDate(dateString) {
|
| 332 |
+
if (!dateString) return 'نامشخص';
|
| 333 |
+
const date = new Date(dateString);
|
| 334 |
+
return date.toLocaleDateString('fa-IR', {
|
| 335 |
+
year: 'numeric',
|
| 336 |
+
month: 'long',
|
| 337 |
+
day: 'numeric',
|
| 338 |
+
hour: '2-digit',
|
| 339 |
+
minute: '2-digit'
|
| 340 |
+
});
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
/**
|
| 344 |
+
* Show toast notification
|
| 345 |
+
*/
|
| 346 |
+
function showToast(message, type = 'info', title = 'اعلان') {
|
| 347 |
+
const toastContainer = document.getElementById('toastContainer');
|
| 348 |
+
if (!toastContainer) return;
|
| 349 |
+
|
| 350 |
+
const toast = document.createElement('div');
|
| 351 |
+
toast.className = `toast ${type}`;
|
| 352 |
+
|
| 353 |
+
const icons = {
|
| 354 |
+
success: 'check-circle',
|
| 355 |
+
error: 'exclamation-triangle',
|
| 356 |
+
warning: 'exclamation-circle',
|
| 357 |
+
info: 'info-circle'
|
| 358 |
+
};
|
| 359 |
+
|
| 360 |
+
toast.innerHTML = `
|
| 361 |
+
<div class="toast-icon">
|
| 362 |
+
<i class="fas fa-${icons[type]}"></i>
|
| 363 |
+
</div>
|
| 364 |
+
<div class="toast-content">
|
| 365 |
+
<div class="toast-title">${title}</div>
|
| 366 |
+
<div class="toast-message">${message}</div>
|
| 367 |
+
</div>
|
| 368 |
+
<button type="button" class="toast-close" onclick="this.parentElement.remove()">
|
| 369 |
+
<i class="fas fa-times"></i>
|
| 370 |
+
</button>
|
| 371 |
+
`;
|
| 372 |
+
|
| 373 |
+
toastContainer.appendChild(toast);
|
| 374 |
+
|
| 375 |
+
// Show toast
|
| 376 |
+
setTimeout(() => toast.classList.add('show'), 100);
|
| 377 |
+
|
| 378 |
+
// Auto remove after 5 seconds
|
| 379 |
+
setTimeout(() => {
|
| 380 |
+
if (toast.parentElement) {
|
| 381 |
+
toast.classList.remove('show');
|
| 382 |
+
setTimeout(() => {
|
| 383 |
+
if (toast.parentElement) {
|
| 384 |
+
toast.remove();
|
| 385 |
+
}
|
| 386 |
+
}, 300);
|
| 387 |
+
}
|
| 388 |
+
}, 5000);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
/**
|
| 392 |
+
* Debounce function for search inputs
|
| 393 |
+
*/
|
| 394 |
+
function debounce(func, wait) {
|
| 395 |
+
let timeout;
|
| 396 |
+
return function executedFunction(...args) {
|
| 397 |
+
const later = () => {
|
| 398 |
+
clearTimeout(timeout);
|
| 399 |
+
func(...args);
|
| 400 |
+
};
|
| 401 |
+
clearTimeout(timeout);
|
| 402 |
+
timeout = setTimeout(later, wait);
|
| 403 |
+
};
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// ==================== GLOBAL API INSTANCE ====================
|
| 407 |
+
|
| 408 |
+
// Create global API instance
|
| 409 |
+
window.legalAPI = new LegalDashboardAPI();
|
| 410 |
+
|
| 411 |
+
// Test connection on load
|
| 412 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 413 |
+
try {
|
| 414 |
+
await window.legalAPI.healthCheck();
|
| 415 |
+
console.log('✅ Backend connection successful');
|
| 416 |
+
} catch (error) {
|
| 417 |
+
console.warn('⚠️ Backend connection failed, using fallback mode:', error.message);
|
| 418 |
+
showToast('اتصال به سرور برقرار نشد. حالت آفلاین فعال است.', 'warning', 'هشدار اتصال');
|
| 419 |
+
}
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
console.log('🔗 Legal Dashboard API Client loaded');
|
app/static/js/api-connection-test.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API Connection Test Script
|
| 3 |
+
* =========================
|
| 4 |
+
*
|
| 5 |
+
* Automated test script to validate all backend API endpoints
|
| 6 |
+
* and provide detailed reporting on frontend-backend integration.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
class APIConnectionTester {
|
| 10 |
+
constructor() {
|
| 11 |
+
this.baseURL = window.location.origin;
|
| 12 |
+
this.results = [];
|
| 13 |
+
this.startTime = null;
|
| 14 |
+
this.endTime = null;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Run comprehensive API tests
|
| 19 |
+
*/
|
| 20 |
+
async runAllTests() {
|
| 21 |
+
console.log('🚀 Starting API Connection Tests...');
|
| 22 |
+
this.startTime = Date.now();
|
| 23 |
+
|
| 24 |
+
const tests = [
|
| 25 |
+
// System Health Tests
|
| 26 |
+
{ name: 'Health Check', url: '/api/health', method: 'GET', category: 'System' },
|
| 27 |
+
|
| 28 |
+
// Dashboard Tests
|
| 29 |
+
{ name: 'Dashboard Summary', url: '/api/dashboard/summary', method: 'GET', category: 'Dashboard' },
|
| 30 |
+
{ name: 'Charts Data', url: '/api/dashboard/charts-data', method: 'GET', category: 'Dashboard' },
|
| 31 |
+
{ name: 'AI Suggestions', url: '/api/dashboard/ai-suggestions', method: 'GET', category: 'Dashboard' },
|
| 32 |
+
{ name: 'Performance Metrics', url: '/api/dashboard/performance-metrics', method: 'GET', category: 'Dashboard' },
|
| 33 |
+
{ name: 'Trends', url: '/api/dashboard/trends', method: 'GET', category: 'Dashboard' },
|
| 34 |
+
|
| 35 |
+
// Documents Tests
|
| 36 |
+
{ name: 'Documents List', url: '/api/documents?limit=5', method: 'GET', category: 'Documents' },
|
| 37 |
+
{ name: 'Document Categories', url: '/api/documents/categories/', method: 'GET', category: 'Documents' },
|
| 38 |
+
{ name: 'Document Sources', url: '/api/documents/sources/', method: 'GET', category: 'Documents' },
|
| 39 |
+
{ name: 'Document Search', url: '/api/documents/search/?q=test', method: 'GET', category: 'Documents' },
|
| 40 |
+
|
| 41 |
+
// OCR Tests
|
| 42 |
+
{ name: 'OCR Status', url: '/api/ocr/status', method: 'GET', category: 'OCR' },
|
| 43 |
+
{ name: 'OCR Models', url: '/api/ocr/models', method: 'GET', category: 'OCR' },
|
| 44 |
+
|
| 45 |
+
// Analytics Tests
|
| 46 |
+
{ name: 'Analytics Overview', url: '/api/analytics/overview', method: 'GET', category: 'Analytics' },
|
| 47 |
+
{ name: 'Analytics Performance', url: '/api/analytics/performance', method: 'GET', category: 'Analytics' },
|
| 48 |
+
{ name: 'Analytics Entities', url: '/api/analytics/entities?limit=10', method: 'GET', category: 'Analytics' },
|
| 49 |
+
{ name: 'Analytics Quality', url: '/api/analytics/quality-analysis', method: 'GET', category: 'Analytics' },
|
| 50 |
+
|
| 51 |
+
// Scraping Tests
|
| 52 |
+
{ name: 'Scraping Statistics', url: '/api/scraping/statistics', method: 'GET', category: 'Scraping' },
|
| 53 |
+
{ name: 'Scraping Status', url: '/api/scraping/status', method: 'GET', category: 'Scraping' },
|
| 54 |
+
{ name: 'Rating Summary', url: '/api/scraping/rating/summary', method: 'GET', category: 'Scraping' },
|
| 55 |
+
{ name: 'Scraping Health', url: '/api/scraping/health', method: 'GET', category: 'Scraping' },
|
| 56 |
+
|
| 57 |
+
// Phase 2 - File Upload Tests
|
| 58 |
+
{ name: 'OCR Upload', url: '/api/ocr/upload', method: 'POST', category: 'File Upload' },
|
| 59 |
+
{ name: 'OCR Process', url: '/api/ocr/process', method: 'POST', category: 'File Upload' },
|
| 60 |
+
{ name: 'OCR Quality Metrics', url: '/api/ocr/quality-metrics', method: 'GET', category: 'File Upload' },
|
| 61 |
+
|
| 62 |
+
// Phase 2 - Document Management Tests
|
| 63 |
+
{ name: 'Create Document', url: '/api/documents', method: 'POST', category: 'Document Management' },
|
| 64 |
+
{ name: 'Update Document', url: '/api/documents/1', method: 'PUT', category: 'Document Management' },
|
| 65 |
+
{ name: 'Delete Document', url: '/api/documents/1', method: 'DELETE', category: 'Document Management' },
|
| 66 |
+
|
| 67 |
+
// Phase 2 - Advanced Scraping Tests
|
| 68 |
+
{ name: 'Scraping Start', url: '/api/scraping/start', method: 'POST', category: 'Advanced Scraping' },
|
| 69 |
+
{ name: 'Scraping Stop', url: '/api/scraping/stop', method: 'POST', category: 'Advanced Scraping' },
|
| 70 |
+
{ name: 'Scraping Results', url: '/api/scraping/results', method: 'GET', category: 'Advanced Scraping' }
|
| 71 |
+
];
|
| 72 |
+
|
| 73 |
+
console.log(`📋 Running ${tests.length} API tests...`);
|
| 74 |
+
|
| 75 |
+
for (const test of tests) {
|
| 76 |
+
await this.runSingleTest(test);
|
| 77 |
+
// Small delay to avoid overwhelming the server
|
| 78 |
+
await this.delay(100);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
this.endTime = Date.now();
|
| 82 |
+
this.generateReport();
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* Run a single API test
|
| 87 |
+
*/
|
| 88 |
+
async runSingleTest(test) {
|
| 89 |
+
const startTime = Date.now();
|
| 90 |
+
let result = {
|
| 91 |
+
name: test.name,
|
| 92 |
+
category: test.category,
|
| 93 |
+
url: test.url,
|
| 94 |
+
method: test.method,
|
| 95 |
+
success: false,
|
| 96 |
+
status: null,
|
| 97 |
+
responseTime: 0,
|
| 98 |
+
data: null,
|
| 99 |
+
error: null,
|
| 100 |
+
timestamp: new Date().toISOString()
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
try {
|
| 104 |
+
const response = await fetch(test.url, {
|
| 105 |
+
method: test.method,
|
| 106 |
+
headers: {
|
| 107 |
+
'Content-Type': 'application/json'
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
result.status = response.status;
|
| 112 |
+
result.responseTime = Date.now() - startTime;
|
| 113 |
+
|
| 114 |
+
if (response.ok) {
|
| 115 |
+
result.success = true;
|
| 116 |
+
try {
|
| 117 |
+
result.data = await response.json();
|
| 118 |
+
} catch (e) {
|
| 119 |
+
result.data = 'Non-JSON response';
|
| 120 |
+
}
|
| 121 |
+
} else {
|
| 122 |
+
result.error = `${response.status}: ${response.statusText}`;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
} catch (error) {
|
| 126 |
+
result.error = error.message;
|
| 127 |
+
result.responseTime = Date.now() - startTime;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
this.results.push(result);
|
| 131 |
+
|
| 132 |
+
// Log result
|
| 133 |
+
const status = result.success ? '✅' : '❌';
|
| 134 |
+
console.log(`${status} ${test.name}: ${result.success ? 'PASS' : 'FAIL'} (${result.responseTime}ms)`);
|
| 135 |
+
|
| 136 |
+
return result;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/**
|
| 140 |
+
* Generate comprehensive test report
|
| 141 |
+
*/
|
| 142 |
+
generateReport() {
|
| 143 |
+
const totalTests = this.results.length;
|
| 144 |
+
const passedTests = this.results.filter(r => r.success).length;
|
| 145 |
+
const failedTests = totalTests - passedTests;
|
| 146 |
+
const totalTime = this.endTime - this.startTime;
|
| 147 |
+
const avgResponseTime = this.results.reduce((sum, r) => sum + r.responseTime, 0) / totalTests;
|
| 148 |
+
|
| 149 |
+
console.log('\n📊 API Connection Test Report');
|
| 150 |
+
console.log('='.repeat(50));
|
| 151 |
+
console.log(`Total Tests: ${totalTests}`);
|
| 152 |
+
console.log(`Passed: ${passedTests} ✅`);
|
| 153 |
+
console.log(`Failed: ${failedTests} ❌`);
|
| 154 |
+
console.log(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
|
| 155 |
+
console.log(`Total Time: ${totalTime}ms`);
|
| 156 |
+
console.log(`Average Response Time: ${avgResponseTime.toFixed(0)}ms`);
|
| 157 |
+
|
| 158 |
+
// Group results by category
|
| 159 |
+
const categories = {};
|
| 160 |
+
this.results.forEach(result => {
|
| 161 |
+
if (!categories[result.category]) {
|
| 162 |
+
categories[result.category] = [];
|
| 163 |
+
}
|
| 164 |
+
categories[result.category].push(result);
|
| 165 |
+
});
|
| 166 |
+
|
| 167 |
+
console.log('\n📈 Results by Category:');
|
| 168 |
+
Object.entries(categories).forEach(([category, results]) => {
|
| 169 |
+
const passed = results.filter(r => r.success).length;
|
| 170 |
+
const total = results.length;
|
| 171 |
+
const rate = ((passed / total) * 100).toFixed(1);
|
| 172 |
+
console.log(`${category}: ${passed}/${total} (${rate}%)`);
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
// Show failed tests
|
| 176 |
+
const failedTests = this.results.filter(r => !r.success);
|
| 177 |
+
if (failedTests.length > 0) {
|
| 178 |
+
console.log('\n❌ Failed Tests:');
|
| 179 |
+
failedTests.forEach(test => {
|
| 180 |
+
console.log(` - ${test.name}: ${test.error}`);
|
| 181 |
+
});
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Show slow tests
|
| 185 |
+
const slowTests = this.results.filter(r => r.responseTime > 1000);
|
| 186 |
+
if (slowTests.length > 0) {
|
| 187 |
+
console.log('\n🐌 Slow Tests (>1s):');
|
| 188 |
+
slowTests.forEach(test => {
|
| 189 |
+
console.log(` - ${test.name}: ${test.responseTime}ms`);
|
| 190 |
+
});
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
this.displayResultsInUI();
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/**
|
| 197 |
+
* Display results in the UI
|
| 198 |
+
*/
|
| 199 |
+
displayResultsInUI() {
|
| 200 |
+
const container = document.getElementById('apiTestResults');
|
| 201 |
+
if (!container) {
|
| 202 |
+
console.warn('No #apiTestResults container found');
|
| 203 |
+
return;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
const totalTests = this.results.length;
|
| 207 |
+
const passedTests = this.results.filter(r => r.success).length;
|
| 208 |
+
const failedTests = totalTests - passedTests;
|
| 209 |
+
const successRate = ((passedTests / totalTests) * 100).toFixed(1);
|
| 210 |
+
|
| 211 |
+
container.innerHTML = `
|
| 212 |
+
<div class="test-report">
|
| 213 |
+
<h3>API Connection Test Results</h3>
|
| 214 |
+
<div class="test-summary">
|
| 215 |
+
<div class="test-stat">
|
| 216 |
+
<span class="stat-label">Total Tests:</span>
|
| 217 |
+
<span class="stat-value">${totalTests}</span>
|
| 218 |
+
</div>
|
| 219 |
+
<div class="test-stat">
|
| 220 |
+
<span class="stat-label">Passed:</span>
|
| 221 |
+
<span class="stat-value success">${passedTests} ✅</span>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="test-stat">
|
| 224 |
+
<span class="stat-label">Failed:</span>
|
| 225 |
+
<span class="stat-value error">${failedTests} ❌</span>
|
| 226 |
+
</div>
|
| 227 |
+
<div class="test-stat">
|
| 228 |
+
<span class="stat-label">Success Rate:</span>
|
| 229 |
+
<span class="stat-value">${successRate}%</span>
|
| 230 |
+
</div>
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<div class="test-details">
|
| 234 |
+
<h4>Test Details:</h4>
|
| 235 |
+
<div class="test-list">
|
| 236 |
+
${this.results.map(result => `
|
| 237 |
+
<div class="test-item ${result.success ? 'success' : 'error'}">
|
| 238 |
+
<span class="test-name">${result.name}</span>
|
| 239 |
+
<span class="test-status">${result.success ? 'PASS' : 'FAIL'}</span>
|
| 240 |
+
<span class="test-time">${result.responseTime}ms</span>
|
| 241 |
+
${result.error ? `<span class="test-error">${result.error}</span>` : ''}
|
| 242 |
+
</div>
|
| 243 |
+
`).join('')}
|
| 244 |
+
</div>
|
| 245 |
+
</div>
|
| 246 |
+
</div>
|
| 247 |
+
`;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/**
|
| 251 |
+
* Test specific endpoint patterns
|
| 252 |
+
*/
|
| 253 |
+
async testEndpointPatterns() {
|
| 254 |
+
console.log('\n🔍 Testing Endpoint Patterns...');
|
| 255 |
+
|
| 256 |
+
const patterns = [
|
| 257 |
+
// Test the broken endpoints that frontend is trying to call
|
| 258 |
+
{ name: 'Frontend Dashboard Summary (BROKEN)', url: '/api/dashboard-summary', expected: false },
|
| 259 |
+
{ name: 'Frontend Charts Data (BROKEN)', url: '/api/charts-data', expected: false },
|
| 260 |
+
{ name: 'Frontend AI Suggestions (BROKEN)', url: '/api/ai-suggestions', expected: false },
|
| 261 |
+
{ name: 'Frontend Train AI (BROKEN)', url: '/api/train-ai', expected: false },
|
| 262 |
+
{ name: 'Frontend Scrape Trigger (BROKEN)', url: '/api/scrape-trigger', expected: false },
|
| 263 |
+
|
| 264 |
+
// Test the correct endpoints
|
| 265 |
+
{ name: 'Backend Dashboard Summary (CORRECT)', url: '/api/dashboard/summary', expected: true },
|
| 266 |
+
{ name: 'Backend Charts Data (CORRECT)', url: '/api/dashboard/charts-data', expected: true },
|
| 267 |
+
{ name: 'Backend AI Suggestions (CORRECT)', url: '/api/dashboard/ai-suggestions', expected: true },
|
| 268 |
+
{ name: 'Backend AI Feedback (CORRECT)', url: '/api/dashboard/ai-feedback', expected: true },
|
| 269 |
+
{ name: 'Backend Scrape (CORRECT)', url: '/api/scraping/scrape', expected: true }
|
| 270 |
+
];
|
| 271 |
+
|
| 272 |
+
for (const pattern of patterns) {
|
| 273 |
+
try {
|
| 274 |
+
const response = await fetch(pattern.url);
|
| 275 |
+
const actual = response.ok;
|
| 276 |
+
const status = actual === pattern.expected ? '✅' : '❌';
|
| 277 |
+
console.log(`${status} ${pattern.name}: ${actual ? 'EXISTS' : 'MISSING'} (Expected: ${pattern.expected ? 'EXISTS' : 'MISSING'})`);
|
| 278 |
+
} catch (error) {
|
| 279 |
+
const status = pattern.expected ? '❌' : '✅';
|
| 280 |
+
console.log(`${status} ${pattern.name}: MISSING (Expected: ${pattern.expected ? 'EXISTS' : 'MISSING'})`);
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/**
|
| 286 |
+
* Test file upload functionality
|
| 287 |
+
*/
|
| 288 |
+
async testFileUpload() {
|
| 289 |
+
console.log('\n📁 Testing File Upload...');
|
| 290 |
+
|
| 291 |
+
// Create a test file
|
| 292 |
+
const testFile = new File(['Test PDF content'], 'test.pdf', { type: 'application/pdf' });
|
| 293 |
+
const formData = new FormData();
|
| 294 |
+
formData.append('file', testFile);
|
| 295 |
+
formData.append('title', 'Test Document');
|
| 296 |
+
formData.append('source', 'Test');
|
| 297 |
+
formData.append('category', 'Test');
|
| 298 |
+
|
| 299 |
+
try {
|
| 300 |
+
const response = await fetch('/api/ocr/process-and-save', {
|
| 301 |
+
method: 'POST',
|
| 302 |
+
body: formData
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
if (response.ok) {
|
| 306 |
+
console.log('✅ File upload endpoint is accessible');
|
| 307 |
+
const result = await response.json();
|
| 308 |
+
console.log('📄 Upload response:', result);
|
| 309 |
+
} else {
|
| 310 |
+
console.log('❌ File upload failed:', response.status, response.statusText);
|
| 311 |
+
}
|
| 312 |
+
} catch (error) {
|
| 313 |
+
console.log('❌ File upload error:', error.message);
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/**
|
| 318 |
+
* Utility function for delays
|
| 319 |
+
*/
|
| 320 |
+
delay(ms) {
|
| 321 |
+
return new Promise(resolve => setTimeout(resolve, ms));
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// Global instance
|
| 326 |
+
window.apiTester = new APIConnectionTester();
|
| 327 |
+
|
| 328 |
+
// Auto-run tests when page loads
|
| 329 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 330 |
+
console.log('🔧 API Connection Tester loaded');
|
| 331 |
+
|
| 332 |
+
// Add test button if not exists
|
| 333 |
+
if (!document.getElementById('runAPITests')) {
|
| 334 |
+
const testButton = document.createElement('button');
|
| 335 |
+
testButton.id = 'runAPITests';
|
| 336 |
+
testButton.textContent = 'Run API Tests';
|
| 337 |
+
testButton.style.cssText = `
|
| 338 |
+
position: fixed;
|
| 339 |
+
top: 10px;
|
| 340 |
+
right: 10px;
|
| 341 |
+
z-index: 10000;
|
| 342 |
+
padding: 10px 20px;
|
| 343 |
+
background: #007bff;
|
| 344 |
+
color: white;
|
| 345 |
+
border: none;
|
| 346 |
+
border-radius: 5px;
|
| 347 |
+
cursor: pointer;
|
| 348 |
+
`;
|
| 349 |
+
testButton.onclick = () => {
|
| 350 |
+
window.apiTester.runAllTests();
|
| 351 |
+
};
|
| 352 |
+
document.body.appendChild(testButton);
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// Add results container if not exists
|
| 356 |
+
if (!document.getElementById('apiTestResults')) {
|
| 357 |
+
const resultsContainer = document.createElement('div');
|
| 358 |
+
resultsContainer.id = 'apiTestResults';
|
| 359 |
+
resultsContainer.style.cssText = `
|
| 360 |
+
position: fixed;
|
| 361 |
+
top: 60px;
|
| 362 |
+
right: 10px;
|
| 363 |
+
width: 400px;
|
| 364 |
+
max-height: 500px;
|
| 365 |
+
overflow-y: auto;
|
| 366 |
+
background: white;
|
| 367 |
+
border: 1px solid #ccc;
|
| 368 |
+
border-radius: 5px;
|
| 369 |
+
padding: 15px;
|
| 370 |
+
z-index: 10000;
|
| 371 |
+
display: none;
|
| 372 |
+
`;
|
| 373 |
+
document.body.appendChild(resultsContainer);
|
| 374 |
+
}
|
| 375 |
+
});
|
| 376 |
+
|
| 377 |
+
// Export for use in other scripts
|
| 378 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 379 |
+
module.exports = APIConnectionTester;
|
| 380 |
+
}
|
app/static/js/chart.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Chart Manager for Legal Dashboard
|
| 3 |
+
* مدیریت کامل Chart.js و نمودارها
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
class ChartManager {
|
| 7 |
+
constructor() {
|
| 8 |
+
this.charts = {};
|
| 9 |
+
this.isChartJSLoaded = false;
|
| 10 |
+
this.loadingAttempts = 0;
|
| 11 |
+
this.maxLoadingAttempts = 10;
|
| 12 |
+
|
| 13 |
+
// Wait for Chart.js to load
|
| 14 |
+
this.waitForChartJS();
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Wait for Chart.js to be available
|
| 19 |
+
*/
|
| 20 |
+
async waitForChartJS() {
|
| 21 |
+
console.log('📊 Waiting for Chart.js to load...');
|
| 22 |
+
|
| 23 |
+
const checkInterval = setInterval(() => {
|
| 24 |
+
this.loadingAttempts++;
|
| 25 |
+
|
| 26 |
+
if (typeof Chart !== 'undefined') {
|
| 27 |
+
this.isChartJSLoaded = true;
|
| 28 |
+
clearInterval(checkInterval);
|
| 29 |
+
console.log('✅ Chart.js loaded successfully');
|
| 30 |
+
this.onChartJSReady();
|
| 31 |
+
} else if (this.loadingAttempts >= this.maxLoadingAttempts) {
|
| 32 |
+
clearInterval(checkInterval);
|
| 33 |
+
console.warn('⚠️ Chart.js failed to load after maximum attempts');
|
| 34 |
+
this.onChartJSFailed();
|
| 35 |
+
}
|
| 36 |
+
}, 500);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Called when Chart.js is ready
|
| 41 |
+
*/
|
| 42 |
+
onChartJSReady() {
|
| 43 |
+
// Hide placeholders and show canvases
|
| 44 |
+
this.showChartCanvases();
|
| 45 |
+
|
| 46 |
+
// Initialize charts
|
| 47 |
+
this.initializeAllCharts();
|
| 48 |
+
|
| 49 |
+
// Notify other components
|
| 50 |
+
if (window.notifications) {
|
| 51 |
+
window.notifications.show('نمودارها بارگذاری شدند', 'success', 'Chart.js');
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/**
|
| 56 |
+
* Called when Chart.js fails to load
|
| 57 |
+
*/
|
| 58 |
+
onChartJSFailed() {
|
| 59 |
+
console.error('❌ Chart.js could not be loaded');
|
| 60 |
+
this.showChartPlaceholders();
|
| 61 |
+
|
| 62 |
+
if (window.notifications) {
|
| 63 |
+
window.notifications.show('Chart.js بارگذاری نشد - نمودارها غیرفعال', 'warning', 'هشدار');
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Show chart canvases and hide placeholders
|
| 69 |
+
*/
|
| 70 |
+
showChartCanvases() {
|
| 71 |
+
// Hide all placeholders
|
| 72 |
+
document.querySelectorAll('.chart-placeholder').forEach(placeholder => {
|
| 73 |
+
placeholder.style.display = 'none';
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
// Show all canvases
|
| 77 |
+
document.querySelectorAll('canvas[id$="Canvas"]').forEach(canvas => {
|
| 78 |
+
canvas.style.display = 'block';
|
| 79 |
+
});
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Show placeholders when Chart.js fails
|
| 84 |
+
*/
|
| 85 |
+
showChartPlaceholders() {
|
| 86 |
+
document.querySelectorAll('.chart-placeholder').forEach(placeholder => {
|
| 87 |
+
placeholder.innerHTML = `
|
| 88 |
+
<i class="fas fa-exclamation-triangle" style="color: #f59e0b;"></i>
|
| 89 |
+
<p>Chart.js بارگذاری نشد</p>
|
| 90 |
+
<small>نمودارها در دسترس نیستند</small>
|
| 91 |
+
`;
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/**
|
| 96 |
+
* Initialize all charts
|
| 97 |
+
*/
|
| 98 |
+
initializeAllCharts() {
|
| 99 |
+
if (!this.isChartJSLoaded) {
|
| 100 |
+
console.warn('Chart.js not loaded, cannot initialize charts');
|
| 101 |
+
return;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
try {
|
| 105 |
+
// Performance Chart
|
| 106 |
+
this.initializePerformanceChart();
|
| 107 |
+
|
| 108 |
+
// Status Chart
|
| 109 |
+
this.initializeStatusChart();
|
| 110 |
+
|
| 111 |
+
console.log('📊 All charts initialized successfully');
|
| 112 |
+
} catch (error) {
|
| 113 |
+
console.error('❌ Error initializing charts:', error);
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/**
|
| 118 |
+
* Initialize performance chart
|
| 119 |
+
*/
|
| 120 |
+
initializePerformanceChart() {
|
| 121 |
+
const ctx = document.getElementById('performanceChartCanvas');
|
| 122 |
+
if (!ctx) return;
|
| 123 |
+
|
| 124 |
+
this.charts.performance = new Chart(ctx, {
|
| 125 |
+
type: 'line',
|
| 126 |
+
data: {
|
| 127 |
+
labels: ['شنبه', 'یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه'],
|
| 128 |
+
datasets: [
|
| 129 |
+
{
|
| 130 |
+
label: 'زمان پاسخ (ms)',
|
| 131 |
+
data: [120, 190, 300, 250, 200, 350, 180],
|
| 132 |
+
borderColor: '#10b981',
|
| 133 |
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
| 134 |
+
tension: 0.4,
|
| 135 |
+
borderWidth: 3,
|
| 136 |
+
pointRadius: 6,
|
| 137 |
+
pointHoverRadius: 8
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
label: 'CPU Usage (%)',
|
| 141 |
+
data: [25, 35, 45, 40, 30, 50, 28],
|
| 142 |
+
borderColor: '#3b82f6',
|
| 143 |
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
| 144 |
+
tension: 0.4,
|
| 145 |
+
borderWidth: 3,
|
| 146 |
+
pointRadius: 6,
|
| 147 |
+
pointHoverRadius: 8
|
| 148 |
+
}
|
| 149 |
+
]
|
| 150 |
+
},
|
| 151 |
+
options: {
|
| 152 |
+
responsive: true,
|
| 153 |
+
maintainAspectRatio: false,
|
| 154 |
+
plugins: {
|
| 155 |
+
legend: {
|
| 156 |
+
position: 'top',
|
| 157 |
+
labels: {
|
| 158 |
+
usePointStyle: true,
|
| 159 |
+
padding: 20,
|
| 160 |
+
font: {
|
| 161 |
+
family: 'Vazirmatn',
|
| 162 |
+
size: 12
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
scales: {
|
| 168 |
+
y: {
|
| 169 |
+
beginAtZero: true,
|
| 170 |
+
grid: {
|
| 171 |
+
color: 'rgba(0, 0, 0, 0.05)'
|
| 172 |
+
},
|
| 173 |
+
ticks: {
|
| 174 |
+
font: {
|
| 175 |
+
family: 'Vazirmatn'
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
},
|
| 179 |
+
x: {
|
| 180 |
+
grid: {
|
| 181 |
+
color: 'rgba(0, 0, 0, 0.05)'
|
| 182 |
+
},
|
| 183 |
+
ticks: {
|
| 184 |
+
font: {
|
| 185 |
+
family: 'Vazirmatn'
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
},
|
| 190 |
+
interaction: {
|
| 191 |
+
intersect: false,
|
| 192 |
+
mode: 'index'
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/**
|
| 199 |
+
* Initialize status chart
|
| 200 |
+
*/
|
| 201 |
+
initializeStatusChart() {
|
| 202 |
+
const ctx = document.getElementById('statusChartCanvas');
|
| 203 |
+
if (!ctx) return;
|
| 204 |
+
|
| 205 |
+
this.charts.status = new Chart(ctx, {
|
| 206 |
+
type: 'doughnut',
|
| 207 |
+
data: {
|
| 208 |
+
labels: ['API آنلاین', 'OCR آماده', 'PDF فعال', 'Cache فعال'],
|
| 209 |
+
datasets: [{
|
| 210 |
+
data: [1, 1, 1, 1],
|
| 211 |
+
backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'],
|
| 212 |
+
borderColor: '#ffffff',
|
| 213 |
+
borderWidth: 3,
|
| 214 |
+
hoverBorderWidth: 5
|
| 215 |
+
}]
|
| 216 |
+
},
|
| 217 |
+
options: {
|
| 218 |
+
responsive: true,
|
| 219 |
+
maintainAspectRatio: false,
|
| 220 |
+
plugins: {
|
| 221 |
+
legend: {
|
| 222 |
+
position: 'bottom',
|
| 223 |
+
labels: {
|
| 224 |
+
usePointStyle: true,
|
| 225 |
+
padding: 15,
|
| 226 |
+
font: {
|
| 227 |
+
family: 'Vazirmatn',
|
| 228 |
+
size: 11
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
},
|
| 233 |
+
cutout: '60%'
|
| 234 |
+
}
|
| 235 |
+
});
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/**
|
| 239 |
+
* Update performance chart
|
| 240 |
+
*/
|
| 241 |
+
updatePerformanceChart(period) {
|
| 242 |
+
if (!this.isChartJSLoaded || !this.charts.performance) {
|
| 243 |
+
if (window.notifications) {
|
| 244 |
+
window.notifications.show('نمودارها در دسترس نیستند', 'warning', 'هشدار');
|
| 245 |
+
}
|
| 246 |
+
return;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
const data = {
|
| 250 |
+
daily: {
|
| 251 |
+
labels: ['ساعت 6', 'ساعت 9', 'ساعت 12', 'ساعت 15', 'ساعت 18', 'ساعت 21', 'ساعت 24'],
|
| 252 |
+
responseTime: [120, 150, 200, 180, 160, 140, 130],
|
| 253 |
+
cpuUsage: [25, 30, 45, 40, 35, 28, 22]
|
| 254 |
+
},
|
| 255 |
+
weekly: {
|
| 256 |
+
labels: ['شنبه', 'یکشنبه', 'دوشنبه', 'سهشنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه'],
|
| 257 |
+
responseTime: [120, 190, 300, 250, 200, 350, 180],
|
| 258 |
+
cpuUsage: [25, 35, 45, 40, 30, 50, 28]
|
| 259 |
+
},
|
| 260 |
+
monthly: {
|
| 261 |
+
labels: ['هفته 1', 'هفته 2', 'هفته 3', 'هفته 4'],
|
| 262 |
+
responseTime: [180, 220, 190, 210],
|
| 263 |
+
cpuUsage: [35, 42, 38, 40]
|
| 264 |
+
}
|
| 265 |
+
};
|
| 266 |
+
|
| 267 |
+
const selectedData = data[period] || data.weekly;
|
| 268 |
+
|
| 269 |
+
this.charts.performance.data.labels = selectedData.labels;
|
| 270 |
+
this.charts.performance.data.datasets[0].data = selectedData.responseTime;
|
| 271 |
+
this.charts.performance.data.datasets[1].data = selectedData.cpuUsage;
|
| 272 |
+
this.charts.performance.update('active');
|
| 273 |
+
|
| 274 |
+
if (window.notifications) {
|
| 275 |
+
const periodText = period === 'daily' ? 'روزانه' : period === 'weekly' ? 'هفتگی' : 'ماهانه';
|
| 276 |
+
window.notifications.show(`نمودار به حالت ${periodText} تغییر کرد`, 'info', 'بروزرسانی');
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/**
|
| 281 |
+
* Update status chart
|
| 282 |
+
*/
|
| 283 |
+
updateStatusChart(type) {
|
| 284 |
+
if (!this.isChartJSLoaded || !this.charts.status) {
|
| 285 |
+
if (window.notifications) {
|
| 286 |
+
window.notifications.show('نمودارها در دسترس نیستند', 'warning', 'هشدار');
|
| 287 |
+
}
|
| 288 |
+
return;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
const data = {
|
| 292 |
+
services: {
|
| 293 |
+
labels: ['API آنلاین', 'OCR آماده', 'PDF فعال', 'Cache فعال'],
|
| 294 |
+
data: [1, 1, 1, 1],
|
| 295 |
+
colors: ['#10b981', '#3b82f6', '#f59e0b', '#ef4444']
|
| 296 |
+
},
|
| 297 |
+
endpoints: {
|
| 298 |
+
labels: ['/health', '/api/ocr/*', '/system/status', '/api/docs'],
|
| 299 |
+
data: [1, 1, 1, 1],
|
| 300 |
+
colors: ['#10b981', '#3b82f6', '#f59e0b', '#8b5cf6']
|
| 301 |
+
}
|
| 302 |
+
};
|
| 303 |
+
|
| 304 |
+
const selectedData = data[type] || data.services;
|
| 305 |
+
|
| 306 |
+
this.charts.status.data.labels = selectedData.labels;
|
| 307 |
+
this.charts.status.data.datasets[0].data = selectedData.data;
|
| 308 |
+
this.charts.status.data.datasets[0].backgroundColor = selectedData.colors;
|
| 309 |
+
this.charts.status.update('active');
|
| 310 |
+
|
| 311 |
+
if (window.notifications) {
|
| 312 |
+
const typeText = type === 'services' ? 'سرویسها' : 'endpoint ها';
|
| 313 |
+
window.notifications.show(`نمودار به حالت ${typeText} تغییر کرد`, 'info', 'بروزرسانی');
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
/**
|
| 318 |
+
* Destroy all charts
|
| 319 |
+
*/
|
| 320 |
+
destroyAllCharts() {
|
| 321 |
+
Object.values(this.charts).forEach(chart => {
|
| 322 |
+
if (chart && typeof chart.destroy === 'function') {
|
| 323 |
+
chart.destroy();
|
| 324 |
+
}
|
| 325 |
+
});
|
| 326 |
+
this.charts = {};
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
/**
|
| 330 |
+
* Check if charts are ready
|
| 331 |
+
*/
|
| 332 |
+
areChartsReady() {
|
| 333 |
+
return this.isChartJSLoaded && Object.keys(this.charts).length > 0;
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Create global instance
|
| 338 |
+
window.chartManager = new ChartManager();
|
| 339 |
+
|
| 340 |
+
console.log('📊 Chart Manager loaded');
|
app/static/js/core.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Legal Dashboard Core Module
|
| 3 |
+
* ==========================
|
| 4 |
+
*
|
| 5 |
+
* Shared core functionality for cross-page communication and data synchronization.
|
| 6 |
+
* This module provides event-driven updates and shared state management across all pages.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
class DashboardCore {
|
| 10 |
+
constructor() {
|
| 11 |
+
this.eventBus = new EventTarget();
|
| 12 |
+
this.cache = new Map();
|
| 13 |
+
this.apiClient = null;
|
| 14 |
+
this.isInitialized = false;
|
| 15 |
+
|
| 16 |
+
// Initialize when DOM is ready
|
| 17 |
+
if (document.readyState === 'loading') {
|
| 18 |
+
document.addEventListener('DOMContentLoaded', () => this.initialize());
|
| 19 |
+
} else {
|
| 20 |
+
this.initialize();
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/**
|
| 25 |
+
* Initialize the core module
|
| 26 |
+
*/
|
| 27 |
+
initialize() {
|
| 28 |
+
if (this.isInitialized) return;
|
| 29 |
+
|
| 30 |
+
console.log('🚀 Initializing Dashboard Core...');
|
| 31 |
+
|
| 32 |
+
// Initialize API client
|
| 33 |
+
this.apiClient = new LegalDashboardAPI();
|
| 34 |
+
|
| 35 |
+
// Set up localStorage synchronization
|
| 36 |
+
this.setupLocalStorageSync();
|
| 37 |
+
|
| 38 |
+
// Set up periodic health checks
|
| 39 |
+
this.setupHealthChecks();
|
| 40 |
+
|
| 41 |
+
// Set up cross-page event listeners
|
| 42 |
+
this.setupEventListeners();
|
| 43 |
+
|
| 44 |
+
this.isInitialized = true;
|
| 45 |
+
console.log('✅ Dashboard Core initialized');
|
| 46 |
+
|
| 47 |
+
// Broadcast initialization event
|
| 48 |
+
this.broadcast('coreInitialized', { timestamp: Date.now() });
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* Broadcast events across pages
|
| 53 |
+
*/
|
| 54 |
+
broadcast(eventName, data = {}) {
|
| 55 |
+
const event = new CustomEvent(eventName, {
|
| 56 |
+
detail: {
|
| 57 |
+
...data,
|
| 58 |
+
timestamp: Date.now(),
|
| 59 |
+
source: window.location.pathname
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
this.eventBus.dispatchEvent(event);
|
| 64 |
+
|
| 65 |
+
// Also store in localStorage for cross-tab communication
|
| 66 |
+
this.storeEvent(eventName, data);
|
| 67 |
+
|
| 68 |
+
console.log(`📡 Broadcast: ${eventName}`, data);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Listen for cross-page events
|
| 73 |
+
*/
|
| 74 |
+
listen(eventName, callback) {
|
| 75 |
+
const wrappedCallback = (event) => {
|
| 76 |
+
callback(event.detail);
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
this.eventBus.addEventListener(eventName, wrappedCallback);
|
| 80 |
+
|
| 81 |
+
// Return unsubscribe function
|
| 82 |
+
return () => {
|
| 83 |
+
this.eventBus.removeEventListener(eventName, wrappedCallback);
|
| 84 |
+
};
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/**
|
| 88 |
+
* Store event in localStorage for cross-tab communication
|
| 89 |
+
*/
|
| 90 |
+
storeEvent(eventName, data) {
|
| 91 |
+
try {
|
| 92 |
+
const events = JSON.parse(localStorage.getItem('dashboard_events') || '[]');
|
| 93 |
+
events.push({
|
| 94 |
+
name: eventName,
|
| 95 |
+
data: data,
|
| 96 |
+
timestamp: Date.now(),
|
| 97 |
+
source: window.location.pathname
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
// Keep only last 50 events
|
| 101 |
+
if (events.length > 50) {
|
| 102 |
+
events.splice(0, events.length - 50);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
localStorage.setItem('dashboard_events', JSON.stringify(events));
|
| 106 |
+
} catch (error) {
|
| 107 |
+
console.warn('Failed to store event in localStorage:', error);
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/**
|
| 112 |
+
* Setup localStorage synchronization
|
| 113 |
+
*/
|
| 114 |
+
setupLocalStorageSync() {
|
| 115 |
+
// Listen for storage changes (cross-tab communication)
|
| 116 |
+
window.addEventListener('storage', (event) => {
|
| 117 |
+
if (event.key === 'dashboard_events') {
|
| 118 |
+
try {
|
| 119 |
+
const events = JSON.parse(event.newValue || '[]');
|
| 120 |
+
const latestEvent = events[events.length - 1];
|
| 121 |
+
|
| 122 |
+
if (latestEvent && latestEvent.source !== window.location.pathname) {
|
| 123 |
+
// Re-broadcast event from other tab
|
| 124 |
+
this.eventBus.dispatchEvent(new CustomEvent(latestEvent.name, {
|
| 125 |
+
detail: latestEvent.data
|
| 126 |
+
}));
|
| 127 |
+
}
|
| 128 |
+
} catch (error) {
|
| 129 |
+
console.warn('Failed to process storage event:', error);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
});
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/**
|
| 136 |
+
* Setup periodic health checks
|
| 137 |
+
*/
|
| 138 |
+
setupHealthChecks() {
|
| 139 |
+
// Check API health every 30 seconds
|
| 140 |
+
setInterval(async () => {
|
| 141 |
+
try {
|
| 142 |
+
const health = await this.apiClient.healthCheck();
|
| 143 |
+
this.broadcast('healthUpdate', health);
|
| 144 |
+
} catch (error) {
|
| 145 |
+
this.broadcast('healthUpdate', { status: 'unhealthy', error: error.message });
|
| 146 |
+
}
|
| 147 |
+
}, 30000);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Setup common event listeners
|
| 152 |
+
*/
|
| 153 |
+
setupEventListeners() {
|
| 154 |
+
// Listen for document uploads
|
| 155 |
+
this.listen('documentUploaded', (data) => {
|
| 156 |
+
this.handleDocumentUpload(data);
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
// Listen for document updates
|
| 160 |
+
this.listen('documentUpdated', (data) => {
|
| 161 |
+
this.handleDocumentUpdate(data);
|
| 162 |
+
});
|
| 163 |
+
|
| 164 |
+
// Listen for document deletions
|
| 165 |
+
this.listen('documentDeleted', (data) => {
|
| 166 |
+
this.handleDocumentDelete(data);
|
| 167 |
+
});
|
| 168 |
+
|
| 169 |
+
// Listen for scraping updates
|
| 170 |
+
this.listen('scrapingUpdate', (data) => {
|
| 171 |
+
this.handleScrapingUpdate(data);
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
// Listen for system health updates
|
| 175 |
+
this.listen('healthUpdate', (data) => {
|
| 176 |
+
this.handleHealthUpdate(data);
|
| 177 |
+
});
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* Handle document upload events
|
| 182 |
+
*/
|
| 183 |
+
handleDocumentUpload(data) {
|
| 184 |
+
console.log('📄 Document uploaded:', data);
|
| 185 |
+
|
| 186 |
+
// Update cache
|
| 187 |
+
this.cache.set(`document_${data.fileId}`, data);
|
| 188 |
+
|
| 189 |
+
// Refresh document lists on relevant pages
|
| 190 |
+
if (window.location.pathname.includes('documents.html') ||
|
| 191 |
+
window.location.pathname.includes('improved_legal_dashboard.html')) {
|
| 192 |
+
this.refreshDocumentList();
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Update dashboard stats
|
| 196 |
+
this.updateDashboardStats();
|
| 197 |
+
|
| 198 |
+
// Show notification
|
| 199 |
+
showToast(`فایل "${data.fileName}" با موفقیت آپلود شد`, 'success');
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/**
|
| 203 |
+
* Handle document update events
|
| 204 |
+
*/
|
| 205 |
+
handleDocumentUpdate(data) {
|
| 206 |
+
console.log('📝 Document updated:', data);
|
| 207 |
+
|
| 208 |
+
// Update cache
|
| 209 |
+
this.cache.set(`document_${data.documentId}`, data);
|
| 210 |
+
|
| 211 |
+
// Refresh document lists
|
| 212 |
+
this.refreshDocumentList();
|
| 213 |
+
|
| 214 |
+
// Show notification
|
| 215 |
+
showToast('سند با موفقیت بهروزرسانی شد', 'success');
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
/**
|
| 219 |
+
* Handle document delete events
|
| 220 |
+
*/
|
| 221 |
+
handleDocumentDelete(data) {
|
| 222 |
+
console.log('🗑️ Document deleted:', data);
|
| 223 |
+
|
| 224 |
+
// Remove from cache
|
| 225 |
+
this.cache.delete(`document_${data.documentId}`);
|
| 226 |
+
|
| 227 |
+
// Refresh document lists
|
| 228 |
+
this.refreshDocumentList();
|
| 229 |
+
|
| 230 |
+
// Update dashboard stats
|
| 231 |
+
this.updateDashboardStats();
|
| 232 |
+
|
| 233 |
+
// Show notification
|
| 234 |
+
showToast('سند با موفقیت حذف شد', 'info');
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
/**
|
| 238 |
+
* Handle scraping update events
|
| 239 |
+
*/
|
| 240 |
+
handleScrapingUpdate(data) {
|
| 241 |
+
console.log('🕷️ Scraping update:', data);
|
| 242 |
+
|
| 243 |
+
// Update scraping dashboard if on that page
|
| 244 |
+
if (window.location.pathname.includes('scraping_dashboard.html')) {
|
| 245 |
+
this.refreshScrapingDashboard();
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
// Show notification
|
| 249 |
+
showToast(`وضعیت scraping: ${data.status}`, 'info');
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/**
|
| 253 |
+
* Handle health update events
|
| 254 |
+
*/
|
| 255 |
+
handleHealthUpdate(data) {
|
| 256 |
+
console.log('💓 Health update:', data);
|
| 257 |
+
|
| 258 |
+
// Update health indicators on all pages
|
| 259 |
+
this.updateHealthIndicators(data);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/**
|
| 263 |
+
* Refresh document list (if function exists)
|
| 264 |
+
*/
|
| 265 |
+
refreshDocumentList() {
|
| 266 |
+
if (typeof loadDocuments === 'function') {
|
| 267 |
+
loadDocuments();
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
if (typeof refreshDocumentTable === 'function') {
|
| 271 |
+
refreshDocumentTable();
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
/**
|
| 276 |
+
* Update dashboard statistics
|
| 277 |
+
*/
|
| 278 |
+
async updateDashboardStats() {
|
| 279 |
+
try {
|
| 280 |
+
const summary = await this.apiClient.getDashboardSummary();
|
| 281 |
+
this.broadcast('dashboardStatsUpdated', summary);
|
| 282 |
+
|
| 283 |
+
// Update dashboard if on dashboard page
|
| 284 |
+
if (window.location.pathname.includes('improved_legal_dashboard.html')) {
|
| 285 |
+
if (typeof updateDashboardStats === 'function') {
|
| 286 |
+
updateDashboardStats(summary);
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
} catch (error) {
|
| 290 |
+
console.error('Failed to update dashboard stats:', error);
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
/**
|
| 295 |
+
* Refresh scraping dashboard
|
| 296 |
+
*/
|
| 297 |
+
refreshScrapingDashboard() {
|
| 298 |
+
if (typeof loadScrapingData === 'function') {
|
| 299 |
+
loadScrapingData();
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
if (typeof updateScrapingStatus === 'function') {
|
| 303 |
+
updateScrapingStatus();
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
/**
|
| 308 |
+
* Update health indicators
|
| 309 |
+
*/
|
| 310 |
+
updateHealthIndicators(healthData) {
|
| 311 |
+
const healthElements = document.querySelectorAll('.health-indicator');
|
| 312 |
+
|
| 313 |
+
healthElements.forEach(element => {
|
| 314 |
+
const status = healthData.status || 'unknown';
|
| 315 |
+
element.className = `health-indicator ${status}`;
|
| 316 |
+
element.textContent = status === 'healthy' ? '🟢' : '🔴';
|
| 317 |
+
});
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
/**
|
| 321 |
+
* Get cached data
|
| 322 |
+
*/
|
| 323 |
+
getCachedData(key) {
|
| 324 |
+
return this.cache.get(key);
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
/**
|
| 328 |
+
* Set cached data
|
| 329 |
+
*/
|
| 330 |
+
setCachedData(key, data) {
|
| 331 |
+
this.cache.set(key, data);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
/**
|
| 335 |
+
* Clear cache
|
| 336 |
+
*/
|
| 337 |
+
clearCache() {
|
| 338 |
+
this.cache.clear();
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
/**
|
| 342 |
+
* Get API client
|
| 343 |
+
*/
|
| 344 |
+
getApiClient() {
|
| 345 |
+
return this.apiClient;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
/**
|
| 349 |
+
* Force refresh all data
|
| 350 |
+
*/
|
| 351 |
+
async forceRefresh() {
|
| 352 |
+
console.log('🔄 Force refreshing all data...');
|
| 353 |
+
|
| 354 |
+
try {
|
| 355 |
+
// Clear cache
|
| 356 |
+
this.clearCache();
|
| 357 |
+
|
| 358 |
+
// Refresh document list
|
| 359 |
+
this.refreshDocumentList();
|
| 360 |
+
|
| 361 |
+
// Update dashboard stats
|
| 362 |
+
await this.updateDashboardStats();
|
| 363 |
+
|
| 364 |
+
// Refresh scraping data
|
| 365 |
+
this.refreshScrapingDashboard();
|
| 366 |
+
|
| 367 |
+
this.broadcast('dataRefreshed', { timestamp: Date.now() });
|
| 368 |
+
|
| 369 |
+
showToast('دادهها با موفقیت بهروزرسانی شدند', 'success');
|
| 370 |
+
} catch (error) {
|
| 371 |
+
console.error('Failed to force refresh:', error);
|
| 372 |
+
showToast('خطا در بهروزرسانی دادهها', 'error');
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// Global instance
|
| 378 |
+
const dashboardCore = new DashboardCore();
|
| 379 |
+
|
| 380 |
+
// Export for use in other modules
|
| 381 |
+
if (typeof module !== 'undefined' && module.exports) {
|
| 382 |
+
module.exports = DashboardCore;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// Make available globally
|
| 386 |
+
window.dashboardCore = dashboardCore;
|
| 387 |
+
|
| 388 |
+
console.log('📦 Dashboard Core module loaded');
|
app/static/js/document-crud.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Document CRUD Handler for Legal Dashboard
|
| 3 |
+
* Manages Create, Read, Update, Delete operations for documents
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
class DocumentCRUDHandler {
|
| 7 |
+
constructor() {
|
| 8 |
+
this.baseEndpoint = '/api/documents';
|
| 9 |
+
this.documents = [];
|
| 10 |
+
this.currentEditId = null;
|
| 11 |
+
this.searchQuery = '';
|
| 12 |
+
this.filters = {
|
| 13 |
+
status: 'all',
|
| 14 |
+
category: 'all',
|
| 15 |
+
dateFrom: '',
|
| 16 |
+
dateTo: ''
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
this.initializeEventListeners();
|
| 20 |
+
this.loadDocuments();
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
initializeEventListeners() {
|
| 24 |
+
// Create document button
|
| 25 |
+
const createBtn = document.getElementById('createDocumentBtn');
|
| 26 |
+
if (createBtn) {
|
| 27 |
+
createBtn.addEventListener('click', () => this.showCreateModal());
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// Search input
|
| 31 |
+
const searchInput = document.getElementById('documentSearch');
|
| 32 |
+
if (searchInput) {
|
| 33 |
+
searchInput.addEventListener('input', (e) => {
|
| 34 |
+
this.searchQuery = e.target.value;
|
| 35 |
+
this.filterDocuments();
|
| 36 |
+
});
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Filter selects
|
| 40 |
+
const statusFilter = document.getElementById('statusFilter');
|
| 41 |
+
if (statusFilter) {
|
| 42 |
+
statusFilter.addEventListener('change', (e) => {
|
| 43 |
+
this.filters.status = e.target.value;
|
| 44 |
+
this.filterDocuments();
|
| 45 |
+
});
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const categoryFilter = document.getElementById('categoryFilter');
|
| 49 |
+
if (categoryFilter) {
|
| 50 |
+
categoryFilter.addEventListener('change', (e) => {
|
| 51 |
+
this.filters.category = e.target.value;
|
| 52 |
+
this.filterDocuments();
|
| 53 |
+
});
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Date filters
|
| 57 |
+
const dateFromFilter = document.getElementById('dateFromFilter');
|
| 58 |
+
if (dateFromFilter) {
|
| 59 |
+
dateFromFilter.addEventListener('change', (e) => {
|
| 60 |
+
this.filters.dateFrom = e.target.value;
|
| 61 |
+
this.filterDocuments();
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
const dateToFilter = document.getElementById('dateToFilter');
|
| 66 |
+
if (dateToFilter) {
|
| 67 |
+
dateToFilter.addEventListener('change', (e) => {
|
| 68 |
+
this.filters.dateTo = e.target.value;
|
| 69 |
+
this.filterDocuments();
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
async loadDocuments() {
|
| 75 |
+
try {
|
| 76 |
+
const response = await fetchWithErrorHandling(this.baseEndpoint);
|
| 77 |
+
this.documents = response.documents || [];
|
| 78 |
+
this.renderDocuments();
|
| 79 |
+
} catch (error) {
|
| 80 |
+
console.error('Failed to load documents:', error);
|
| 81 |
+
this.showToast('خطا در بارگذاری اسناد', 'error');
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
async createDocument(documentData) {
|
| 86 |
+
try {
|
| 87 |
+
const response = await fetchWithErrorHandling(this.baseEndpoint, {
|
| 88 |
+
method: 'POST',
|
| 89 |
+
body: JSON.stringify(documentData)
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
this.showToast('سند با موفقیت ایجاد شد', 'success');
|
| 93 |
+
this.loadDocuments();
|
| 94 |
+
return response;
|
| 95 |
+
} catch (error) {
|
| 96 |
+
this.showToast(`خطا در ایجاد سند: ${error.message}`, 'error');
|
| 97 |
+
throw error;
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
async updateDocument(id, documentData) {
|
| 102 |
+
try {
|
| 103 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/${id}`, {
|
| 104 |
+
method: 'PUT',
|
| 105 |
+
body: JSON.stringify(documentData)
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
this.showToast('سند با موفقیت بهروزرسانی شد', 'success');
|
| 109 |
+
this.loadDocuments();
|
| 110 |
+
return response;
|
| 111 |
+
} catch (error) {
|
| 112 |
+
this.showToast(`خطا در بهروزرسانی سند: ${error.message}`, 'error');
|
| 113 |
+
throw error;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
async deleteDocument(id) {
|
| 118 |
+
try {
|
| 119 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/${id}`, {
|
| 120 |
+
method: 'DELETE'
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
this.showToast('سند با موفقیت حذف شد', 'success');
|
| 124 |
+
this.loadDocuments();
|
| 125 |
+
return response;
|
| 126 |
+
} catch (error) {
|
| 127 |
+
this.showToast(`خطا در حذف سند: ${error.message}`, 'error');
|
| 128 |
+
throw error;
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async searchDocuments(query) {
|
| 133 |
+
try {
|
| 134 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/search?q=${encodeURIComponent(query)}`);
|
| 135 |
+
return response.results || [];
|
| 136 |
+
} catch (error) {
|
| 137 |
+
console.error('Search failed:', error);
|
| 138 |
+
return [];
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
renderDocuments() {
|
| 143 |
+
const container = document.getElementById('documentsList');
|
| 144 |
+
if (!container) return;
|
| 145 |
+
|
| 146 |
+
if (this.documents.length === 0) {
|
| 147 |
+
container.innerHTML = '<p class="no-documents">هیچ سندی یافت نشد</p>';
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
const documentsHTML = this.documents.map(doc => this.renderDocumentItem(doc)).join('');
|
| 152 |
+
container.innerHTML = documentsHTML;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
renderDocumentItem(doc) {
|
| 156 |
+
const statusClass = this.getStatusClass(doc.status);
|
| 157 |
+
const qualityColor = this.getQualityColor(doc.quality);
|
| 158 |
+
|
| 159 |
+
return `
|
| 160 |
+
<div class="document-item" data-id="${doc.id}">
|
| 161 |
+
<div class="document-header">
|
| 162 |
+
<h4 class="document-title" data-id="${doc.id}">${doc.title}</h4>
|
| 163 |
+
<div class="document-actions">
|
| 164 |
+
<button class="btn-edit" onclick="documentCRUDHandler.editDocument(${doc.id})">
|
| 165 |
+
<i class="fas fa-edit"></i>
|
| 166 |
+
</button>
|
| 167 |
+
<button class="btn-delete" onclick="documentCRUDHandler.confirmDelete(${doc.id})">
|
| 168 |
+
<i class="fas fa-trash"></i>
|
| 169 |
+
</button>
|
| 170 |
+
</div>
|
| 171 |
+
</div>
|
| 172 |
+
<div class="document-details">
|
| 173 |
+
<div class="document-info">
|
| 174 |
+
<span class="document-status ${statusClass}">${this.getStatusText(doc.status)}</span>
|
| 175 |
+
<span class="document-quality" style="color: ${qualityColor}">
|
| 176 |
+
کیفیت: ${(doc.quality || 0).toFixed(1)}%
|
| 177 |
+
</span>
|
| 178 |
+
<span class="document-date">${this.formatDate(doc.created_at)}</span>
|
| 179 |
+
</div>
|
| 180 |
+
<div class="document-content">
|
| 181 |
+
<p class="document-description">${doc.description || 'توضیحات موجود نیست'}</p>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
`;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
showCreateModal() {
|
| 189 |
+
const modal = document.getElementById('createDocumentModal');
|
| 190 |
+
if (modal) {
|
| 191 |
+
modal.style.display = 'block';
|
| 192 |
+
this.resetCreateForm();
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
hideCreateModal() {
|
| 197 |
+
const modal = document.getElementById('createDocumentModal');
|
| 198 |
+
if (modal) {
|
| 199 |
+
modal.style.display = 'none';
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
resetCreateForm() {
|
| 204 |
+
const form = document.getElementById('createDocumentForm');
|
| 205 |
+
if (form) {
|
| 206 |
+
form.reset();
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
async handleCreateDocument(event) {
|
| 211 |
+
event.preventDefault();
|
| 212 |
+
|
| 213 |
+
const formData = new FormData(event.target);
|
| 214 |
+
const documentData = {
|
| 215 |
+
title: formData.get('title'),
|
| 216 |
+
description: formData.get('description'),
|
| 217 |
+
category: formData.get('category'),
|
| 218 |
+
status: 'pending'
|
| 219 |
+
};
|
| 220 |
+
|
| 221 |
+
try {
|
| 222 |
+
await this.createDocument(documentData);
|
| 223 |
+
this.hideCreateModal();
|
| 224 |
+
} catch (error) {
|
| 225 |
+
console.error('Create document failed:', error);
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
editDocument(id) {
|
| 230 |
+
const document = this.documents.find(doc => doc.id === id);
|
| 231 |
+
if (!document) return;
|
| 232 |
+
|
| 233 |
+
this.currentEditId = id;
|
| 234 |
+
this.showEditModal(document);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
showEditModal(document) {
|
| 238 |
+
const modal = document.getElementById('editDocumentModal');
|
| 239 |
+
if (modal) {
|
| 240 |
+
// Populate form fields
|
| 241 |
+
const titleInput = modal.querySelector('#editTitle');
|
| 242 |
+
const descriptionInput = modal.querySelector('#editDescription');
|
| 243 |
+
const categoryInput = modal.querySelector('#editCategory');
|
| 244 |
+
const statusInput = modal.querySelector('#editStatus');
|
| 245 |
+
|
| 246 |
+
if (titleInput) titleInput.value = document.title;
|
| 247 |
+
if (descriptionInput) descriptionInput.value = document.description || '';
|
| 248 |
+
if (categoryInput) categoryInput.value = document.category || '';
|
| 249 |
+
if (statusInput) statusInput.value = document.status || 'pending';
|
| 250 |
+
|
| 251 |
+
modal.style.display = 'block';
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
hideEditModal() {
|
| 256 |
+
const modal = document.getElementById('editDocumentModal');
|
| 257 |
+
if (modal) {
|
| 258 |
+
modal.style.display = 'none';
|
| 259 |
+
this.currentEditId = null;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
async handleEditDocument(event) {
|
| 264 |
+
event.preventDefault();
|
| 265 |
+
|
| 266 |
+
if (!this.currentEditId) return;
|
| 267 |
+
|
| 268 |
+
const formData = new FormData(event.target);
|
| 269 |
+
const documentData = {
|
| 270 |
+
title: formData.get('title'),
|
| 271 |
+
description: formData.get('description'),
|
| 272 |
+
category: formData.get('category'),
|
| 273 |
+
status: formData.get('status')
|
| 274 |
+
};
|
| 275 |
+
|
| 276 |
+
try {
|
| 277 |
+
await this.updateDocument(this.currentEditId, documentData);
|
| 278 |
+
this.hideEditModal();
|
| 279 |
+
} catch (error) {
|
| 280 |
+
console.error('Update document failed:', error);
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
confirmDelete(id) {
|
| 285 |
+
const document = this.documents.find(doc => doc.id === id);
|
| 286 |
+
if (!document) return;
|
| 287 |
+
|
| 288 |
+
const confirmed = confirm(`آیا از حذف سند "${document.title}" اطمینان دارید؟`);
|
| 289 |
+
if (confirmed) {
|
| 290 |
+
this.deleteDocument(id);
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
filterDocuments() {
|
| 295 |
+
let filtered = this.documents;
|
| 296 |
+
|
| 297 |
+
// Apply search filter
|
| 298 |
+
if (this.searchQuery) {
|
| 299 |
+
filtered = filtered.filter(doc =>
|
| 300 |
+
doc.title.toLowerCase().includes(this.searchQuery.toLowerCase()) ||
|
| 301 |
+
(doc.description && doc.description.toLowerCase().includes(this.searchQuery.toLowerCase()))
|
| 302 |
+
);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
// Apply status filter
|
| 306 |
+
if (this.filters.status !== 'all') {
|
| 307 |
+
filtered = filtered.filter(doc => doc.status === this.filters.status);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// Apply category filter
|
| 311 |
+
if (this.filters.category !== 'all') {
|
| 312 |
+
filtered = filtered.filter(doc => doc.category === this.filters.category);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
// Apply date filters
|
| 316 |
+
if (this.filters.dateFrom) {
|
| 317 |
+
filtered = filtered.filter(doc =>
|
| 318 |
+
new Date(doc.created_at) >= new Date(this.filters.dateFrom)
|
| 319 |
+
);
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
if (this.filters.dateTo) {
|
| 323 |
+
filtered = filtered.filter(doc =>
|
| 324 |
+
new Date(doc.created_at) <= new Date(this.filters.dateTo)
|
| 325 |
+
);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
this.renderFilteredDocuments(filtered);
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
renderFilteredDocuments(filtered) {
|
| 332 |
+
const container = document.getElementById('documentsList');
|
| 333 |
+
if (!container) return;
|
| 334 |
+
|
| 335 |
+
if (filtered.length === 0) {
|
| 336 |
+
container.innerHTML = '<p class="no-results">هیچ نتیجهای یافت نشد</p>';
|
| 337 |
+
return;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
const documentsHTML = filtered.map(doc => this.renderDocumentItem(doc)).join('');
|
| 341 |
+
container.innerHTML = documentsHTML;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
getStatusClass(status) {
|
| 345 |
+
const statusMap = {
|
| 346 |
+
'pending': 'status-pending',
|
| 347 |
+
'processing': 'status-processing',
|
| 348 |
+
'completed': 'status-completed',
|
| 349 |
+
'error': 'status-error'
|
| 350 |
+
};
|
| 351 |
+
return statusMap[status] || 'status-unknown';
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
getStatusText(status) {
|
| 355 |
+
const statusMap = {
|
| 356 |
+
'pending': 'در انتظار',
|
| 357 |
+
'processing': 'در حال پردازش',
|
| 358 |
+
'completed': 'تکمیل شده',
|
| 359 |
+
'error': 'خطا'
|
| 360 |
+
};
|
| 361 |
+
return statusMap[status] || 'نامشخص';
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
getQualityColor(quality) {
|
| 365 |
+
if (quality >= 80) return '#28a745';
|
| 366 |
+
if (quality >= 60) return '#ffc107';
|
| 367 |
+
return '#dc3545';
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
formatDate(dateString) {
|
| 371 |
+
if (!dateString) return 'نامشخص';
|
| 372 |
+
const date = new Date(dateString);
|
| 373 |
+
return date.toLocaleDateString('fa-IR');
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
showToast(message, type = 'info') {
|
| 377 |
+
if (typeof showToast === 'function') {
|
| 378 |
+
showToast(message, type);
|
| 379 |
+
} else {
|
| 380 |
+
console.log(`${type.toUpperCase()}: ${message}`);
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// Initialize document CRUD handler
|
| 386 |
+
const documentCRUDHandler = new DocumentCRUDHandler();
|
app/static/js/file-upload-handler.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* File Upload Handler for Legal Dashboard
|
| 3 |
+
* Manages document uploads, OCR processing, and real-time progress
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
class FileUploadHandler {
|
| 7 |
+
constructor() {
|
| 8 |
+
this.uploadEndpoint = '/api/ocr/upload';
|
| 9 |
+
this.processEndpoint = '/api/ocr/process';
|
| 10 |
+
this.maxFileSize = 10 * 1024 * 1024; // 10MB
|
| 11 |
+
this.allowedTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/tiff'];
|
| 12 |
+
this.currentUpload = null;
|
| 13 |
+
this.uploadQueue = [];
|
| 14 |
+
|
| 15 |
+
this.initializeEventListeners();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
initializeEventListeners() {
|
| 19 |
+
// File input change
|
| 20 |
+
const fileInput = document.getElementById('documentUpload');
|
| 21 |
+
if (fileInput) {
|
| 22 |
+
fileInput.addEventListener('change', (e) => this.handleFileSelection(e));
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Upload button
|
| 26 |
+
const uploadBtn = document.getElementById('uploadButton');
|
| 27 |
+
if (uploadBtn) {
|
| 28 |
+
uploadBtn.addEventListener('click', () => this.startUpload());
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Drag and drop
|
| 32 |
+
const dropZone = document.getElementById('uploadDropZone');
|
| 33 |
+
if (dropZone) {
|
| 34 |
+
dropZone.addEventListener('dragover', (e) => this.handleDragOver(e));
|
| 35 |
+
dropZone.addEventListener('drop', (e) => this.handleDrop(e));
|
| 36 |
+
dropZone.addEventListener('dragleave', (e) => this.handleDragLeave(e));
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
handleFileSelection(event) {
|
| 41 |
+
const files = event.target.files;
|
| 42 |
+
if (files.length > 0) {
|
| 43 |
+
this.validateAndQueueFiles(files);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
handleDragOver(event) {
|
| 48 |
+
event.preventDefault();
|
| 49 |
+
event.currentTarget.classList.add('drag-over');
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
handleDragLeave(event) {
|
| 53 |
+
event.preventDefault();
|
| 54 |
+
event.currentTarget.classList.remove('drag-over');
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
handleDrop(event) {
|
| 58 |
+
event.preventDefault();
|
| 59 |
+
event.currentTarget.classList.remove('drag-over');
|
| 60 |
+
|
| 61 |
+
const files = event.dataTransfer.files;
|
| 62 |
+
if (files.length > 0) {
|
| 63 |
+
this.validateAndQueueFiles(files);
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
validateAndQueueFiles(files) {
|
| 68 |
+
const validFiles = [];
|
| 69 |
+
const errors = [];
|
| 70 |
+
|
| 71 |
+
for (let file of files) {
|
| 72 |
+
// Check file size
|
| 73 |
+
if (file.size > this.maxFileSize) {
|
| 74 |
+
errors.push(`${file.name}: File too large (max 10MB)`);
|
| 75 |
+
continue;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// Check file type
|
| 79 |
+
if (!this.allowedTypes.includes(file.type)) {
|
| 80 |
+
errors.push(`${file.name}: Unsupported file type`);
|
| 81 |
+
continue;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
validFiles.push(file);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Show errors if any
|
| 88 |
+
if (errors.length > 0) {
|
| 89 |
+
this.showErrors(errors);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Queue valid files
|
| 93 |
+
if (validFiles.length > 0) {
|
| 94 |
+
this.uploadQueue.push(...validFiles);
|
| 95 |
+
this.updateUploadQueue();
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
updateUploadQueue() {
|
| 100 |
+
const queueContainer = document.getElementById('uploadQueue');
|
| 101 |
+
if (!queueContainer) return;
|
| 102 |
+
|
| 103 |
+
if (this.uploadQueue.length === 0) {
|
| 104 |
+
queueContainer.innerHTML = '<p class="no-files">هیچ فایلی برای آپلود انتخاب نشده</p>';
|
| 105 |
+
return;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
const queueHTML = this.uploadQueue.map((file, index) => `
|
| 109 |
+
<div class="queue-item" data-index="${index}">
|
| 110 |
+
<div class="file-info">
|
| 111 |
+
<span class="file-name">${file.name}</span>
|
| 112 |
+
<span class="file-size">${this.formatFileSize(file.size)}</span>
|
| 113 |
+
</div>
|
| 114 |
+
<div class="file-actions">
|
| 115 |
+
<button class="remove-file" onclick="fileUploadHandler.removeFromQueue(${index})">
|
| 116 |
+
<i class="fas fa-times"></i>
|
| 117 |
+
</button>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
`).join('');
|
| 121 |
+
|
| 122 |
+
queueContainer.innerHTML = queueHTML;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
removeFromQueue(index) {
|
| 126 |
+
this.uploadQueue.splice(index, 1);
|
| 127 |
+
this.updateUploadQueue();
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
async startUpload() {
|
| 131 |
+
if (this.uploadQueue.length === 0) {
|
| 132 |
+
this.showToast('لطفاً فایلی برای آپلود انتخاب کنید', 'warning');
|
| 133 |
+
return;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
if (this.currentUpload) {
|
| 137 |
+
this.showToast('آپلود در حال انجام است، لطفاً صبر کنید', 'warning');
|
| 138 |
+
return;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
this.currentUpload = true;
|
| 142 |
+
this.showUploadProgress();
|
| 143 |
+
|
| 144 |
+
try {
|
| 145 |
+
for (let i = 0; i < this.uploadQueue.length; i++) {
|
| 146 |
+
const file = this.uploadQueue[i];
|
| 147 |
+
await this.uploadFile(file, i + 1, this.uploadQueue.length);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
this.showToast('تمام فایلها با موفقیت آپلود شدند', 'success');
|
| 151 |
+
this.refreshDocumentsList();
|
| 152 |
+
} catch (error) {
|
| 153 |
+
this.showToast(`خطا در آپلود: ${error.message}`, 'error');
|
| 154 |
+
} finally {
|
| 155 |
+
this.currentUpload = false;
|
| 156 |
+
this.hideUploadProgress();
|
| 157 |
+
this.uploadQueue = [];
|
| 158 |
+
this.updateUploadQueue();
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
async uploadFile(file, current, total) {
|
| 163 |
+
return new Promise((resolve, reject) => {
|
| 164 |
+
const formData = new FormData();
|
| 165 |
+
formData.append('file', file);
|
| 166 |
+
formData.append('filename', file.name);
|
| 167 |
+
|
| 168 |
+
const xhr = new XMLHttpRequest();
|
| 169 |
+
|
| 170 |
+
// Progress tracking
|
| 171 |
+
xhr.upload.addEventListener('progress', (e) => {
|
| 172 |
+
if (e.lengthComputable) {
|
| 173 |
+
const percentComplete = (e.loaded / e.total) * 100;
|
| 174 |
+
this.updateProgress(percentComplete, current, total);
|
| 175 |
+
}
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
// Response handling
|
| 179 |
+
xhr.addEventListener('load', () => {
|
| 180 |
+
if (xhr.status === 200) {
|
| 181 |
+
try {
|
| 182 |
+
const response = JSON.parse(xhr.responseText);
|
| 183 |
+
this.handleUploadSuccess(response, file);
|
| 184 |
+
resolve(response);
|
| 185 |
+
} catch (error) {
|
| 186 |
+
reject(new Error('Invalid response format'));
|
| 187 |
+
}
|
| 188 |
+
} else {
|
| 189 |
+
reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
|
| 190 |
+
}
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
xhr.addEventListener('error', () => {
|
| 194 |
+
reject(new Error('Network error during upload'));
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
xhr.addEventListener('abort', () => {
|
| 198 |
+
reject(new Error('Upload cancelled'));
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
// Start upload
|
| 202 |
+
xhr.open('POST', this.uploadEndpoint);
|
| 203 |
+
xhr.send(formData);
|
| 204 |
+
});
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
handleUploadSuccess(response, file) {
|
| 208 |
+
// Update dashboard stats
|
| 209 |
+
this.updateDashboardStats();
|
| 210 |
+
|
| 211 |
+
// Show success message
|
| 212 |
+
this.showToast(`${file.name} با موفقیت آپلود شد`, 'success');
|
| 213 |
+
|
| 214 |
+
// Process OCR if needed
|
| 215 |
+
if (response.document_id) {
|
| 216 |
+
this.processOCR(response.document_id, file.name);
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
async processOCR(documentId, fileName) {
|
| 221 |
+
try {
|
| 222 |
+
const response = await fetch(this.processEndpoint, {
|
| 223 |
+
method: 'POST',
|
| 224 |
+
headers: {
|
| 225 |
+
'Content-Type': 'application/json',
|
| 226 |
+
},
|
| 227 |
+
body: JSON.stringify({
|
| 228 |
+
document_id: documentId,
|
| 229 |
+
filename: fileName
|
| 230 |
+
})
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
+
if (!response.ok) {
|
| 234 |
+
throw new Error(`OCR processing failed: ${response.status}`);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
const result = await response.json();
|
| 238 |
+
this.showOCRResult(result, fileName);
|
| 239 |
+
} catch (error) {
|
| 240 |
+
this.showToast(`خطا در پردازش OCR: ${error.message}`, 'error');
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
showOCRResult(result, fileName) {
|
| 245 |
+
const ocrResultsContainer = document.getElementById('ocrResults');
|
| 246 |
+
if (!ocrResultsContainer) return;
|
| 247 |
+
|
| 248 |
+
const resultHTML = `
|
| 249 |
+
<div class="ocr-result">
|
| 250 |
+
<h4>نتایج OCR - ${fileName}</h4>
|
| 251 |
+
<div class="ocr-content">
|
| 252 |
+
<p><strong>کیفیت:</strong> ${(result.quality || 0).toFixed(2)}%</p>
|
| 253 |
+
<p><strong>متن استخراج شده:</strong></p>
|
| 254 |
+
<div class="extracted-text">
|
| 255 |
+
${result.text || 'متنی استخراج نشد'}
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
`;
|
| 260 |
+
|
| 261 |
+
ocrResultsContainer.insertAdjacentHTML('afterbegin', resultHTML);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
updateProgress(percent, current, total) {
|
| 265 |
+
const progressBar = document.getElementById('uploadProgressBar');
|
| 266 |
+
const progressText = document.getElementById('uploadProgressText');
|
| 267 |
+
|
| 268 |
+
if (progressBar) {
|
| 269 |
+
progressBar.style.width = `${percent}%`;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
if (progressText) {
|
| 273 |
+
progressText.textContent = `آپلود فایل ${current} از ${total} (${Math.round(percent)}%)`;
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
showUploadProgress() {
|
| 278 |
+
const progressContainer = document.getElementById('uploadProgress');
|
| 279 |
+
if (progressContainer) {
|
| 280 |
+
progressContainer.style.display = 'block';
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
hideUploadProgress() {
|
| 285 |
+
const progressContainer = document.getElementById('uploadProgress');
|
| 286 |
+
if (progressContainer) {
|
| 287 |
+
progressContainer.style.display = 'none';
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
updateDashboardStats() {
|
| 292 |
+
// Trigger dashboard stats refresh
|
| 293 |
+
if (typeof loadDashboardStats === 'function') {
|
| 294 |
+
loadDashboardStats();
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
refreshDocumentsList() {
|
| 299 |
+
// Trigger documents list refresh
|
| 300 |
+
if (typeof loadDocumentsList === 'function') {
|
| 301 |
+
loadDocumentsList();
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
showErrors(errors) {
|
| 306 |
+
const errorMessage = errors.join('\n');
|
| 307 |
+
this.showToast(errorMessage, 'error');
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
showToast(message, type = 'info') {
|
| 311 |
+
if (typeof showToast === 'function') {
|
| 312 |
+
showToast(message, type);
|
| 313 |
+
} else {
|
| 314 |
+
console.log(`${type.toUpperCase()}: ${message}`);
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
formatFileSize(bytes) {
|
| 319 |
+
if (bytes === 0) return '0 Bytes';
|
| 320 |
+
const k = 1024;
|
| 321 |
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
| 322 |
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
| 323 |
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
| 324 |
+
}
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
// Initialize file upload handler
|
| 328 |
+
const fileUploadHandler = new FileUploadHandler();
|
app/static/js/notifications.js
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Notification System for Legal Dashboard
|
| 3 |
+
* ======================================
|
| 4 |
+
*
|
| 5 |
+
* Handles real-time notifications, WebSocket connections, and notification management.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
class NotificationManager {
|
| 9 |
+
constructor() {
|
| 10 |
+
this.websocket = null;
|
| 11 |
+
this.reconnectAttempts = 0;
|
| 12 |
+
this.maxReconnectAttempts = 5;
|
| 13 |
+
this.reconnectDelay = 1000;
|
| 14 |
+
this.notifications = [];
|
| 15 |
+
this.unreadCount = 0;
|
| 16 |
+
this.isConnected = false;
|
| 17 |
+
this.userId = null;
|
| 18 |
+
|
| 19 |
+
// Initialize notification elements
|
| 20 |
+
this.initElements();
|
| 21 |
+
|
| 22 |
+
// Start WebSocket connection
|
| 23 |
+
this.connectWebSocket();
|
| 24 |
+
|
| 25 |
+
// Set up periodic cleanup
|
| 26 |
+
setInterval(() => this.cleanupExpiredNotifications(), 60000); // Every minute
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
initElements() {
|
| 30 |
+
// Create notification container if it doesn't exist
|
| 31 |
+
if (!document.getElementById('notification-container')) {
|
| 32 |
+
const container = document.createElement('div');
|
| 33 |
+
container.id = 'notification-container';
|
| 34 |
+
container.className = 'notification-container';
|
| 35 |
+
document.body.appendChild(container);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Create notification bell if it doesn't exist
|
| 39 |
+
if (!document.getElementById('notification-bell')) {
|
| 40 |
+
const bell = document.createElement('div');
|
| 41 |
+
bell.id = 'notification-bell';
|
| 42 |
+
bell.className = 'notification-bell';
|
| 43 |
+
bell.innerHTML = `
|
| 44 |
+
<i class="fas fa-bell"></i>
|
| 45 |
+
<span class="notification-badge" id="notification-badge">0</span>
|
| 46 |
+
`;
|
| 47 |
+
bell.addEventListener('click', () => this.toggleNotificationPanel());
|
| 48 |
+
document.body.appendChild(bell);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Create notification panel if it doesn't exist
|
| 52 |
+
if (!document.getElementById('notification-panel')) {
|
| 53 |
+
const panel = document.createElement('div');
|
| 54 |
+
panel.id = 'notification-panel';
|
| 55 |
+
panel.className = 'notification-panel hidden';
|
| 56 |
+
panel.innerHTML = `
|
| 57 |
+
<div class="notification-header">
|
| 58 |
+
<h3>Notifications</h3>
|
| 59 |
+
<button class="close-btn" onclick="notificationManager.closeNotificationPanel()">×</button>
|
| 60 |
+
</div>
|
| 61 |
+
<div class="notification-list" id="notification-list"></div>
|
| 62 |
+
<div class="notification-footer">
|
| 63 |
+
<button onclick="notificationManager.markAllAsRead()">Mark All as Read</button>
|
| 64 |
+
<button onclick="notificationManager.clearAll()">Clear All</button>
|
| 65 |
+
</div>
|
| 66 |
+
`;
|
| 67 |
+
document.body.appendChild(panel);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
// Add CSS styles
|
| 71 |
+
this.addStyles();
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
addStyles() {
|
| 75 |
+
if (!document.getElementById('notification-styles')) {
|
| 76 |
+
const styles = document.createElement('style');
|
| 77 |
+
styles.id = 'notification-styles';
|
| 78 |
+
styles.textContent = `
|
| 79 |
+
.notification-container {
|
| 80 |
+
position: fixed;
|
| 81 |
+
top: 20px;
|
| 82 |
+
right: 20px;
|
| 83 |
+
z-index: 10000;
|
| 84 |
+
max-width: 400px;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.notification-bell {
|
| 88 |
+
position: fixed;
|
| 89 |
+
top: 20px;
|
| 90 |
+
right: 20px;
|
| 91 |
+
background: #007bff;
|
| 92 |
+
color: white;
|
| 93 |
+
padding: 10px;
|
| 94 |
+
border-radius: 50%;
|
| 95 |
+
cursor: pointer;
|
| 96 |
+
z-index: 10001;
|
| 97 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
| 98 |
+
transition: all 0.3s ease;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.notification-bell:hover {
|
| 102 |
+
transform: scale(1.1);
|
| 103 |
+
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.notification-badge {
|
| 107 |
+
position: absolute;
|
| 108 |
+
top: -5px;
|
| 109 |
+
right: -5px;
|
| 110 |
+
background: #dc3545;
|
| 111 |
+
color: white;
|
| 112 |
+
border-radius: 50%;
|
| 113 |
+
padding: 2px 6px;
|
| 114 |
+
font-size: 12px;
|
| 115 |
+
min-width: 18px;
|
| 116 |
+
text-align: center;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.notification-panel {
|
| 120 |
+
position: fixed;
|
| 121 |
+
top: 70px;
|
| 122 |
+
right: 20px;
|
| 123 |
+
width: 350px;
|
| 124 |
+
max-height: 500px;
|
| 125 |
+
background: white;
|
| 126 |
+
border-radius: 8px;
|
| 127 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
| 128 |
+
z-index: 10002;
|
| 129 |
+
overflow: hidden;
|
| 130 |
+
transition: all 0.3s ease;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.notification-panel.hidden {
|
| 134 |
+
transform: translateX(100%);
|
| 135 |
+
opacity: 0;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.notification-header {
|
| 139 |
+
padding: 15px;
|
| 140 |
+
border-bottom: 1px solid #eee;
|
| 141 |
+
display: flex;
|
| 142 |
+
justify-content: space-between;
|
| 143 |
+
align-items: center;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.notification-header h3 {
|
| 147 |
+
margin: 0;
|
| 148 |
+
color: #333;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.close-btn {
|
| 152 |
+
background: none;
|
| 153 |
+
border: none;
|
| 154 |
+
font-size: 20px;
|
| 155 |
+
cursor: pointer;
|
| 156 |
+
color: #666;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.notification-list {
|
| 160 |
+
max-height: 350px;
|
| 161 |
+
overflow-y: auto;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.notification-item {
|
| 165 |
+
padding: 15px;
|
| 166 |
+
border-bottom: 1px solid #f0f0f0;
|
| 167 |
+
cursor: pointer;
|
| 168 |
+
transition: background-color 0.2s ease;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.notification-item:hover {
|
| 172 |
+
background-color: #f8f9fa;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.notification-item.unread {
|
| 176 |
+
background-color: #e3f2fd;
|
| 177 |
+
border-left: 4px solid #2196f3;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.notification-title {
|
| 181 |
+
font-weight: bold;
|
| 182 |
+
margin-bottom: 5px;
|
| 183 |
+
color: #333;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.notification-message {
|
| 187 |
+
color: #666;
|
| 188 |
+
font-size: 14px;
|
| 189 |
+
margin-bottom: 5px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.notification-meta {
|
| 193 |
+
font-size: 12px;
|
| 194 |
+
color: #999;
|
| 195 |
+
display: flex;
|
| 196 |
+
justify-content: space-between;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.notification-footer {
|
| 200 |
+
padding: 15px;
|
| 201 |
+
border-top: 1px solid #eee;
|
| 202 |
+
display: flex;
|
| 203 |
+
gap: 10px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.notification-footer button {
|
| 207 |
+
flex: 1;
|
| 208 |
+
padding: 8px;
|
| 209 |
+
border: 1px solid #ddd;
|
| 210 |
+
background: white;
|
| 211 |
+
border-radius: 4px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
font-size: 12px;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.notification-footer button:hover {
|
| 217 |
+
background: #f8f9fa;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.notification-toast {
|
| 221 |
+
background: white;
|
| 222 |
+
border-radius: 8px;
|
| 223 |
+
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
| 224 |
+
margin-bottom: 10px;
|
| 225 |
+
padding: 15px;
|
| 226 |
+
border-left: 4px solid #007bff;
|
| 227 |
+
animation: slideIn 0.3s ease;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.notification-toast.success {
|
| 231 |
+
border-left-color: #28a745;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.notification-toast.warning {
|
| 235 |
+
border-left-color: #ffc107;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.notification-toast.error {
|
| 239 |
+
border-left-color: #dc3545;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
@keyframes slideIn {
|
| 243 |
+
from {
|
| 244 |
+
transform: translateX(100%);
|
| 245 |
+
opacity: 0;
|
| 246 |
+
}
|
| 247 |
+
to {
|
| 248 |
+
transform: translateX(0);
|
| 249 |
+
opacity: 1;
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
.notification-type-icon {
|
| 254 |
+
margin-right: 8px;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.notification-type-icon.info { color: #007bff; }
|
| 258 |
+
.notification-type-icon.success { color: #28a745; }
|
| 259 |
+
.notification-type-icon.warning { color: #ffc107; }
|
| 260 |
+
.notification-type-icon.error { color: #dc3545; }
|
| 261 |
+
`;
|
| 262 |
+
document.head.appendChild(styles);
|
| 263 |
+
}
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
connectWebSocket() {
|
| 267 |
+
try {
|
| 268 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 269 |
+
const wsUrl = `${protocol}//${window.location.host}/api/notifications/ws`;
|
| 270 |
+
|
| 271 |
+
this.websocket = new WebSocket(wsUrl);
|
| 272 |
+
|
| 273 |
+
this.websocket.onopen = () => {
|
| 274 |
+
console.log('WebSocket connected');
|
| 275 |
+
this.isConnected = true;
|
| 276 |
+
this.reconnectAttempts = 0;
|
| 277 |
+
|
| 278 |
+
// Send user authentication if available
|
| 279 |
+
const token = localStorage.getItem('auth_token');
|
| 280 |
+
if (token) {
|
| 281 |
+
this.websocket.send(JSON.stringify({
|
| 282 |
+
type: 'auth',
|
| 283 |
+
token: token
|
| 284 |
+
}));
|
| 285 |
+
}
|
| 286 |
+
};
|
| 287 |
+
|
| 288 |
+
this.websocket.onmessage = (event) => {
|
| 289 |
+
try {
|
| 290 |
+
const data = JSON.parse(event.data);
|
| 291 |
+
this.handleNotification(data);
|
| 292 |
+
} catch (error) {
|
| 293 |
+
console.error('Error parsing WebSocket message:', error);
|
| 294 |
+
}
|
| 295 |
+
};
|
| 296 |
+
|
| 297 |
+
this.websocket.onclose = () => {
|
| 298 |
+
console.log('WebSocket disconnected');
|
| 299 |
+
this.isConnected = false;
|
| 300 |
+
this.handleReconnect();
|
| 301 |
+
};
|
| 302 |
+
|
| 303 |
+
this.websocket.onerror = (error) => {
|
| 304 |
+
console.error('WebSocket error:', error);
|
| 305 |
+
this.isConnected = false;
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
} catch (error) {
|
| 309 |
+
console.error('Error connecting to WebSocket:', error);
|
| 310 |
+
this.handleReconnect();
|
| 311 |
+
}
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
handleReconnect() {
|
| 315 |
+
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
| 316 |
+
this.reconnectAttempts++;
|
| 317 |
+
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
| 318 |
+
|
| 319 |
+
setTimeout(() => {
|
| 320 |
+
this.connectWebSocket();
|
| 321 |
+
}, this.reconnectDelay * this.reconnectAttempts);
|
| 322 |
+
} else {
|
| 323 |
+
console.error('Max reconnection attempts reached');
|
| 324 |
+
this.showToast('Connection lost. Please refresh the page.', 'error');
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
handleNotification(data) {
|
| 329 |
+
if (data.type === 'connection_established') {
|
| 330 |
+
console.log('Notification service connected');
|
| 331 |
+
this.userId = data.user_id;
|
| 332 |
+
return;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
// Add notification to list
|
| 336 |
+
const notification = {
|
| 337 |
+
id: data.id || Date.now(),
|
| 338 |
+
type: data.type || 'info',
|
| 339 |
+
title: data.title || 'Notification',
|
| 340 |
+
message: data.message || '',
|
| 341 |
+
priority: data.priority || 'medium',
|
| 342 |
+
created_at: data.created_at || new Date().toISOString(),
|
| 343 |
+
metadata: data.metadata || {},
|
| 344 |
+
read: false
|
| 345 |
+
};
|
| 346 |
+
|
| 347 |
+
this.notifications.unshift(notification);
|
| 348 |
+
this.unreadCount++;
|
| 349 |
+
this.updateBadge();
|
| 350 |
+
this.showToast(notification);
|
| 351 |
+
this.updateNotificationPanel();
|
| 352 |
+
|
| 353 |
+
// Play notification sound if enabled
|
| 354 |
+
this.playNotificationSound();
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
showToast(notification) {
|
| 358 |
+
const container = document.getElementById('notification-container');
|
| 359 |
+
const toast = document.createElement('div');
|
| 360 |
+
toast.className = `notification-toast ${notification.type}`;
|
| 361 |
+
|
| 362 |
+
const icon = this.getNotificationIcon(notification.type);
|
| 363 |
+
|
| 364 |
+
toast.innerHTML = `
|
| 365 |
+
<div class="notification-title">
|
| 366 |
+
<i class="fas ${icon} notification-type-icon ${notification.type}"></i>
|
| 367 |
+
${notification.title}
|
| 368 |
+
</div>
|
| 369 |
+
<div class="notification-message">${notification.message}</div>
|
| 370 |
+
<div class="notification-meta">
|
| 371 |
+
<span>${this.formatTime(notification.created_at)}</span>
|
| 372 |
+
<span>${notification.priority}</span>
|
| 373 |
+
</div>
|
| 374 |
+
`;
|
| 375 |
+
|
| 376 |
+
container.appendChild(toast);
|
| 377 |
+
|
| 378 |
+
// Auto-remove after 5 seconds
|
| 379 |
+
setTimeout(() => {
|
| 380 |
+
if (toast.parentNode) {
|
| 381 |
+
toast.parentNode.removeChild(toast);
|
| 382 |
+
}
|
| 383 |
+
}, 5000);
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
getNotificationIcon(type) {
|
| 387 |
+
const icons = {
|
| 388 |
+
'info': 'fa-info-circle',
|
| 389 |
+
'success': 'fa-check-circle',
|
| 390 |
+
'warning': 'fa-exclamation-triangle',
|
| 391 |
+
'error': 'fa-times-circle',
|
| 392 |
+
'upload_complete': 'fa-upload',
|
| 393 |
+
'ocr_complete': 'fa-file-text',
|
| 394 |
+
'scraping_complete': 'fa-spider',
|
| 395 |
+
'system_error': 'fa-exclamation-circle',
|
| 396 |
+
'user_activity': 'fa-user'
|
| 397 |
+
};
|
| 398 |
+
return icons[type] || 'fa-bell';
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
formatTime(timestamp) {
|
| 402 |
+
const date = new Date(timestamp);
|
| 403 |
+
const now = new Date();
|
| 404 |
+
const diff = now - date;
|
| 405 |
+
|
| 406 |
+
if (diff < 60000) { // Less than 1 minute
|
| 407 |
+
return 'Just now';
|
| 408 |
+
} else if (diff < 3600000) { // Less than 1 hour
|
| 409 |
+
const minutes = Math.floor(diff / 60000);
|
| 410 |
+
return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
| 411 |
+
} else if (diff < 86400000) { // Less than 1 day
|
| 412 |
+
const hours = Math.floor(diff / 3600000);
|
| 413 |
+
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
| 414 |
+
} else {
|
| 415 |
+
return date.toLocaleDateString();
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
updateBadge() {
|
| 420 |
+
const badge = document.getElementById('notification-badge');
|
| 421 |
+
if (badge) {
|
| 422 |
+
badge.textContent = this.unreadCount;
|
| 423 |
+
badge.style.display = this.unreadCount > 0 ? 'block' : 'none';
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
toggleNotificationPanel() {
|
| 428 |
+
const panel = document.getElementById('notification-panel');
|
| 429 |
+
if (panel) {
|
| 430 |
+
panel.classList.toggle('hidden');
|
| 431 |
+
if (!panel.classList.contains('hidden')) {
|
| 432 |
+
this.updateNotificationPanel();
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
closeNotificationPanel() {
|
| 438 |
+
const panel = document.getElementById('notification-panel');
|
| 439 |
+
if (panel) {
|
| 440 |
+
panel.classList.add('hidden');
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
updateNotificationPanel() {
|
| 445 |
+
const list = document.getElementById('notification-list');
|
| 446 |
+
if (!list) return;
|
| 447 |
+
|
| 448 |
+
list.innerHTML = '';
|
| 449 |
+
|
| 450 |
+
this.notifications.slice(0, 20).forEach(notification => {
|
| 451 |
+
const item = document.createElement('div');
|
| 452 |
+
item.className = `notification-item ${notification.read ? '' : 'unread'}`;
|
| 453 |
+
item.onclick = () => this.markAsRead(notification.id);
|
| 454 |
+
|
| 455 |
+
const icon = this.getNotificationIcon(notification.type);
|
| 456 |
+
|
| 457 |
+
item.innerHTML = `
|
| 458 |
+
<div class="notification-title">
|
| 459 |
+
<i class="fas ${icon} notification-type-icon ${notification.type}"></i>
|
| 460 |
+
${notification.title}
|
| 461 |
+
</div>
|
| 462 |
+
<div class="notification-message">${notification.message}</div>
|
| 463 |
+
<div class="notification-meta">
|
| 464 |
+
<span>${this.formatTime(notification.created_at)}</span>
|
| 465 |
+
<span>${notification.priority}</span>
|
| 466 |
+
</div>
|
| 467 |
+
`;
|
| 468 |
+
|
| 469 |
+
list.appendChild(item);
|
| 470 |
+
});
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
markAsRead(notificationId) {
|
| 474 |
+
const notification = this.notifications.find(n => n.id === notificationId);
|
| 475 |
+
if (notification && !notification.read) {
|
| 476 |
+
notification.read = true;
|
| 477 |
+
this.unreadCount = Math.max(0, this.unreadCount - 1);
|
| 478 |
+
this.updateBadge();
|
| 479 |
+
this.updateNotificationPanel();
|
| 480 |
+
|
| 481 |
+
// Send to server
|
| 482 |
+
this.sendToServer('mark_read', { notification_id: notificationId });
|
| 483 |
+
}
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
markAllAsRead() {
|
| 487 |
+
this.notifications.forEach(notification => {
|
| 488 |
+
notification.read = true;
|
| 489 |
+
});
|
| 490 |
+
this.unreadCount = 0;
|
| 491 |
+
this.updateBadge();
|
| 492 |
+
this.updateNotificationPanel();
|
| 493 |
+
|
| 494 |
+
// Send to server
|
| 495 |
+
this.sendToServer('mark_all_read', {});
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
clearAll() {
|
| 499 |
+
this.notifications = [];
|
| 500 |
+
this.unreadCount = 0;
|
| 501 |
+
this.updateBadge();
|
| 502 |
+
this.updateNotificationPanel();
|
| 503 |
+
|
| 504 |
+
// Send to server
|
| 505 |
+
this.sendToServer('clear_all', {});
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
sendToServer(action, data) {
|
| 509 |
+
if (this.websocket && this.isConnected) {
|
| 510 |
+
this.websocket.send(JSON.stringify({
|
| 511 |
+
action: action,
|
| 512 |
+
...data
|
| 513 |
+
}));
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
playNotificationSound() {
|
| 518 |
+
// Create audio context for notification sound
|
| 519 |
+
try {
|
| 520 |
+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
| 521 |
+
const oscillator = audioContext.createOscillator();
|
| 522 |
+
const gainNode = audioContext.createGain();
|
| 523 |
+
|
| 524 |
+
oscillator.connect(gainNode);
|
| 525 |
+
gainNode.connect(audioContext.destination);
|
| 526 |
+
|
| 527 |
+
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
|
| 528 |
+
oscillator.frequency.setValueAtTime(600, audioContext.currentTime + 0.1);
|
| 529 |
+
|
| 530 |
+
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
| 531 |
+
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
|
| 532 |
+
|
| 533 |
+
oscillator.start(audioContext.currentTime);
|
| 534 |
+
oscillator.stop(audioContext.currentTime + 0.2);
|
| 535 |
+
} catch (error) {
|
| 536 |
+
console.log('Could not play notification sound:', error);
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
cleanupExpiredNotifications() {
|
| 541 |
+
const now = new Date();
|
| 542 |
+
const expired = this.notifications.filter(notification => {
|
| 543 |
+
const created = new Date(notification.created_at);
|
| 544 |
+
const diff = now - created;
|
| 545 |
+
return diff > 24 * 60 * 60 * 1000; // 24 hours
|
| 546 |
+
});
|
| 547 |
+
|
| 548 |
+
expired.forEach(notification => {
|
| 549 |
+
const index = this.notifications.indexOf(notification);
|
| 550 |
+
if (index > -1) {
|
| 551 |
+
this.notifications.splice(index, 1);
|
| 552 |
+
}
|
| 553 |
+
});
|
| 554 |
+
|
| 555 |
+
this.updateNotificationPanel();
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
// Public methods for external use
|
| 559 |
+
showInfo(title, message, duration = 5000) {
|
| 560 |
+
this.showCustomNotification('info', title, message, duration);
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
showSuccess(title, message, duration = 5000) {
|
| 564 |
+
this.showCustomNotification('success', title, message, duration);
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
showWarning(title, message, duration = 5000) {
|
| 568 |
+
this.showCustomNotification('warning', title, message, duration);
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
showError(title, message, duration = 5000) {
|
| 572 |
+
this.showCustomNotification('error', title, message, duration);
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
showCustomNotification(type, title, message, duration) {
|
| 576 |
+
const notification = {
|
| 577 |
+
id: Date.now(),
|
| 578 |
+
type: type,
|
| 579 |
+
title: title,
|
| 580 |
+
message: message,
|
| 581 |
+
priority: 'medium',
|
| 582 |
+
created_at: new Date().toISOString(),
|
| 583 |
+
metadata: {},
|
| 584 |
+
read: false
|
| 585 |
+
};
|
| 586 |
+
|
| 587 |
+
this.notifications.unshift(notification);
|
| 588 |
+
this.unreadCount++;
|
| 589 |
+
this.updateBadge();
|
| 590 |
+
this.showToast(notification);
|
| 591 |
+
this.updateNotificationPanel();
|
| 592 |
+
|
| 593 |
+
if (duration > 0) {
|
| 594 |
+
setTimeout(() => {
|
| 595 |
+
const index = this.notifications.indexOf(notification);
|
| 596 |
+
if (index > -1) {
|
| 597 |
+
this.notifications.splice(index, 1);
|
| 598 |
+
this.updateNotificationPanel();
|
| 599 |
+
}
|
| 600 |
+
}, duration);
|
| 601 |
+
}
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
// Initialize notification manager when DOM is loaded
|
| 606 |
+
let notificationManager;
|
| 607 |
+
|
| 608 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 609 |
+
notificationManager = new NotificationManager();
|
| 610 |
+
});
|
| 611 |
+
|
| 612 |
+
// Global function for external use
|
| 613 |
+
window.showNotification = (type, title, message) => {
|
| 614 |
+
if (notificationManager) {
|
| 615 |
+
notificationManager.showCustomNotification(type, title, message);
|
| 616 |
+
}
|
| 617 |
+
};
|
app/static/js/scraping-control.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Scraping Control Panel for Legal Dashboard
|
| 3 |
+
* Manages web scraping operations, real-time monitoring, and results display
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
class ScrapingControlPanel {
|
| 7 |
+
constructor() {
|
| 8 |
+
this.baseEndpoint = '/api/scraping';
|
| 9 |
+
this.currentJob = null;
|
| 10 |
+
this.isRunning = false;
|
| 11 |
+
this.statusInterval = null;
|
| 12 |
+
this.logs = [];
|
| 13 |
+
|
| 14 |
+
this.initializeEventListeners();
|
| 15 |
+
this.loadScrapingStatus();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
initializeEventListeners() {
|
| 19 |
+
// Start scraping button
|
| 20 |
+
const startBtn = document.getElementById('startScrapingBtn');
|
| 21 |
+
if (startBtn) {
|
| 22 |
+
startBtn.addEventListener('click', () => this.startScraping());
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Stop scraping button
|
| 26 |
+
const stopBtn = document.getElementById('stopScrapingBtn');
|
| 27 |
+
if (stopBtn) {
|
| 28 |
+
stopBtn.addEventListener('click', () => this.stopScraping());
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Refresh results button
|
| 32 |
+
const refreshBtn = document.getElementById('refreshResultsBtn');
|
| 33 |
+
if (refreshBtn) {
|
| 34 |
+
refreshBtn.addEventListener('click', () => this.loadScrapingResults());
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Clear logs button
|
| 38 |
+
const clearLogsBtn = document.getElementById('clearLogsBtn');
|
| 39 |
+
if (clearLogsBtn) {
|
| 40 |
+
clearLogsBtn.addEventListener('click', () => this.clearLogs());
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
async startScraping() {
|
| 45 |
+
if (this.isRunning) {
|
| 46 |
+
this.showToast('اسکرپینگ در حال انجام است', 'warning');
|
| 47 |
+
return;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
const scrapingConfig = this.getScrapingConfig();
|
| 51 |
+
if (!scrapingConfig.url) {
|
| 52 |
+
this.showToast('لطفاً URL را وارد کنید', 'error');
|
| 53 |
+
return;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
try {
|
| 57 |
+
this.isRunning = true;
|
| 58 |
+
this.updateStartButton(true);
|
| 59 |
+
this.addLog('شروع اسکرپینگ...', 'info');
|
| 60 |
+
|
| 61 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/start`, {
|
| 62 |
+
method: 'POST',
|
| 63 |
+
body: JSON.stringify(scrapingConfig)
|
| 64 |
+
});
|
| 65 |
+
|
| 66 |
+
this.currentJob = response.job_id;
|
| 67 |
+
this.showToast('اسکرپینگ شروع شد', 'success');
|
| 68 |
+
this.addLog(`شغل اسکرپینگ ایجاد شد: ${this.currentJob}`, 'success');
|
| 69 |
+
|
| 70 |
+
// Start monitoring
|
| 71 |
+
this.startStatusMonitoring();
|
| 72 |
+
} catch (error) {
|
| 73 |
+
this.showToast(`خطا در شروع اسکرپینگ: ${error.message}`, 'error');
|
| 74 |
+
this.addLog(`خطا در شروع اسکرپینگ: ${error.message}`, 'error');
|
| 75 |
+
this.isRunning = false;
|
| 76 |
+
this.updateStartButton(false);
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
async stopScraping() {
|
| 81 |
+
if (!this.isRunning) {
|
| 82 |
+
this.showToast('هیچ اسکرپینگی در حال انجام نیست', 'warning');
|
| 83 |
+
return;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
try {
|
| 87 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/stop`, {
|
| 88 |
+
method: 'POST',
|
| 89 |
+
body: JSON.stringify({ job_id: this.currentJob })
|
| 90 |
+
});
|
| 91 |
+
|
| 92 |
+
this.showToast('اسکرپینگ متوقف شد', 'success');
|
| 93 |
+
this.addLog('اسکرپینگ متوقف شد', 'info');
|
| 94 |
+
|
| 95 |
+
this.isRunning = false;
|
| 96 |
+
this.currentJob = null;
|
| 97 |
+
this.updateStartButton(false);
|
| 98 |
+
this.stopStatusMonitoring();
|
| 99 |
+
} catch (error) {
|
| 100 |
+
this.showToast(`خطا در توقف اسکرپینگ: ${error.message}`, 'error');
|
| 101 |
+
this.addLog(`خطا در توقف اسکرپینگ: ${error.message}`, 'error');
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
async loadScrapingStatus() {
|
| 106 |
+
try {
|
| 107 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/status`);
|
| 108 |
+
this.updateStatusDisplay(response);
|
| 109 |
+
} catch (error) {
|
| 110 |
+
console.error('Failed to load scraping status:', error);
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
async loadScrapingResults() {
|
| 115 |
+
try {
|
| 116 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/results`);
|
| 117 |
+
this.renderResults(response);
|
| 118 |
+
} catch (error) {
|
| 119 |
+
console.error('Failed to load scraping results:', error);
|
| 120 |
+
this.showToast('خطا در بارگذاری نتایج', 'error');
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
async loadScrapingStatistics() {
|
| 125 |
+
try {
|
| 126 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/statistics`);
|
| 127 |
+
this.renderStatistics(response);
|
| 128 |
+
} catch (error) {
|
| 129 |
+
console.error('Failed to load scraping statistics:', error);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
startStatusMonitoring() {
|
| 134 |
+
this.statusInterval = setInterval(async () => {
|
| 135 |
+
if (this.isRunning) {
|
| 136 |
+
await this.updateScrapingStatus();
|
| 137 |
+
}
|
| 138 |
+
}, 5000); // Update every 5 seconds
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
stopStatusMonitoring() {
|
| 142 |
+
if (this.statusInterval) {
|
| 143 |
+
clearInterval(this.statusInterval);
|
| 144 |
+
this.statusInterval = null;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
async updateScrapingStatus() {
|
| 149 |
+
try {
|
| 150 |
+
const response = await fetchWithErrorHandling(`${this.baseEndpoint}/status`);
|
| 151 |
+
this.updateStatusDisplay(response);
|
| 152 |
+
|
| 153 |
+
// Check if scraping is complete
|
| 154 |
+
if (response.status === 'completed' || response.status === 'failed') {
|
| 155 |
+
this.isRunning = false;
|
| 156 |
+
this.currentJob = null;
|
| 157 |
+
this.updateStartButton(false);
|
| 158 |
+
this.stopStatusMonitoring();
|
| 159 |
+
|
| 160 |
+
if (response.status === 'completed') {
|
| 161 |
+
this.addLog('اسکرپینگ تکمیل شد', 'success');
|
| 162 |
+
this.loadScrapingResults();
|
| 163 |
+
} else {
|
| 164 |
+
this.addLog('اسکرپینگ با خطا مواجه شد', 'error');
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
} catch (error) {
|
| 168 |
+
console.error('Failed to update scraping status:', error);
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
updateStatusDisplay(status) {
|
| 173 |
+
const statusElement = document.getElementById('scrapingStatus');
|
| 174 |
+
const progressElement = document.getElementById('scrapingProgress');
|
| 175 |
+
const statsElement = document.getElementById('scrapingStats');
|
| 176 |
+
|
| 177 |
+
if (statusElement) {
|
| 178 |
+
statusElement.textContent = this.getStatusText(status.status);
|
| 179 |
+
statusElement.className = `status-indicator ${this.getStatusClass(status.status)}`;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
if (progressElement && status.progress) {
|
| 183 |
+
progressElement.style.width = `${status.progress}%`;
|
| 184 |
+
progressElement.textContent = `${status.progress}%`;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
if (statsElement) {
|
| 188 |
+
statsElement.innerHTML = `
|
| 189 |
+
<div class="stat-item">
|
| 190 |
+
<span class="stat-label">صفحات پردازش شده:</span>
|
| 191 |
+
<span class="stat-value">${status.pages_processed || 0}</span>
|
| 192 |
+
</div>
|
| 193 |
+
<div class="stat-item">
|
| 194 |
+
<span class="stat-label">نتایج یافت شده:</span>
|
| 195 |
+
<span class="stat-value">${status.items_found || 0}</span>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="stat-item">
|
| 198 |
+
<span class="stat-label">زمان سپری شده:</span>
|
| 199 |
+
<span class="stat-value">${status.elapsed_time || '0s'}</span>
|
| 200 |
+
</div>
|
| 201 |
+
`;
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
renderResults(results) {
|
| 206 |
+
const container = document.getElementById('scrapingResults');
|
| 207 |
+
if (!container) return;
|
| 208 |
+
|
| 209 |
+
if (!results.items || results.items.length === 0) {
|
| 210 |
+
container.innerHTML = '<p class="no-results">هیچ نتیجهای یافت نشد</p>';
|
| 211 |
+
return;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
const resultsHTML = results.items.map(item => `
|
| 215 |
+
<div class="scraping-item">
|
| 216 |
+
<div class="item-header">
|
| 217 |
+
<h4 class="item-title">${item.title || 'بدون عنوان'}</h4>
|
| 218 |
+
<span class="item-rating">امتیاز: ${item.rating || 0}</span>
|
| 219 |
+
</div>
|
| 220 |
+
<div class="item-content">
|
| 221 |
+
<p class="item-url">
|
| 222 |
+
<a href="${item.url}" target="_blank">${item.url}</a>
|
| 223 |
+
</p>
|
| 224 |
+
<p class="item-description">${item.description || 'توضیحات موجود نیست'}</p>
|
| 225 |
+
<div class="item-meta">
|
| 226 |
+
<span class="item-date">${this.formatDate(item.date)}</span>
|
| 227 |
+
<span class="item-category">${item.category || 'نامشخص'}</span>
|
| 228 |
+
</div>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
`).join('');
|
| 232 |
+
|
| 233 |
+
container.innerHTML = resultsHTML;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
renderStatistics(stats) {
|
| 237 |
+
const container = document.getElementById('scrapingStatistics');
|
| 238 |
+
if (!container) return;
|
| 239 |
+
|
| 240 |
+
const statsHTML = `
|
| 241 |
+
<div class="stats-grid">
|
| 242 |
+
<div class="stat-card">
|
| 243 |
+
<div class="stat-number">${stats.total_scraped || 0}</div>
|
| 244 |
+
<div class="stat-label">کل نتایج</div>
|
| 245 |
+
</div>
|
| 246 |
+
<div class="stat-card">
|
| 247 |
+
<div class="stat-number">${stats.success_rate || 0}%</div>
|
| 248 |
+
<div class="stat-label">نرخ موفقیت</div>
|
| 249 |
+
</div>
|
| 250 |
+
<div class="stat-card">
|
| 251 |
+
<div class="stat-number">${stats.average_speed || 0}</div>
|
| 252 |
+
<div class="stat-label">سرعت متوسط (صفحه/دقیقه)</div>
|
| 253 |
+
</div>
|
| 254 |
+
<div class="stat-card">
|
| 255 |
+
<div class="stat-number">${stats.average_rating || 0}</div>
|
| 256 |
+
<div class="stat-label">امتیاز متوسط</div>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
`;
|
| 260 |
+
|
| 261 |
+
container.innerHTML = statsHTML;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
getScrapingConfig() {
|
| 265 |
+
const urlInput = document.getElementById('scrapingUrl');
|
| 266 |
+
const depthInput = document.getElementById('scrapingDepth');
|
| 267 |
+
const maxPagesInput = document.getElementById('maxPages');
|
| 268 |
+
const filtersInput = document.getElementById('scrapingFilters');
|
| 269 |
+
|
| 270 |
+
return {
|
| 271 |
+
url: urlInput ? urlInput.value : '',
|
| 272 |
+
depth: depthInput ? parseInt(depthInput.value) : 1,
|
| 273 |
+
max_pages: maxPagesInput ? parseInt(maxPagesInput.value) : 100,
|
| 274 |
+
filters: filtersInput ? filtersInput.value : ''
|
| 275 |
+
};
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
updateStartButton(isRunning) {
|
| 279 |
+
const startBtn = document.getElementById('startScrapingBtn');
|
| 280 |
+
const stopBtn = document.getElementById('stopScrapingBtn');
|
| 281 |
+
|
| 282 |
+
if (startBtn) {
|
| 283 |
+
startBtn.disabled = isRunning;
|
| 284 |
+
startBtn.textContent = isRunning ? 'در حال اجرا...' : 'شروع اسکرپینگ';
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
if (stopBtn) {
|
| 288 |
+
stopBtn.disabled = !isRunning;
|
| 289 |
+
stopBtn.style.display = isRunning ? 'inline-block' : 'none';
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
addLog(message, type = 'info') {
|
| 294 |
+
const timestamp = new Date().toLocaleTimeString('fa-IR');
|
| 295 |
+
const logEntry = {
|
| 296 |
+
timestamp,
|
| 297 |
+
message,
|
| 298 |
+
type
|
| 299 |
+
};
|
| 300 |
+
|
| 301 |
+
this.logs.unshift(logEntry);
|
| 302 |
+
this.renderLogs();
|
| 303 |
+
|
| 304 |
+
// Keep only last 100 logs
|
| 305 |
+
if (this.logs.length > 100) {
|
| 306 |
+
this.logs = this.logs.slice(0, 100);
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
renderLogs() {
|
| 311 |
+
const container = document.getElementById('scrapingLogs');
|
| 312 |
+
if (!container) return;
|
| 313 |
+
|
| 314 |
+
const logsHTML = this.logs.map(log => `
|
| 315 |
+
<div class="log-entry ${log.type}">
|
| 316 |
+
<span class="log-timestamp">${log.timestamp}</span>
|
| 317 |
+
<span class="log-message">${log.message}</span>
|
| 318 |
+
</div>
|
| 319 |
+
`).join('');
|
| 320 |
+
|
| 321 |
+
container.innerHTML = logsHTML;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
clearLogs() {
|
| 325 |
+
this.logs = [];
|
| 326 |
+
this.renderLogs();
|
| 327 |
+
this.showToast('لاگها پاک شدند', 'info');
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
getStatusClass(status) {
|
| 331 |
+
const statusMap = {
|
| 332 |
+
'idle': 'status-idle',
|
| 333 |
+
'running': 'status-running',
|
| 334 |
+
'completed': 'status-completed',
|
| 335 |
+
'failed': 'status-failed',
|
| 336 |
+
'stopped': 'status-stopped'
|
| 337 |
+
};
|
| 338 |
+
return statusMap[status] || 'status-unknown';
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
getStatusText(status) {
|
| 342 |
+
const statusMap = {
|
| 343 |
+
'idle': 'آماده',
|
| 344 |
+
'running': 'در حال اجرا',
|
| 345 |
+
'completed': 'تکمیل شده',
|
| 346 |
+
'failed': 'ناموفق',
|
| 347 |
+
'stopped': 'متوقف شده'
|
| 348 |
+
};
|
| 349 |
+
return statusMap[status] || 'نامشخص';
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
formatDate(dateString) {
|
| 353 |
+
if (!dateString) return 'نامشخص';
|
| 354 |
+
const date = new Date(dateString);
|
| 355 |
+
return date.toLocaleDateString('fa-IR');
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
showToast(message, type = 'info') {
|
| 359 |
+
if (typeof showToast === 'function') {
|
| 360 |
+
showToast(message, type);
|
| 361 |
+
} else {
|
| 362 |
+
console.log(`${type.toUpperCase()}: ${message}`);
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
// Initialize scraping control panel
|
| 368 |
+
const scrapingControlPanel = new ScrapingControlPanel();
|