Really-amin commited on
Commit
120e2d5
·
verified ·
1 Parent(s): 4625a5e

Upload 14 files

Browse files
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();