Really-amin commited on
Commit
8d3cb40
·
verified ·
1 Parent(s): 384c61c

Upload 149 files

Browse files
.vscode/.server-controller-port.log ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "port": 13452,
3
+ "time": 1754155169544,
4
+ "version": "0.0.3"
5
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "git.ignoreLimitWarning": true
3
+ }
Dockerfile CHANGED
@@ -1,77 +1,89 @@
1
- # ────────────────
2
- # Stage 1: Builder
3
- # ────────────────
4
- FROM python:3.10-slim AS builder
5
-
6
- # Install build dependencies
7
- RUN apt-get update && apt-get install -y \
8
- build-essential \
9
- gcc \
10
- && rm -rf /var/lib/apt/lists/*
11
-
12
- # Upgrade pip
13
- RUN pip install --upgrade pip
14
-
15
- # Create virtual environment
16
- RUN python -m venv /opt/venv
17
- ENV PATH="/opt/venv/bin:$PATH"
18
-
19
- # Copy requirements and install dependencies
20
- WORKDIR /app
21
- COPY requirements.txt .
22
- RUN pip install --no-cache-dir -r requirements.txt
23
-
24
- # ────────────────
25
- # Stage 2: Production
26
- # ────────────────
27
- FROM python:3.10-slim
28
-
29
- # Create non-root user
30
- RUN groupadd -r appuser && useradd -r -g appuser appuser
31
-
32
- # Install runtime dependencies
33
- RUN apt-get update && apt-get install -y \
34
- poppler-utils \
35
- tesseract-ocr \
36
- libgl1 \
37
- curl \
38
- sqlite3 \
39
- && rm -rf /var/lib/apt/lists/*
40
-
41
- # Copy virtual environment from builder
42
- COPY --from=builder /opt/venv /opt/venv
43
- ENV PATH="/opt/venv/bin:$PATH"
44
-
45
- # Set working directory
46
- WORKDIR /app
47
-
48
- # Create persistent directories with permissions
49
- RUN mkdir -p /app/data /app/cache /app/logs /app/uploads /app/backups \
50
- && chown -R appuser:appuser /app
51
-
52
- # Copy application files
53
- COPY --chown=appuser:appuser . .
54
-
55
- # Ensure startup script exists and is executable (optional)
56
- RUN if [ -f start.sh ]; then chmod +x start.sh; fi
57
-
58
- # Environment variables
59
- ENV PYTHONPATH=/app
60
- ENV DATABASE_PATH=/app/data/legal_dashboard.db
61
- ENV TRANSFORMERS_CACHE=/app/cache
62
- ENV HF_HOME=/app/cache
63
- ENV LOG_LEVEL=INFO
64
- ENV ENVIRONMENT=production
65
-
66
- # Switch to non-root user
67
- USER appuser
68
-
69
- # Expose port
70
- EXPOSE 8000
71
-
72
- # Health check
73
- HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
74
- CMD curl -fs http://localhost:8000/api/health || exit 1
75
-
76
- # Default CMD
77
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────
2
+ # Stage 1: Builder
3
+ # ────────────────
4
+ FROM python:3.10-slim AS builder
5
+
6
+ # Install build dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ build-essential \
9
+ gcc \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Upgrade pip
13
+ RUN pip install --upgrade pip
14
+
15
+ # Create virtual environment
16
+ RUN python -m venv /opt/venv
17
+ ENV PATH="/opt/venv/bin:$PATH"
18
+
19
+ # Copy requirements and install dependencies
20
+ WORKDIR /app
21
+ COPY requirements.txt .
22
+ RUN pip install --no-cache-dir -r requirements.txt
23
+
24
+ # ────────────────
25
+ # Stage 2: Production
26
+ # ────────────────
27
+ FROM python:3.10-slim
28
+
29
+ # Create non-root user with specific UID/GID for compatibility
30
+ RUN groupadd -g 1000 appuser && useradd -r -u 1000 -g appuser appuser
31
+
32
+ # Install runtime dependencies
33
+ RUN apt-get update && apt-get install -y \
34
+ poppler-utils \
35
+ tesseract-ocr \
36
+ libgl1 \
37
+ curl \
38
+ sqlite3 \
39
+ && rm -rf /var/lib/apt/lists/*
40
+
41
+ # Copy virtual environment from builder
42
+ COPY --from=builder /opt/venv /opt/venv
43
+ ENV PATH="/opt/venv/bin:$PATH"
44
+
45
+ # Set working directory
46
+ WORKDIR /app
47
+
48
+ # Create all necessary directories with proper permissions
49
+ RUN mkdir -p \
50
+ /app/data \
51
+ /app/database \
52
+ /app/cache \
53
+ /app/logs \
54
+ /app/uploads \
55
+ /app/backups \
56
+ /tmp/app_fallback \
57
+ && chown -R appuser:appuser /app \
58
+ && chown -R appuser:appuser /tmp/app_fallback \
59
+ && chmod -R 755 /app \
60
+ && chmod -R 777 /tmp/app_fallback
61
+
62
+ # Copy application files with proper ownership
63
+ COPY --chown=appuser:appuser . .
64
+
65
+ # Make startup script executable if exists
66
+ RUN if [ -f start.sh ]; then chmod +x start.sh; fi
67
+
68
+ # Environment variables
69
+ ENV PYTHONPATH=/app
70
+ ENV DATABASE_DIR=/app/data
71
+ ENV DATABASE_PATH=/app/data/legal_documents.db
72
+ ENV TRANSFORMERS_CACHE=/app/cache
73
+ ENV HF_HOME=/app/cache
74
+ ENV LOG_LEVEL=INFO
75
+ ENV ENVIRONMENT=production
76
+ ENV PYTHONUNBUFFERED=1
77
+
78
+ # Switch to non-root user BEFORE any file operations
79
+ USER appuser
80
+
81
+ # Expose port
82
+ EXPOSE 8000
83
+
84
+ # Health check
85
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
86
+ CMD curl -fs http://localhost:8000/health || exit 1
87
+
88
+ # Default CMD with error handling
89
+ CMD ["sh", "-c", "python -c 'import os; os.makedirs(\"/app/data\", exist_ok=True)' && uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1"]
app/api/auth.py CHANGED
@@ -1,322 +1,579 @@
1
- """
2
- Authentication API endpoints for Legal Dashboard
3
- ================================================
4
- Provides user authentication, JWT token management, and role-based access control.
5
- """
6
-
7
- import os
8
- import logging
9
- from datetime import datetime, timedelta
10
- from typing import Optional, Dict, Any
11
- from passlib.context import CryptContext
12
- from jose import JWTError, jwt
13
- from fastapi import APIRouter, HTTPException, Depends, status
14
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
15
- from pydantic import BaseModel, EmailStr
16
- import sqlite3
17
- from contextlib import contextmanager
18
-
19
- # ─────────────────────────────
20
- # Logging configuration
21
- # ─────────────────────────────
22
- logger = logging.getLogger(__name__)
23
-
24
- # ─────────────────────────────
25
- # Security configuration
26
- # ─────────────────────────────
27
- SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
28
- ALGORITHM = "HS256"
29
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
30
- REFRESH_TOKEN_EXPIRE_DAYS = 7
31
-
32
- # Password hashing
33
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
34
-
35
- # Security scheme
36
- security = HTTPBearer()
37
-
38
- # ─────────────────────────────
39
- # Database path configuration
40
- # ─────────────────────────────
41
- DB_DIR = os.getenv("DATABASE_DIR", "/app/database")
42
- DB_NAME = os.getenv("DATABASE_NAME", "legal_documents.db")
43
- DB_PATH = os.path.join(DB_DIR, DB_NAME)
44
-
45
- # ایجاد پوشه دیتابیس
46
- os.makedirs(DB_DIR, exist_ok=True)
47
-
48
- # ─────────────────────────────
49
- # Pydantic models
50
- # ─────────────────────────────
51
- class UserCreate(BaseModel):
52
- username: str
53
- email: EmailStr
54
- password: str
55
- role: str = "user"
56
-
57
- class UserLogin(BaseModel):
58
- username: str
59
- password: str
60
-
61
- class Token(BaseModel):
62
- access_token: str
63
- refresh_token: str
64
- token_type: str
65
- expires_in: int
66
-
67
- class UserResponse(BaseModel):
68
- id: int
69
- username: str
70
- email: str
71
- role: str
72
- is_active: bool
73
- created_at: str
74
-
75
- class PasswordChange(BaseModel):
76
- current_password: str
77
- new_password: str
78
-
79
- # ─────────────────────────────
80
- # Database connection
81
- # ─────────────────────────────
82
- @contextmanager
83
- def get_db_connection():
84
- conn = sqlite3.connect(DB_PATH, check_same_thread=False)
85
- conn.row_factory = sqlite3.Row
86
- try:
87
- yield conn
88
- finally:
89
- conn.close()
90
-
91
- # ─────────────────────────────
92
- # Initialize database tables
93
- # ─────────────────────────────
94
- def init_auth_tables():
95
- with get_db_connection() as conn:
96
- cursor = conn.cursor()
97
- # Users table
98
- cursor.execute("""
99
- CREATE TABLE IF NOT EXISTS users (
100
- id INTEGER PRIMARY KEY AUTOINCREMENT,
101
- username TEXT UNIQUE NOT NULL,
102
- email TEXT UNIQUE NOT NULL,
103
- hashed_password TEXT NOT NULL,
104
- role TEXT NOT NULL DEFAULT 'user',
105
- is_active BOOLEAN NOT NULL DEFAULT 1,
106
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
107
- last_login TIMESTAMP,
108
- failed_login_attempts INTEGER DEFAULT 0,
109
- locked_until TIMESTAMP
110
- )
111
- """)
112
- # Sessions table
113
- cursor.execute("""
114
- CREATE TABLE IF NOT EXISTS sessions (
115
- id INTEGER PRIMARY KEY AUTOINCREMENT,
116
- user_id INTEGER NOT NULL,
117
- refresh_token TEXT UNIQUE NOT NULL,
118
- expires_at TIMESTAMP NOT NULL,
119
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
120
- FOREIGN KEY (user_id) REFERENCES users (id)
121
- )
122
- """)
123
- # Audit log
124
- cursor.execute("""
125
- CREATE TABLE IF NOT EXISTS auth_audit_log (
126
- id INTEGER PRIMARY KEY AUTOINCREMENT,
127
- user_id INTEGER,
128
- action TEXT NOT NULL,
129
- ip_address TEXT,
130
- user_agent TEXT,
131
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
132
- success BOOLEAN NOT NULL,
133
- details TEXT,
134
- FOREIGN KEY (user_id) REFERENCES users (id)
135
- )
136
- """)
137
- # Default admin
138
- cursor.execute("SELECT COUNT(*) FROM users WHERE username = 'admin'")
139
- if cursor.fetchone()[0] == 0:
140
- hashed_password = pwd_context.hash("admin123")
141
- cursor.execute("""
142
- INSERT INTO users (username, email, hashed_password, role)
143
- VALUES (?, ?, ?, ?)
144
- """, ("admin", "[email protected]", hashed_password, "admin"))
145
- conn.commit()
146
-
147
- # ─────────────────────────────
148
- # Password utilities
149
- # ─────────────────────────────
150
- def verify_password(plain_password: str, hashed_password: str) -> bool:
151
- return pwd_context.verify(plain_password, hashed_password)
152
-
153
- def get_password_hash(password: str) -> str:
154
- return pwd_context.hash(password)
155
-
156
- # ─────────────────────────────
157
- # Token utilities
158
- # ─────────────────────────────
159
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
160
- to_encode = data.copy()
161
- expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
162
- to_encode.update({"exp": expire, "type": "access"})
163
- return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
164
-
165
- def create_refresh_token(data: dict):
166
- to_encode = data.copy()
167
- expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
168
- to_encode.update({"exp": expire, "type": "refresh"})
169
- return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
170
-
171
- def verify_token(token: str) -> Optional[Dict[str, Any]]:
172
- try:
173
- return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
174
- except JWTError:
175
- return None
176
-
177
- # ─────────────────────────────
178
- # User utilities
179
- # ─────────────────────────────
180
- def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
181
- with get_db_connection() as conn:
182
- cursor = conn.cursor()
183
- cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
184
- user = cursor.fetchone()
185
- return dict(user) if user else None
186
-
187
- def get_user_by_id(user_id: int) -> Optional[Dict[str, Any]]:
188
- with get_db_connection() as conn:
189
- cursor = conn.cursor()
190
- cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
191
- user = cursor.fetchone()
192
- return dict(user) if user else None
193
-
194
- def update_last_login(user_id: int):
195
- with get_db_connection() as conn:
196
- cursor = conn.cursor()
197
- cursor.execute("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?", (user_id,))
198
- conn.commit()
199
-
200
- def log_auth_attempt(user_id: Optional[int], action: str, success: bool,
201
- ip_address: str = None, user_agent: str = None, details: str = None):
202
- with get_db_connection() as conn:
203
- cursor = conn.cursor()
204
- cursor.execute("""
205
- INSERT INTO auth_audit_log (user_id, action, ip_address, user_agent, success, details)
206
- VALUES (?, ?, ?, ?, ?, ?)
207
- """, (user_id, action, ip_address, user_agent, success, details))
208
- conn.commit()
209
-
210
- # ─────────────────────────────
211
- # Authentication dependency
212
- # ─────────────────────────────
213
- async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]:
214
- token = credentials.credentials
215
- payload = verify_token(token)
216
- if not payload or payload.get("type") != "access":
217
- raise HTTPException(status_code=401, detail="Invalid access token")
218
- user = get_user_by_id(int(payload.get("sub")))
219
- if not user or not user.get("is_active"):
220
- raise HTTPException(status_code=401, detail="User not found or inactive")
221
- return user
222
-
223
- def require_role(required_role: str):
224
- def role_checker(current_user: Dict[str, Any] = Depends(get_current_user)):
225
- if current_user.get("role", "user") not in ("admin", required_role):
226
- raise HTTPException(status_code=403, detail="Insufficient permissions")
227
- return current_user
228
- return role_checker
229
-
230
- # ─────────────────────────────
231
- # API Router
232
- # ─────────────────────────────
233
- router = APIRouter()
234
-
235
- @router.post("/register", response_model=UserResponse)
236
- async def register_user(user_data: UserCreate):
237
- existing_user = get_user_by_username(user_data.username)
238
- if existing_user:
239
- raise HTTPException(status_code=400, detail="Username already registered")
240
- hashed_password = get_password_hash(user_data.password)
241
- with get_db_connection() as conn:
242
- cursor = conn.cursor()
243
- cursor.execute("""
244
- INSERT INTO users (username, email, hashed_password, role)
245
- VALUES (?, ?, ?, ?)
246
- """, (user_data.username, user_data.email, hashed_password, user_data.role))
247
- user_id = cursor.lastrowid
248
- conn.commit()
249
- user = get_user_by_id(user_id)
250
- log_auth_attempt(user_id, "register", True)
251
- return UserResponse(**user)
252
-
253
- @router.post("/login", response_model=Token)
254
- async def login(user_credentials: UserLogin):
255
- user = get_user_by_username(user_credentials.username)
256
- if not user or not verify_password(user_credentials.password, user["hashed_password"]):
257
- log_auth_attempt(user["id"] if user else None, "login", False, details="Invalid credentials")
258
- raise HTTPException(status_code=401, detail="Invalid credentials")
259
- update_last_login(user["id"])
260
- access_token = create_access_token(data={"sub": str(user["id"])})
261
- refresh_token = create_refresh_token(data={"sub": str(user["id"])})
262
- with get_db_connection() as conn:
263
- cursor = conn.cursor()
264
- cursor.execute("INSERT INTO sessions (user_id, refresh_token, expires_at) VALUES (?, ?, ?)",
265
- (user["id"], refresh_token, (datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)).isoformat()))
266
- conn.commit()
267
- log_auth_attempt(user["id"], "login", True)
268
- return Token(access_token=access_token, refresh_token=refresh_token, token_type="bearer", expires_in=ACCESS_TOKEN_EXPIRE_MINUTES*60)
269
-
270
- @router.post("/refresh", response_model=Token)
271
- async def refresh_token(refresh_token: str):
272
- payload = verify_token(refresh_token)
273
- if not payload or payload.get("type") != "refresh":
274
- raise HTTPException(status_code=401, detail="Invalid refresh token")
275
- user_id = int(payload.get("sub"))
276
- with get_db_connection() as conn:
277
- cursor = conn.cursor()
278
- cursor.execute("SELECT * FROM sessions WHERE refresh_token = ? AND expires_at > ?", (refresh_token, datetime.utcnow().isoformat()))
279
- if not cursor.fetchone():
280
- raise HTTPException(status_code=401, detail="Invalid or expired refresh token")
281
- new_access = create_access_token(data={"sub": str(user_id)})
282
- new_refresh = create_refresh_token(data={"sub": str(user_id)})
283
- with get_db_connection() as conn:
284
- cursor = conn.cursor()
285
- cursor.execute("UPDATE sessions SET refresh_token = ?, expires_at = ? WHERE refresh_token = ?",
286
- (new_refresh, (datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)).isoformat(), refresh_token))
287
- conn.commit()
288
- return Token(access_token=new_access, refresh_token=new_refresh, token_type="bearer", expires_in=ACCESS_TOKEN_EXPIRE_MINUTES*60)
289
-
290
- @router.post("/logout")
291
- async def logout(current_user: Dict[str, Any] = Depends(get_current_user)):
292
- log_auth_attempt(current_user["id"], "logout", True)
293
- return {"message": "Successfully logged out"}
294
-
295
- @router.get("/me", response_model=UserResponse)
296
- async def get_current_user_info(current_user: Dict[str, Any] = Depends(get_current_user)):
297
- return UserResponse(**current_user)
298
-
299
- @router.put("/change-password")
300
- async def change_password(password_data: PasswordChange, current_user: Dict[str, Any] = Depends(get_current_user)):
301
- if not verify_password(password_data.current_password, current_user["hashed_password"]):
302
- raise HTTPException(status_code=400, detail="Current password is incorrect")
303
- new_hash = get_password_hash(password_data.new_password)
304
- with get_db_connection() as conn:
305
- cursor = conn.cursor()
306
- cursor.execute("UPDATE users SET hashed_password = ? WHERE id = ?", (new_hash, current_user["id"]))
307
- conn.commit()
308
- log_auth_attempt(current_user["id"], "password_change", True)
309
- return {"message": "Password changed successfully"}
310
-
311
- @router.get("/users", response_model=list[UserResponse])
312
- async def get_users(current_user: Dict[str, Any] = Depends(require_role("admin"))):
313
- with get_db_connection() as conn:
314
- cursor = conn.cursor()
315
- cursor.execute("SELECT * FROM users ORDER BY created_at DESC")
316
- users = [dict(row) for row in cursor.fetchall()]
317
- return [UserResponse(**user) for user in users]
318
-
319
- # ─────────────────────────────
320
- # Initialize tables
321
- # ─────────────────────────────
322
- init_auth_tables()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Authentication API endpoints for Legal Dashboard
3
+ ================================================
4
+ Provides user authentication, JWT token management, and role-based access control.
5
+ """
6
+
7
+ import os
8
+ import logging
9
+ import tempfile
10
+ from datetime import datetime, timedelta
11
+ from typing import Optional, Dict, Any
12
+ from pathlib import Path
13
+ from passlib.context import CryptContext
14
+ from jose import JWTError, jwt
15
+ from fastapi import APIRouter, HTTPException, Depends, status
16
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
17
+ from pydantic import BaseModel, EmailStr
18
+ import sqlite3
19
+ from contextlib import contextmanager
20
+
21
+ # ─────────────────────────────
22
+ # Logging configuration
23
+ # ─────────────────────────────
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # ─────────────────────────────
27
+ # Security configuration
28
+ # ─────────────────────────────
29
+ SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
30
+ ALGORITHM = "HS256"
31
+ ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
32
+ REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "7"))
33
+
34
+ # Password hashing
35
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
36
+
37
+ # Security scheme
38
+ security = HTTPBearer()
39
+
40
+ # ─────────────────────────────
41
+ # Database path configuration with fallback
42
+ # ─────────────────────────────
43
+ def get_database_path():
44
+ """Get database path with fallback options for different environments"""
45
+ # Try environment variable first
46
+ db_dir = os.getenv("DATABASE_DIR", "/app/data")
47
+ db_name = os.getenv("DATABASE_NAME", "legal_documents.db")
48
+
49
+ # List of possible directories to try
50
+ possible_dirs = [
51
+ db_dir,
52
+ "/app/data",
53
+ "/app/database",
54
+ "/tmp/app_fallback",
55
+ tempfile.gettempdir() + "/legal_dashboard"
56
+ ]
57
+
58
+ for directory in possible_dirs:
59
+ try:
60
+ # Try to create directory
61
+ Path(directory).mkdir(parents=True, exist_ok=True)
62
+
63
+ # Test write permissions
64
+ test_file = Path(directory) / "test_write.tmp"
65
+ test_file.write_text("test")
66
+ test_file.unlink()
67
+
68
+ db_path = Path(directory) / db_name
69
+ logger.info(f"Using database directory: {directory}")
70
+ return str(db_path)
71
+
72
+ except (PermissionError, OSError) as e:
73
+ logger.warning(f"Cannot use directory {directory}: {e}")
74
+ continue
75
+
76
+ # Final fallback - in-memory database
77
+ logger.warning("Using in-memory SQLite database - data will not persist!")
78
+ return ":memory:"
79
+
80
+ DB_PATH = get_database_path()
81
+
82
+ # ─────────────────────────────
83
+ # Pydantic models
84
+ # ─────────────────────────────
85
+ class UserCreate(BaseModel):
86
+ username: str
87
+ email: EmailStr
88
+ password: str
89
+ role: str = "user"
90
+
91
+ class UserLogin(BaseModel):
92
+ username: str
93
+ password: str
94
+
95
+ class Token(BaseModel):
96
+ access_token: str
97
+ refresh_token: str
98
+ token_type: str
99
+ expires_in: int
100
+
101
+ class UserResponse(BaseModel):
102
+ id: int
103
+ username: str
104
+ email: str
105
+ role: str
106
+ is_active: bool
107
+ created_at: str
108
+
109
+ class PasswordChange(BaseModel):
110
+ current_password: str
111
+ new_password: str
112
+
113
+ # ─────────────────────────────
114
+ # Database connection
115
+ # ─────────────────────────────
116
+ @contextmanager
117
+ def get_db_connection():
118
+ """Database connection with error handling"""
119
+ conn = None
120
+ try:
121
+ conn = sqlite3.connect(DB_PATH, check_same_thread=False, timeout=30.0)
122
+ conn.row_factory = sqlite3.Row
123
+ conn.execute("PRAGMA foreign_keys = ON")
124
+ yield conn
125
+ except sqlite3.Error as e:
126
+ logger.error(f"Database connection error: {e}")
127
+ if conn:
128
+ conn.rollback()
129
+ raise HTTPException(status_code=500, detail="Database connection failed")
130
+ finally:
131
+ if conn:
132
+ conn.close()
133
+
134
+ # ─────────────────────────────
135
+ # Initialize database tables
136
+ # ─────────────────────────────
137
+ def init_auth_tables():
138
+ """Initialize authentication tables with error handling"""
139
+ try:
140
+ with get_db_connection() as conn:
141
+ cursor = conn.cursor()
142
+
143
+ # Users table
144
+ cursor.execute("""
145
+ CREATE TABLE IF NOT EXISTS users (
146
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
147
+ username TEXT UNIQUE NOT NULL,
148
+ email TEXT UNIQUE NOT NULL,
149
+ hashed_password TEXT NOT NULL,
150
+ role TEXT NOT NULL DEFAULT 'user',
151
+ is_active BOOLEAN NOT NULL DEFAULT 1,
152
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
153
+ last_login TIMESTAMP,
154
+ failed_login_attempts INTEGER DEFAULT 0,
155
+ locked_until TIMESTAMP
156
+ )
157
+ """)
158
+
159
+ # Sessions table
160
+ cursor.execute("""
161
+ CREATE TABLE IF NOT EXISTS sessions (
162
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
163
+ user_id INTEGER NOT NULL,
164
+ refresh_token TEXT UNIQUE NOT NULL,
165
+ expires_at TIMESTAMP NOT NULL,
166
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
167
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
168
+ )
169
+ """)
170
+
171
+ # Audit log
172
+ cursor.execute("""
173
+ CREATE TABLE IF NOT EXISTS auth_audit_log (
174
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
175
+ user_id INTEGER,
176
+ action TEXT NOT NULL,
177
+ ip_address TEXT,
178
+ user_agent TEXT,
179
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
180
+ success BOOLEAN NOT NULL,
181
+ details TEXT,
182
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
183
+ )
184
+ """)
185
+
186
+ # Create default admin user if not exists
187
+ cursor.execute("SELECT COUNT(*) FROM users WHERE username = 'admin'")
188
+ if cursor.fetchone()[0] == 0:
189
+ hashed_password = pwd_context.hash("admin123")
190
+ cursor.execute("""
191
+ INSERT INTO users (username, email, hashed_password, role)
192
+ VALUES (?, ?, ?, ?)
193
+ """, ("admin", "[email protected]", hashed_password, "admin"))
194
+ logger.info("Default admin user created (username: admin, password: admin123)")
195
+
196
+ conn.commit()
197
+ logger.info("Authentication tables initialized successfully")
198
+
199
+ except Exception as e:
200
+ logger.error(f"Failed to initialize auth tables: {e}")
201
+ raise
202
+
203
+ # ─────────────────────────────
204
+ # Password utilities
205
+ # ─────────────────────────────
206
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
207
+ """Verify password against hash"""
208
+ try:
209
+ return pwd_context.verify(plain_password, hashed_password)
210
+ except Exception as e:
211
+ logger.error(f"Password verification error: {e}")
212
+ return False
213
+
214
+ def get_password_hash(password: str) -> str:
215
+ """Generate password hash"""
216
+ return pwd_context.hash(password)
217
+
218
+ # ─────────────────────────────
219
+ # Token utilities
220
+ # ─────────────────────────────
221
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
222
+ """Create JWT access token"""
223
+ to_encode = data.copy()
224
+ expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
225
+ to_encode.update({"exp": expire, "type": "access"})
226
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
227
+
228
+ def create_refresh_token(data: dict):
229
+ """Create JWT refresh token"""
230
+ to_encode = data.copy()
231
+ expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
232
+ to_encode.update({"exp": expire, "type": "refresh"})
233
+ return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
234
+
235
+ def verify_token(token: str) -> Optional[Dict[str, Any]]:
236
+ """Verify JWT token"""
237
+ try:
238
+ return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
239
+ except JWTError as e:
240
+ logger.debug(f"Token verification failed: {e}")
241
+ return None
242
+
243
+ # ─────────────────────────────
244
+ # User utilities
245
+ # ─────────────────────────────
246
+ def get_user_by_username(username: str) -> Optional[Dict[str, Any]]:
247
+ """Get user by username"""
248
+ try:
249
+ with get_db_connection() as conn:
250
+ cursor = conn.cursor()
251
+ cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
252
+ user = cursor.fetchone()
253
+ return dict(user) if user else None
254
+ except Exception as e:
255
+ logger.error(f"Error getting user by username: {e}")
256
+ return None
257
+
258
+ def get_user_by_id(user_id: int) -> Optional[Dict[str, Any]]:
259
+ """Get user by ID"""
260
+ try:
261
+ with get_db_connection() as conn:
262
+ cursor = conn.cursor()
263
+ cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
264
+ user = cursor.fetchone()
265
+ return dict(user) if user else None
266
+ except Exception as e:
267
+ logger.error(f"Error getting user by ID: {e}")
268
+ return None
269
+
270
+ def update_last_login(user_id: int):
271
+ """Update user's last login timestamp"""
272
+ try:
273
+ with get_db_connection() as conn:
274
+ cursor = conn.cursor()
275
+ cursor.execute("UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?", (user_id,))
276
+ conn.commit()
277
+ except Exception as e:
278
+ logger.error(f"Error updating last login: {e}")
279
+
280
+ def log_auth_attempt(user_id: Optional[int], action: str, success: bool,
281
+ ip_address: str = None, user_agent: str = None, details: str = None):
282
+ """Log authentication attempt"""
283
+ try:
284
+ with get_db_connection() as conn:
285
+ cursor = conn.cursor()
286
+ cursor.execute("""
287
+ INSERT INTO auth_audit_log (user_id, action, ip_address, user_agent, success, details)
288
+ VALUES (?, ?, ?, ?, ?, ?)
289
+ """, (user_id, action, ip_address, user_agent, success, details))
290
+ conn.commit()
291
+ except Exception as e:
292
+ logger.error(f"Error logging auth attempt: {e}")
293
+
294
+ # ─────────────────────────────
295
+ # Authentication dependency
296
+ # ─────────────────────────────
297
+ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]:
298
+ """Get current authenticated user"""
299
+ token = credentials.credentials
300
+ payload = verify_token(token)
301
+
302
+ if not payload or payload.get("type") != "access":
303
+ raise HTTPException(
304
+ status_code=status.HTTP_401_UNAUTHORIZED,
305
+ detail="Invalid access token",
306
+ headers={"WWW-Authenticate": "Bearer"}
307
+ )
308
+
309
+ user = get_user_by_id(int(payload.get("sub")))
310
+ if not user or not user.get("is_active"):
311
+ raise HTTPException(
312
+ status_code=status.HTTP_401_UNAUTHORIZED,
313
+ detail="User not found or inactive"
314
+ )
315
+
316
+ return user
317
+
318
+ def require_role(required_role: str):
319
+ """Require specific role for endpoint access"""
320
+ def role_checker(current_user: Dict[str, Any] = Depends(get_current_user)):
321
+ user_role = current_user.get("role", "user")
322
+ if user_role not in ("admin", required_role) and required_role != "user":
323
+ raise HTTPException(
324
+ status_code=status.HTTP_403_FORBIDDEN,
325
+ detail="Insufficient permissions"
326
+ )
327
+ return current_user
328
+ return role_checker
329
+
330
+ # ─────────────────────────────
331
+ # API Router
332
+ # ─────────────────────────────
333
+ router = APIRouter()
334
+
335
+ @router.get("/health")
336
+ async def health_check():
337
+ """Health check endpoint"""
338
+ try:
339
+ with get_db_connection() as conn:
340
+ cursor = conn.cursor()
341
+ cursor.execute("SELECT 1")
342
+ cursor.fetchone()
343
+ return {"status": "healthy", "database": "connected", "timestamp": datetime.utcnow()}
344
+ except Exception as e:
345
+ logger.error(f"Health check failed: {e}")
346
+ return {"status": "unhealthy", "database": "disconnected", "error": str(e)}
347
+
348
+ @router.post("/register", response_model=UserResponse)
349
+ async def register_user(user_data: UserCreate):
350
+ """Register a new user"""
351
+ # Check if username already exists
352
+ existing_user = get_user_by_username(user_data.username)
353
+ if existing_user:
354
+ raise HTTPException(
355
+ status_code=status.HTTP_400_BAD_REQUEST,
356
+ detail="Username already registered"
357
+ )
358
+
359
+ # Hash password and create user
360
+ hashed_password = get_password_hash(user_data.password)
361
+
362
+ try:
363
+ with get_db_connection() as conn:
364
+ cursor = conn.cursor()
365
+ cursor.execute("""
366
+ INSERT INTO users (username, email, hashed_password, role)
367
+ VALUES (?, ?, ?, ?)
368
+ """, (user_data.username, user_data.email, hashed_password, user_data.role))
369
+ user_id = cursor.lastrowid
370
+ conn.commit()
371
+
372
+ user = get_user_by_id(user_id)
373
+ log_auth_attempt(user_id, "register", True)
374
+ return UserResponse(**user)
375
+
376
+ except sqlite3.IntegrityError:
377
+ raise HTTPException(
378
+ status_code=status.HTTP_400_BAD_REQUEST,
379
+ detail="Username or email already exists"
380
+ )
381
+ except Exception as e:
382
+ logger.error(f"User registration failed: {e}")
383
+ raise HTTPException(
384
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
385
+ detail="Registration failed"
386
+ )
387
+
388
+ @router.post("/login", response_model=Token)
389
+ async def login(user_credentials: UserLogin):
390
+ """User login"""
391
+ user = get_user_by_username(user_credentials.username)
392
+
393
+ if not user or not verify_password(user_credentials.password, user["hashed_password"]):
394
+ log_auth_attempt(user["id"] if user else None, "login", False, details="Invalid credentials")
395
+ raise HTTPException(
396
+ status_code=status.HTTP_401_UNAUTHORIZED,
397
+ detail="Invalid credentials"
398
+ )
399
+
400
+ if not user.get("is_active"):
401
+ log_auth_attempt(user["id"], "login", False, details="Account inactive")
402
+ raise HTTPException(
403
+ status_code=status.HTTP_401_UNAUTHORIZED,
404
+ detail="Account is inactive"
405
+ )
406
+
407
+ # Update last login
408
+ update_last_login(user["id"])
409
+
410
+ # Create tokens
411
+ access_token = create_access_token(data={"sub": str(user["id"])})
412
+ refresh_token = create_refresh_token(data={"sub": str(user["id"])})
413
+
414
+ # Store refresh token
415
+ try:
416
+ with get_db_connection() as conn:
417
+ cursor = conn.cursor()
418
+ cursor.execute("""
419
+ INSERT INTO sessions (user_id, refresh_token, expires_at)
420
+ VALUES (?, ?, ?)
421
+ """, (
422
+ user["id"],
423
+ refresh_token,
424
+ (datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)).isoformat()
425
+ ))
426
+ conn.commit()
427
+ except Exception as e:
428
+ logger.error(f"Failed to store refresh token: {e}")
429
+
430
+ log_auth_attempt(user["id"], "login", True)
431
+
432
+ return Token(
433
+ access_token=access_token,
434
+ refresh_token=refresh_token,
435
+ token_type="bearer",
436
+ expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60
437
+ )
438
+
439
+ @router.post("/refresh", response_model=Token)
440
+ async def refresh_token(refresh_token: str):
441
+ """Refresh access token"""
442
+ payload = verify_token(refresh_token)
443
+
444
+ if not payload or payload.get("type") != "refresh":
445
+ raise HTTPException(
446
+ status_code=status.HTTP_401_UNAUTHORIZED,
447
+ detail="Invalid refresh token"
448
+ )
449
+
450
+ user_id = int(payload.get("sub"))
451
+
452
+ # Verify refresh token exists and is valid
453
+ try:
454
+ with get_db_connection() as conn:
455
+ cursor = conn.cursor()
456
+ cursor.execute("""
457
+ SELECT * FROM sessions
458
+ WHERE refresh_token = ? AND expires_at > ? AND user_id = ?
459
+ """, (refresh_token, datetime.utcnow().isoformat(), user_id))
460
+
461
+ if not cursor.fetchone():
462
+ raise HTTPException(
463
+ status_code=status.HTTP_401_UNAUTHORIZED,
464
+ detail="Invalid or expired refresh token"
465
+ )
466
+ except Exception as e:
467
+ logger.error(f"Refresh token validation failed: {e}")
468
+ raise HTTPException(
469
+ status_code=status.HTTP_401_UNAUTHORIZED,
470
+ detail="Token validation failed"
471
+ )
472
+
473
+ # Create new tokens
474
+ new_access = create_access_token(data={"sub": str(user_id)})
475
+ new_refresh = create_refresh_token(data={"sub": str(user_id)})
476
+
477
+ # Update refresh token in database
478
+ try:
479
+ with get_db_connection() as conn:
480
+ cursor = conn.cursor()
481
+ cursor.execute("""
482
+ UPDATE sessions
483
+ SET refresh_token = ?, expires_at = ?
484
+ WHERE refresh_token = ?
485
+ """, (
486
+ new_refresh,
487
+ (datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)).isoformat(),
488
+ refresh_token
489
+ ))
490
+ conn.commit()
491
+ except Exception as e:
492
+ logger.error(f"Failed to update refresh token: {e}")
493
+
494
+ return Token(
495
+ access_token=new_access,
496
+ refresh_token=new_refresh,
497
+ token_type="bearer",
498
+ expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60
499
+ )
500
+
501
+ @router.post("/logout")
502
+ async def logout(current_user: Dict[str, Any] = Depends(get_current_user)):
503
+ """User logout"""
504
+ try:
505
+ # Clean up user sessions
506
+ with get_db_connection() as conn:
507
+ cursor = conn.cursor()
508
+ cursor.execute("DELETE FROM sessions WHERE user_id = ?", (current_user["id"],))
509
+ conn.commit()
510
+ except Exception as e:
511
+ logger.error(f"Logout cleanup failed: {e}")
512
+
513
+ log_auth_attempt(current_user["id"], "logout", True)
514
+ return {"message": "Successfully logged out"}
515
+
516
+ @router.get("/me", response_model=UserResponse)
517
+ async def get_current_user_info(current_user: Dict[str, Any] = Depends(get_current_user)):
518
+ """Get current user information"""
519
+ return UserResponse(**current_user)
520
+
521
+ @router.put("/change-password")
522
+ async def change_password(
523
+ password_data: PasswordChange,
524
+ current_user: Dict[str, Any] = Depends(get_current_user)
525
+ ):
526
+ """Change user password"""
527
+ if not verify_password(password_data.current_password, current_user["hashed_password"]):
528
+ raise HTTPException(
529
+ status_code=status.HTTP_400_BAD_REQUEST,
530
+ detail="Current password is incorrect"
531
+ )
532
+
533
+ new_hash = get_password_hash(password_data.new_password)
534
+
535
+ try:
536
+ with get_db_connection() as conn:
537
+ cursor = conn.cursor()
538
+ cursor.execute(
539
+ "UPDATE users SET hashed_password = ? WHERE id = ?",
540
+ (new_hash, current_user["id"])
541
+ )
542
+ conn.commit()
543
+
544
+ log_auth_attempt(current_user["id"], "password_change", True)
545
+ return {"message": "Password changed successfully"}
546
+
547
+ except Exception as e:
548
+ logger.error(f"Password change failed: {e}")
549
+ raise HTTPException(
550
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
551
+ detail="Password change failed"
552
+ )
553
+
554
+ @router.get("/users", response_model=list[UserResponse])
555
+ async def get_users(current_user: Dict[str, Any] = Depends(require_role("admin"))):
556
+ """Get all users (admin only)"""
557
+ try:
558
+ with get_db_connection() as conn:
559
+ cursor = conn.cursor()
560
+ cursor.execute("SELECT * FROM users ORDER BY created_at DESC")
561
+ users = [dict(row) for row in cursor.fetchall()]
562
+
563
+ return [UserResponse(**user) for user in users]
564
+
565
+ except Exception as e:
566
+ logger.error(f"Failed to get users: {e}")
567
+ raise HTTPException(
568
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
569
+ detail="Failed to retrieve users"
570
+ )
571
+
572
+ # ─────────────────────────────
573
+ # Initialize tables on module import
574
+ # ─────────────────────────────
575
+ try:
576
+ init_auth_tables()
577
+ except Exception as e:
578
+ logger.error(f"Failed to initialize authentication system: {e}")
579
+ # Don't raise here to allow the app to start even if DB init fails
docker-compose.yml CHANGED
@@ -1,94 +1,110 @@
1
- version: "3.8"
2
-
3
- services:
4
- # FastAPI Application
5
- legal-dashboard:
6
- build: .
7
- container_name: legal_dashboard_app
8
- restart: unless-stopped
9
- networks:
10
- - app_network
11
- user: "1000:1000" # هم‌تراز با کاربر میزبان برای حل مشکل پرمیشن
12
- volumes:
13
- - ./data:/app/data
14
- - ./cache:/app/cache
15
- - ./logs:/app/logs
16
- - ./uploads:/app/uploads
17
- - ./backups:/app/backups
18
- environment:
19
- - DATABASE_PATH=/app/data/legal_dashboard.db
20
- - TRANSFORMERS_CACHE=/app/cache
21
- - HF_HOME=/app/cache
22
- - LOG_LEVEL=INFO
23
- - ENVIRONMENT=production
24
- - JWT_SECRET_KEY=${JWT_SECRET_KEY:-your-secret-key-change-in-production}
25
- healthcheck:
26
- test: ["CMD-SHELL", "curl -fs http://localhost:8000/api/health || exit 1"]
27
- interval: 30s
28
- timeout: 10s
29
- retries: 5
30
- start_period: 60s
31
- depends_on:
32
- - redis
33
-
34
- # Redis for caching and sessions
35
- redis:
36
- image: redis:7-alpine
37
- container_name: legal_dashboard_redis
38
- restart: unless-stopped
39
- networks:
40
- - app_network
41
- volumes:
42
- - redis_data:/data
43
- command: redis-server --appendonly yes
44
- healthcheck:
45
- test: ["CMD", "redis-cli", "ping"]
46
- interval: 30s
47
- timeout: 10s
48
- retries: 3
49
-
50
- # Nginx Reverse Proxy
51
- nginx:
52
- image: nginx:alpine
53
- container_name: legal_dashboard_nginx
54
- restart: unless-stopped
55
- ports:
56
- - "80:80"
57
- - "443:443"
58
- volumes:
59
- - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
60
- - ./ssl:/etc/nginx/ssl:ro
61
- - ./logs/nginx:/var/log/nginx
62
- depends_on:
63
- - legal-dashboard
64
- networks:
65
- - app_network
66
-
67
- # Backup Service
68
- backup:
69
- image: alpine:latest
70
- container_name: legal_dashboard_backup
71
- restart: unless-stopped
72
- volumes:
73
- - ./data:/app/data
74
- - ./backups:/app/backups
75
- - ./logs:/app/logs
76
- command: |
77
- sh -c "
78
- mkdir -p /app/backups &&
79
- while true; do
80
- sleep 86400;
81
- tar -czf /app/backups/backup-$$(date +%Y%m%d_%H%M%S).tar.gz /app/data /app/logs;
82
- find /app/backups -name 'backup-*.tar.gz' -mtime +7 -delete;
83
- done
84
- "
85
- networks:
86
- - app_network
87
-
88
- networks:
89
- app_network:
90
- driver: bridge
91
-
92
- volumes:
93
- redis_data:
94
- driver: local
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.8"
2
+
3
+ services:
4
+ # FastAPI Application
5
+ legal-dashboard:
6
+ build:
7
+ context: .
8
+ dockerfile: Dockerfile
9
+ container_name: legal_dashboard_app
10
+ restart: unless-stopped
11
+ networks:
12
+ - app_network
13
+ # Remove user override to use the user from Dockerfile
14
+ # user: "1000:1000" # Commented out - using Dockerfile user instead
15
+ volumes:
16
+ # Create host directories with proper permissions first
17
+ - ./data:/app/data:rw
18
+ - ./cache:/app/cache:rw
19
+ - ./logs:/app/logs:rw
20
+ - ./uploads:/app/uploads:rw
21
+ - ./backups:/app/backups:rw
22
+ environment:
23
+ - DATABASE_DIR=/app/data
24
+ - DATABASE_PATH=/app/data/legal_dashboard.db
25
+ - TRANSFORMERS_CACHE=/app/cache
26
+ - HF_HOME=/app/cache
27
+ - LOG_LEVEL=INFO
28
+ - ENVIRONMENT=production
29
+ - JWT_SECRET_KEY=${JWT_SECRET_KEY:-your-secret-key-change-in-production}
30
+ - PYTHONPATH=/app
31
+ - PYTHONUNBUFFERED=1
32
+ ports:
33
+ - "8000:8000"
34
+ healthcheck:
35
+ test: ["CMD-SHELL", "curl -fs http://localhost:8000/api/auth/health || exit 1"]
36
+ interval: 30s
37
+ timeout: 10s
38
+ retries: 5
39
+ start_period: 60s
40
+ depends_on:
41
+ - redis
42
+
43
+ # Redis for caching and sessions
44
+ redis:
45
+ image: redis:7-alpine
46
+ container_name: legal_dashboard_redis
47
+ restart: unless-stopped
48
+ networks:
49
+ - app_network
50
+ volumes:
51
+ - redis_data:/data
52
+ command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
53
+ healthcheck:
54
+ test: ["CMD", "redis-cli", "ping"]
55
+ interval: 30s
56
+ timeout: 10s
57
+ retries: 3
58
+ ports:
59
+ - "6379:6379"
60
+
61
+ # Nginx Reverse Proxy (Optional - commented out for simple deployment)
62
+ # nginx:
63
+ # image: nginx:alpine
64
+ # container_name: legal_dashboard_nginx
65
+ # restart: unless-stopped
66
+ # ports:
67
+ # - "80:80"
68
+ # - "443:443"
69
+ # volumes:
70
+ # - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
71
+ # - ./ssl:/etc/nginx/ssl:ro
72
+ # - ./logs/nginx:/var/log/nginx
73
+ # depends_on:
74
+ # - legal-dashboard
75
+ # networks:
76
+ # - app_network
77
+
78
+ # Backup Service
79
+ backup:
80
+ image: alpine:latest
81
+ container_name: legal_dashboard_backup
82
+ restart: unless-stopped
83
+ volumes:
84
+ - ./data:/app/data:ro
85
+ - ./backups:/app/backups:rw
86
+ - ./logs:/app/logs:ro
87
+ command: |
88
+ sh -c "
89
+ echo 'Starting backup service...' &&
90
+ mkdir -p /app/backups &&
91
+ while true; do
92
+ echo 'Creating backup...' &&
93
+ timestamp=$$(date +%Y%m%d_%H%M%S) &&
94
+ tar -czf /app/backups/backup-$$timestamp.tar.gz -C /app data logs 2>/dev/null || echo 'Backup failed' &&
95
+ echo 'Cleaning old backups...' &&
96
+ find /app/backups -name 'backup-*.tar.gz' -mtime +7 -delete 2>/dev/null || echo 'Cleanup completed' &&
97
+ echo 'Backup cycle completed. Sleeping for 24 hours...' &&
98
+ sleep 86400
99
+ done
100
+ "
101
+ networks:
102
+ - app_network
103
+
104
+ networks:
105
+ app_network:
106
+ driver: bridge
107
+
108
+ volumes:
109
+ redis_data:
110
+ driver: local
fix_permissions.sh ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Fix Permissions Script for Legal Dashboard
4
+ # This script resolves common permission issues
5
+
6
+ echo "🔧 Legal Dashboard - Permission Fix Script"
7
+ echo "=========================================="
8
+
9
+ # Function to create directory with fallback
10
+ create_dir_with_fallback() {
11
+ local dir_path=$1
12
+ local fallback_path=$2
13
+
14
+ echo "📁 Creating directory: $dir_path"
15
+
16
+ if mkdir -p "$dir_path" 2>/dev/null && [ -w "$dir_path" ]; then
17
+ echo "✅ Successfully created: $dir_path"
18
+ return 0
19
+ else
20
+ echo "⚠️ Failed to create $dir_path, trying fallback: $fallback_path"
21
+ if mkdir -p "$fallback_path" 2>/dev/null && [ -w "$fallback_path" ]; then
22
+ echo "✅ Successfully created fallback: $fallback_path"
23
+ return 0
24
+ else
25
+ echo "❌ Failed to create both primary and fallback directories"
26
+ return 1
27
+ fi
28
+ fi
29
+ }
30
+
31
+ # Check current user and permissions
32
+ echo ""
33
+ echo "🔍 System Information:"
34
+ echo " - Current user: $(whoami)"
35
+ echo " - User ID: $(id -u)"
36
+ echo " - Group ID: $(id -g)"
37
+ echo " - Working directory: $(pwd)"
38
+ echo " - Home directory: $HOME"
39
+
40
+ # Check if running in Docker
41
+ if [ -f /.dockerenv ]; then
42
+ echo " - Environment: Docker Container"
43
+ IN_DOCKER=true
44
+ else
45
+ echo " - Environment: Host System"
46
+ IN_DOCKER=false
47
+ fi
48
+
49
+ # Check if running in HF Spaces
50
+ if [ "$SPACE_ID" != "" ]; then
51
+ echo " - Platform: Hugging Face Spaces"
52
+ IN_HF_SPACES=true
53
+ else
54
+ echo " - Platform: Standard"
55
+ IN_HF_SPACES=false
56
+ fi
57
+
58
+ echo ""
59
+ echo "🏗️ Creating necessary directories..."
60
+
61
+ # Create directories with fallbacks
62
+ if [ "$IN_HF_SPACES" = true ]; then
63
+ # HF Spaces specific paths
64
+ create_dir_with_fallback "/tmp/legal_dashboard/data" "$HOME/legal_dashboard/data"
65
+ create_dir_with_fallback "/tmp/legal_dashboard/cache" "$HOME/legal_dashboard/cache"
66
+ create_dir_with_fallback "/tmp/legal_dashboard/logs" "$HOME/legal_dashboard/logs"
67
+ create_dir_with_fallback "/tmp/legal_dashboard/uploads" "$HOME/legal_dashboard/uploads"
68
+
69
+ # Set environment variables for HF Spaces
70
+ export DATABASE_DIR="/tmp/legal_dashboard/data"
71
+ export TRANSFORMERS_CACHE="/tmp/legal_dashboard/cache"
72
+ export HF_HOME="/tmp/legal_dashboard/cache"
73
+
74
+ echo "✅ HF Spaces directories configured"
75
+
76
+ elif [ "$IN_DOCKER" = true ]; then
77
+ # Docker specific paths
78
+ create_dir_with_fallback "/app/data" "/tmp/app_data"
79
+ create_dir_with_fallback "/app/database" "/tmp/app_database"
80
+ create_dir_with_fallback "/app/cache" "/tmp/app_cache"
81
+ create_dir_with_fallback "/app/logs" "/tmp/app_logs"
82
+ create_dir_with_fallback "/app/uploads" "/tmp/app_uploads"
83
+ create_dir_with_fallback "/app/backups" "/tmp/app_backups"
84
+
85
+ echo "✅ Docker directories configured"
86
+
87
+ else
88
+ # Host system paths
89
+ create_dir_with_fallback "./data" "$HOME/legal_dashboard/data"
90
+ create_dir_with_fallback "./cache" "$HOME/legal_dashboard/cache"
91
+ create_dir_with_fallback "./logs" "$HOME/legal_dashboard/logs"
92
+ create_dir_with_fallback "./uploads" "$HOME/legal_dashboard/uploads"
93
+ create_dir_with_fallback "./backups" "$HOME/legal_dashboard/backups"
94
+
95
+ echo "✅ Host system directories configured"
96
+ fi
97
+
98
+ # Set permissions where possible
99
+ echo ""
100
+ echo "🔒 Setting permissions..."
101
+
102
+ # Function to set permissions safely
103
+ safe_chmod() {
104
+ local path=$1
105
+ local perm=$2
106
+
107
+ if [ -d "$path" ] && [ -w "$path" ]; then
108
+ chmod $perm "$path" 2>/dev/null && echo " ✅ Set $perm on $path" || echo " ⚠️ Could not set permissions on $path"
109
+ fi
110
+ }
111
+
112
+ # Apply permissions to existing directories
113
+ for dir in "/app/data" "/app/database" "/app/cache" "/app/logs" "/app/uploads" "/app/backups" \
114
+ "/tmp/legal_dashboard" "/tmp/app_data" "/tmp/app_database" "/tmp/app_cache" \
115
+ "$HOME/legal_dashboard" "./data" "./cache" "./logs" "./uploads" "./backups"; do
116
+ safe_chmod "$dir" 755
117
+ done
118
+
119
+ # Test database creation
120
+ echo ""
121
+ echo "🗄️ Testing database creation..."
122
+
123
+ test_db_creation() {
124
+ local test_dir=$1
125
+ local test_db="$test_dir/test.db"
126
+
127
+ if [ -d "$test_dir" ] && [ -w "$test_dir" ]; then
128
+ if python3 -c "
129
+ import sqlite3
130
+ import os
131
+ try:
132
+ conn = sqlite3.connect('$test_db')
133
+ conn.execute('CREATE TABLE test (id INTEGER)')
134
+ conn.close()
135
+ os.remove('$test_db')
136
+ print('✅ Database test successful in $test_dir')
137
+ exit(0)
138
+ except Exception as e:
139
+ print('❌ Database test failed in $test_dir: {}'.format(e))
140
+ exit(1)
141
+ " 2>/dev/null; then
142
+ echo "$test_dir" # Return the working directory
143
+ return 0
144
+ fi
145
+ fi
146
+ return 1
147
+ }
148
+
149
+ # Find working database directory
150
+ WORKING_DB_DIR=""
151
+ for test_dir in "/app/data" "/tmp/legal_dashboard/data" "/tmp/app_data" "$HOME/legal_dashboard/data" "./data"; do
152
+ if test_db_creation "$test_dir"; then
153
+ WORKING_DB_DIR="$test_dir"
154
+ break
155
+ fi
156
+ done
157
+
158
+ if [ "$WORKING_DB_DIR" != "" ]; then
159
+ echo "✅ Database directory confirmed: $WORKING_DB_DIR"
160
+ export DATABASE_DIR="$WORKING_DB_DIR"
161
+ else
162
+ echo "⚠️ No writable database directory found, will use in-memory database"
163
+ export DATABASE_DIR=":memory:"
164
+ fi
165
+
166
+ # Create .env file if it doesn't exist
167
+ echo ""
168
+ echo "⚙️ Creating environment configuration..."
169
+
170
+ ENV_FILE=".env"
171
+ if [ ! -f "$ENV_FILE" ]; then
172
+ cat > "$ENV_FILE" << EOF
173
+ # Auto-generated environment configuration
174
+ DATABASE_DIR=$DATABASE_DIR
175
+ DATABASE_NAME=legal_documents.db
176
+ JWT_SECRET_KEY=your-secret-key-change-in-production-$(date +%s)
177
+ LOG_LEVEL=INFO
178
+ ENVIRONMENT=production
179
+ PYTHONPATH=/app
180
+ PYTHONUNBUFFERED=1
181
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
182
+ REFRESH_TOKEN_EXPIRE_DAYS=7
183
+ EOF
184
+ echo "✅ Created $ENV_FILE"
185
+ else
186
+ echo "✅ $ENV_FILE already exists"
187
+ fi
188
+
189
+ # Test Python imports
190
+ echo ""
191
+ echo "🐍 Testing Python dependencies..."
192
+
193
+ python3 -c "
194
+ try:
195
+ import fastapi
196
+ import uvicorn
197
+ import sqlite3
198
+ import os
199
+ print('✅ Core dependencies available')
200
+ except ImportError as e:
201
+ print('❌ Missing dependency: {}'.format(e))
202
+
203
+ try:
204
+ import gradio
205
+ print('✅ Gradio available')
206
+ except ImportError:
207
+ print('⚠️ Gradio not available (ok if not using HF Spaces)')
208
+ " 2>/dev/null
209
+
210
+ # Final status
211
+ echo ""
212
+ echo "📊 Final Status:"
213
+ echo " - Database directory: ${DATABASE_DIR:-Not set}"
214
+ echo " - Cache directory: ${TRANSFORMERS_CACHE:-Not set}"
215
+ echo " - Log level: ${LOG_LEVEL:-INFO}"
216
+ echo " - Environment: ${ENVIRONMENT:-development}"
217
+
218
+ # Instructions
219
+ echo ""
220
+ echo "🚀 Next Steps:"
221
+ echo " 1. Run the application:"
222
+ if [ "$IN_HF_SPACES" = true ]; then
223
+ echo " python app.py"
224
+ elif [ "$IN_DOCKER" = true ]; then
225
+ echo " ./start.sh"
226
+ echo " # or"
227
+ echo " uvicorn app.main:app --host 0.0.0.0 --port 8000"
228
+ else
229
+ echo " ./start.sh"
230
+ echo " # or"
231
+ echo " python app.py"
232
+ echo " # or"
233
+ echo " uvicorn app.main:app --host 0.0.0.0 --port 8000"
234
+ fi
235
+
236
+ echo ""
237
+ echo " 2. Default login credentials:"
238
+ echo " Username: admin"
239
+ echo " Password: admin123"
240
+ echo ""
241
+ echo " 3. Change default password after first login!"
242
+
243
+ echo ""
244
+ echo "🎉 Permission fix completed!"
245
+ echo "============================================"
frontend/dev/comprehensive-test.html CHANGED
@@ -1,764 +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-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
- .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
- width: 12px;
64
- height: 12px;
65
- border-radius: 50%;
66
- margin-right: 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-height: 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-bottom: 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-bottom: 5px;
108
- }
109
- .stat-label {
110
- color: #666;
111
- font-size: 0.9rem;
112
- }
113
- .progress-bar {
114
- width: 100%;
115
- height: 4px;
116
- background: #e5e7eb;
117
- border-radius: 2px;
118
- overflow: hidden;
119
- margin: 10px 0;
120
- }
121
- .progress-fill {
122
- height: 100%;
123
- background: #3b82f6;
124
- transition: width 0.3s ease;
125
- }
126
- </style>
127
- </head>
128
- <body>
129
- <h1>🔍 Comprehensive Frontend Test - Legal Dashboard</h1>
130
-
131
- <div class="test-section">
132
- <h2>📊 Test Summary</h2>
133
- <div class="summary-stats">
134
- <div class="stat-card">
135
- <div class="stat-number" id="totalPages">0</div>
136
- <div class="stat-label">Total Pages</div>
137
- </div>
138
- <div class="stat-card">
139
- <div class="stat-number" id="passedPages">0</div>
140
- <div class="stat-label">Passed</div>
141
- </div>
142
- <div class="stat-card">
143
- <div class="stat-number" id="failedPages">0</div>
144
- <div class="stat-label">Failed</div>
145
- </div>
146
- <div class="stat-card">
147
- <div class="stat-number" id="successRate">0%</div>
148
- <div class="stat-label">Success Rate</div>
149
- </div>
150
- </div>
151
- <div class="progress-bar">
152
- <div class="progress-fill" id="progressBar" style="width: 0%"></div>
153
- </div>
154
- </div>
155
-
156
- <div class="test-section">
157
- <h2>🎛️ Test Controls</h2>
158
- <button type="button" onclick="runAllTests()" id="runAllBtn">Run All Tests</button>
159
- <button type="button" onclick="testCoreSystem()">Test Core System</button>
160
- <button type="button" onclick="testAPIConnectivity()">Test API Connectivity</button>
161
- <button type="button" onclick="testPageIntegration()">Test Page Integration</button>
162
- <button type="button" onclick="clearResults()">Clear Results</button>
163
- <button type="button" onclick="exportResults()">Export Results</button>
164
- </div>
165
-
166
- <div class="test-section">
167
- <h2>📄 Page Tests</h2>
168
- <div id="pageTests">
169
- <!-- Page tests will be generated here -->
170
- </div>
171
- </div>
172
-
173
- <div class="test-section">
174
- <h2>📋 Test Results</h2>
175
- <div class="test-results" id="testResults">
176
- <!-- Test results will be displayed here -->
177
- </div>
178
- </div>
179
-
180
- <script src="../js/api-client.js"></script>
181
- <script src="../js/core.js"></script>
182
- <script src="../js/notifications.js"></script>
183
- <script>
184
- class ComprehensiveTester {
185
- constructor() {
186
- this.baseURL = window.location.origin;
187
- this.results = [];
188
- this.testStats = {
189
- total: 0,
190
- passed: 0,
191
- failed: 0,
192
- successRate: 0
193
- };
194
- this.isRunning = false;
195
-
196
- this.pages = [
197
- {
198
- name: 'Main Dashboard',
199
- url: 'improved_legal_dashboard.html',
200
- description: 'Main dashboard with analytics and charts',
201
- tests: ['load', 'api', 'core', 'charts']
202
- },
203
- {
204
- name: 'Documents Page',
205
- url: 'documents.html',
206
- description: 'Document management and CRUD operations',
207
- tests: ['load', 'api', 'core', 'crud']
208
- },
209
- {
210
- name: 'Upload Page',
211
- url: 'upload.html',
212
- description: 'File upload and OCR processing',
213
- tests: ['load', 'api', 'core', 'upload']
214
- },
215
- {
216
- name: 'Scraping Page',
217
- url: 'scraping.html',
218
- description: 'Web scraping and content extraction',
219
- tests: ['load', 'api', 'core', 'scraping']
220
- },
221
- {
222
- name: 'Scraping Dashboard',
223
- url: 'scraping_dashboard.html',
224
- description: 'Scraping statistics and monitoring',
225
- tests: ['load', 'api', 'core', 'stats']
226
- },
227
- {
228
- name: 'Reports Page',
229
- url: 'reports.html',
230
- description: 'Analytics reports and insights',
231
- tests: ['load', 'api', 'core', 'reports']
232
- },
233
- {
234
- name: 'Index Page',
235
- url: 'index.html',
236
- description: 'Landing page and navigation',
237
- tests: ['load', 'api', 'core', 'navigation']
238
- }
239
- ];
240
-
241
- this.initialize();
242
- }
243
-
244
- initialize() {
245
- this.createPageTests();
246
- this.updateStats();
247
- }
248
-
249
- createPageTests() {
250
- const container = document.getElementById('pageTests');
251
- container.innerHTML = '';
252
-
253
- this.pages.forEach((page, index) => {
254
- const testDiv = document.createElement('div');
255
- testDiv.className = 'page-test';
256
- testDiv.id = `page-${index}`;
257
-
258
- testDiv.innerHTML = `
259
- <div class="status-indicator"></div>
260
- <h3>${page.name}</h3>
261
- <p>${page.description}</p>
262
- <div style="font-size: 0.8rem; color: #666; margin: 5px 0;">
263
- File: ${page.url}
264
- </div>
265
- <div class="tests" id="tests-${index}">
266
- ${page.tests.map((test, testIndex) => `
267
- <div class="test" id="test-${index}-${testIndex}">
268
- <span class="status-indicator"></span>
269
- ${test.charAt(0).toUpperCase() + test.slice(1)} Test
270
- </div>
271
- `).join('')}
272
- </div>
273
- <button type="button" onclick="tester.testSinglePage(${index})" class="test-page-btn">
274
- Test Page
275
- </button>
276
- `;
277
-
278
- container.appendChild(testDiv);
279
- });
280
- }
281
-
282
- async testSinglePage(pageIndex) {
283
- const page = this.pages[pageIndex];
284
- const testDiv = document.getElementById(`page-${pageIndex}`);
285
-
286
- // Set testing state
287
- testDiv.className = 'page-test testing';
288
- testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
289
- testDiv.querySelector('.test-page-btn').disabled = true;
290
-
291
- this.logResult({
292
- page: page.name,
293
- status: 'started',
294
- message: `Starting tests for ${page.name}`
295
- });
296
-
297
- let allTestsPassed = true;
298
-
299
- for (let testIndex = 0; testIndex < page.tests.length; testIndex++) {
300
- const test = page.tests[testIndex];
301
- const testDiv = document.getElementById(`test-${pageIndex}-${testIndex}`);
302
-
303
- // Set test testing state
304
- testDiv.querySelector('.status-indicator').className = 'status-indicator testing';
305
-
306
- try {
307
- const result = await this.executeTest(test, page);
308
-
309
- if (result.success) {
310
- testDiv.querySelector('.status-indicator').className = 'status-indicator success';
311
- this.logResult({
312
- page: page.name,
313
- test: test,
314
- status: 'success',
315
- message: `${test} test passed for ${page.name}`
316
- });
317
- } else {
318
- testDiv.querySelector('.status-indicator').className = 'status-indicator error';
319
- allTestsPassed = false;
320
- this.logResult({
321
- page: page.name,
322
- test: test,
323
- status: 'error',
324
- message: `${test} test failed for ${page.name}: ${result.error}`
325
- });
326
- }
327
- } catch (error) {
328
- testDiv.querySelector('.status-indicator').className = 'status-indicator error';
329
- allTestsPassed = false;
330
- this.logResult({
331
- page: page.name,
332
- test: test,
333
- status: 'error',
334
- message: `${test} test failed for ${page.name}: ${error.message}`
335
- });
336
- }
337
-
338
- await this.delay(200); // Small delay between tests
339
- }
340
-
341
- // Update page status
342
- testDiv.className = `page-test ${allTestsPassed ? 'success' : 'error'}`;
343
- testDiv.querySelector('.status-indicator').className = `status-indicator ${allTestsPassed ? 'success' : 'error'}`;
344
- testDiv.querySelector('.test-page-btn').disabled = false;
345
-
346
- this.logResult({
347
- page: page.name,
348
- status: allTestsPassed ? 'completed' : 'failed',
349
- message: `${page.name} ${allTestsPassed ? 'completed successfully' : 'failed'}`
350
- });
351
-
352
- this.updateStats();
353
- }
354
-
355
- async executeTest(test, page) {
356
- switch (test) {
357
- case 'load':
358
- return await this.testPageLoad(page);
359
- case 'api':
360
- return await this.testAPIConnectivity(page);
361
- case 'core':
362
- return await this.testCoreIntegration(page);
363
- case 'charts':
364
- return await this.testChartsFunctionality(page);
365
- case 'crud':
366
- return await this.testCRUDOperations(page);
367
- case 'upload':
368
- return await this.testUploadFunctionality(page);
369
- case 'scraping':
370
- return await this.testScrapingFunctionality(page);
371
- case 'stats':
372
- return await this.testStatisticsFunctionality(page);
373
- case 'reports':
374
- return await this.testReportsFunctionality(page);
375
- case 'navigation':
376
- return await this.testNavigationFunctionality(page);
377
- default:
378
- return { success: false, error: 'Unknown test' };
379
- }
380
- }
381
-
382
- async testPageLoad(page) {
383
- try {
384
- const response = await fetch(`${this.baseURL}/${page.url}`);
385
- return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
386
- } catch (error) {
387
- return { success: false, error: error.message };
388
- }
389
- }
390
-
391
- async testAPIConnectivity(page) {
392
- try {
393
- const response = await fetch(`${this.baseURL}/api/health`);
394
- return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
395
- } catch (error) {
396
- return { success: false, error: error.message };
397
- }
398
- }
399
-
400
- async testCoreIntegration(page) {
401
- try {
402
- // Check if core.js is loaded
403
- if (typeof dashboardCore === 'undefined') {
404
- return { success: false, error: 'Core module not loaded' };
405
- }
406
-
407
- // Check if core is initialized
408
- if (!dashboardCore.isInitialized) {
409
- return { success: false, error: 'Core module not initialized' };
410
- }
411
-
412
- return { success: true, error: null };
413
- } catch (error) {
414
- return { success: false, error: error.message };
415
- }
416
- }
417
-
418
- async testChartsFunctionality(page) {
419
- try {
420
- // Check if Chart.js is available
421
- if (typeof Chart === 'undefined') {
422
- return { success: false, error: 'Chart.js not loaded' };
423
- }
424
-
425
- return { success: true, error: null };
426
- } catch (error) {
427
- return { success: false, error: error.message };
428
- }
429
- }
430
-
431
- async testCRUDOperations(page) {
432
- try {
433
- const response = await fetch(`${this.baseURL}/api/documents`);
434
- return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
435
- } catch (error) {
436
- return { success: false, error: error.message };
437
- }
438
- }
439
-
440
- async testUploadFunctionality(page) {
441
- try {
442
- const response = await fetch(`${this.baseURL}/api/ocr/status`);
443
- return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
444
- } catch (error) {
445
- return { success: false, error: error.message };
446
- }
447
- }
448
-
449
- async testScrapingFunctionality(page) {
450
- try {
451
- const response = await fetch(`${this.baseURL}/api/scraping/health`);
452
- return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
453
- } catch (error) {
454
- return { success: false, error: error.message };
455
- }
456
- }
457
-
458
- async testStatisticsFunctionality(page) {
459
- try {
460
- const response = await fetch(`${this.baseURL}/api/scraping/scrape/statistics`);
461
- return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
462
- } catch (error) {
463
- return { success: false, error: error.message };
464
- }
465
- }
466
-
467
- async testReportsFunctionality(page) {
468
- try {
469
- const response = await fetch(`${this.baseURL}/api/analytics/overview`);
470
- return { success: response.ok, error: response.ok ? null : `HTTP ${response.status}` };
471
- } catch (error) {
472
- return { success: false, error: error.message };
473
- }
474
- }
475
-
476
- async testNavigationFunctionality(page) {
477
- try {
478
- // Check if navigation elements exist
479
- const response = await fetch(`${this.baseURL}/${page.url}`);
480
- const html = await response.text();
481
-
482
- // Check for navigation elements
483
- const hasNavigation = html.includes('nav') || html.includes('sidebar') || html.includes('menu');
484
-
485
- return { success: hasNavigation, error: hasNavigation ? null : 'No navigation found' };
486
- } catch (error) {
487
- return { success: false, error: error.message };
488
- }
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.pages.length; i++) {
501
- await this.testSinglePage(i);
502
- await this.delay(500); // Delay between pages
503
- }
504
-
505
- this.isRunning = false;
506
- document.getElementById('runAllBtn').disabled = false;
507
- document.getElementById('runAllBtn').textContent = 'Run All Tests';
508
- }
509
-
510
- async testCoreSystem() {
511
- this.logResult({
512
- test: 'Core System',
513
- status: 'started',
514
- message: 'Testing core system integration'
515
- });
516
-
517
- try {
518
- // Test core module loading
519
- if (typeof dashboardCore === 'undefined') {
520
- throw new Error('Core module not loaded');
521
- }
522
-
523
- // Test core initialization
524
- if (!dashboardCore.isInitialized) {
525
- throw new Error('Core module not initialized');
526
- }
527
-
528
- // Test API client
529
- if (!dashboardCore.apiClient) {
530
- throw new Error('API client not available');
531
- }
532
-
533
- this.logResult({
534
- test: 'Core System',
535
- status: 'success',
536
- message: 'Core system integration working correctly'
537
- });
538
-
539
- } catch (error) {
540
- this.logResult({
541
- test: 'Core System',
542
- status: 'error',
543
- message: `Core system test failed: ${error.message}`
544
- });
545
- }
546
-
547
- this.updateStats();
548
- }
549
-
550
- async testAPIConnectivity() {
551
- this.logResult({
552
- test: 'API Connectivity',
553
- status: 'started',
554
- message: 'Testing API connectivity'
555
- });
556
-
557
- const endpoints = [
558
- '/api/health',
559
- '/api/dashboard/summary',
560
- '/api/documents',
561
- '/api/ocr/status',
562
- '/api/scraping/health',
563
- '/api/analytics/overview'
564
- ];
565
-
566
- let successCount = 0;
567
- let totalCount = endpoints.length;
568
-
569
- for (const endpoint of endpoints) {
570
- try {
571
- const response = await fetch(`${this.baseURL}${endpoint}`);
572
- if (response.ok) {
573
- successCount++;
574
- this.logResult({
575
- test: 'API Connectivity',
576
- endpoint: endpoint,
577
- status: 'success',
578
- message: `${endpoint} - OK`
579
- });
580
- } else {
581
- this.logResult({
582
- test: 'API Connectivity',
583
- endpoint: endpoint,
584
- status: 'error',
585
- message: `${endpoint} - HTTP ${response.status}`
586
- });
587
- }
588
- } catch (error) {
589
- this.logResult({
590
- test: 'API Connectivity',
591
- endpoint: endpoint,
592
- status: 'error',
593
- message: `${endpoint} - ${error.message}`
594
- });
595
- }
596
- }
597
-
598
- const successRate = Math.round((successCount / totalCount) * 100);
599
- this.logResult({
600
- test: 'API Connectivity',
601
- status: 'completed',
602
- message: `API connectivity test completed: ${successCount}/${totalCount} endpoints working (${successRate}%)`
603
- });
604
-
605
- this.updateStats();
606
- }
607
-
608
- async testPageIntegration() {
609
- this.logResult({
610
- test: 'Page Integration',
611
- status: 'started',
612
- message: 'Testing page integration with core system'
613
- });
614
-
615
- try {
616
- // Test if pages can communicate with core
617
- if (typeof dashboardCore !== 'undefined') {
618
- // Test event broadcasting
619
- dashboardCore.broadcast('testIntegration', { test: true });
620
-
621
- // Test event listening
622
- let eventReceived = false;
623
- const unsubscribe = dashboardCore.listen('testIntegration', (data) => {
624
- eventReceived = true;
625
- });
626
-
627
- // Broadcast again to trigger the listener
628
- dashboardCore.broadcast('testIntegration', { test: true });
629
-
630
- // Clean up
631
- if (unsubscribe) unsubscribe();
632
-
633
- this.logResult({
634
- test: 'Page Integration',
635
- status: 'success',
636
- message: 'Page integration with core system working correctly'
637
- });
638
- } else {
639
- throw new Error('Core system not available');
640
- }
641
-
642
- } catch (error) {
643
- this.logResult({
644
- test: 'Page Integration',
645
- status: 'error',
646
- message: `Page integration test failed: ${error.message}`
647
- });
648
- }
649
-
650
- this.updateStats();
651
- }
652
-
653
- logResult(result) {
654
- this.results.push({
655
- ...result,
656
- timestamp: new Date().toISOString()
657
- });
658
-
659
- const resultsDiv = document.getElementById('testResults');
660
- const resultEntry = document.createElement('div');
661
- resultEntry.className = `test-result ${result.status === 'success' || result.status === 'completed' ? 'success' : 'error'}`;
662
- resultEntry.innerHTML = `
663
- <strong>${result.page || result.test}</strong>${result.test && result.page ? ` - ${result.test}` : ''} -
664
- ${result.status.toUpperCase()} -
665
- ${result.message}
666
- <br><small>${new Date().toLocaleTimeString()}</small>
667
- `;
668
-
669
- resultsDiv.appendChild(resultEntry);
670
- resultsDiv.scrollTop = resultsDiv.scrollHeight;
671
- }
672
-
673
- updateStats() {
674
- const total = this.results.length;
675
- const passed = this.results.filter(r =>
676
- r.status === 'success' || r.status === 'completed'
677
- ).length;
678
- const failed = total - passed;
679
- const successRate = total > 0 ? Math.round((passed / total) * 100) : 0;
680
-
681
- this.testStats = { total, passed, failed, successRate };
682
-
683
- document.getElementById('totalPages').textContent = total;
684
- document.getElementById('passedPages').textContent = passed;
685
- document.getElementById('failedPages').textContent = failed;
686
- document.getElementById('successRate').textContent = successRate + '%';
687
-
688
- const progressBar = document.getElementById('progressBar');
689
- progressBar.style.width = successRate + '%';
690
- progressBar.style.background = successRate >= 80 ? '#10b981' : successRate >= 60 ? '#f59e0b' : '#ef4444';
691
- }
692
-
693
- clearResults() {
694
- this.results = [];
695
- document.getElementById('testResults').innerHTML = '';
696
- this.updateStats();
697
-
698
- // Reset all page tests
699
- this.pages.forEach((page, index) => {
700
- const testDiv = document.getElementById(`page-${index}`);
701
- testDiv.className = 'page-test';
702
- testDiv.querySelector('.status-indicator').className = 'status-indicator';
703
- testDiv.querySelector('.test-page-btn').disabled = false;
704
-
705
- page.tests.forEach((test, testIndex) => {
706
- const testDiv = document.getElementById(`test-${index}-${testIndex}`);
707
- testDiv.querySelector('.status-indicator').className = 'status-indicator';
708
- });
709
- });
710
- }
711
-
712
- exportResults() {
713
- const data = {
714
- timestamp: new Date().toISOString(),
715
- stats: this.testStats,
716
- results: this.results
717
- };
718
-
719
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
720
- const url = URL.createObjectURL(blob);
721
- const a = document.createElement('a');
722
- a.href = url;
723
- a.download = `comprehensive-test-results-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
724
- a.click();
725
- URL.revokeObjectURL(url);
726
- }
727
-
728
- delay(ms) {
729
- return new Promise(resolve => setTimeout(resolve, ms));
730
- }
731
- }
732
-
733
- // Global tester instance
734
- const tester = new ComprehensiveTester();
735
-
736
- // Global functions for button clicks
737
- function runAllTests() {
738
- tester.runAllTests();
739
- }
740
-
741
- function testCoreSystem() {
742
- tester.testCoreSystem();
743
- }
744
-
745
- function testAPIConnectivity() {
746
- tester.testAPIConnectivity();
747
- }
748
-
749
- function testPageIntegration() {
750
- tester.testPageIntegration();
751
- }
752
-
753
- function clearResults() {
754
- tester.clearResults();
755
- }
756
-
757
- function exportResults() {
758
- tester.exportResults();
759
- }
760
-
761
- console.log('🔍 Comprehensive Tester initialized');
762
- </script>
763
- </body>
 
 
 
764
  </html>
 
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>
frontend/dev/integration-test.html CHANGED
@@ -7,7 +7,7 @@
7
  <style>
8
  body {
9
  font-family: 'Arial', sans-serif;
10
- max-width: 1200px;
11
  margin: 0 auto;
12
  padding: 20px;
13
  background: #f5f5f5;
@@ -40,7 +40,7 @@
40
  padding: 10px;
41
  border-radius: 4px;
42
  overflow-x: auto;
43
- max-height: 300px;
44
  overflow-y: auto;
45
  }
46
  .event-log {
@@ -49,15 +49,15 @@
49
  padding: 15px;
50
  border-radius: 8px;
51
  font-family: 'Courier New', monospace;
52
- max-height: 400px;
53
  overflow-y: auto;
54
  }
55
  .status-indicator {
56
  display: inline-block;
57
- width: 12px;
58
- height: 12px;
59
  border-radius: 50%;
60
- margin-right: 8px;
61
  }
62
  .status-indicator.success { background: #10b981; }
63
  .status-indicator.error { background: #ef4444; }
@@ -67,36 +67,36 @@
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 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 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 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 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 onclick="simulateDocumentUpload()">Simulate Document Upload</button>
98
- <button onclick="simulateDocumentUpdate()">Simulate Document Update</button>
99
- <button onclick="simulateDocumentDelete()">Simulate Document Delete</button>
100
  <div id="realtimeTestResult"></div>
101
  </div>
102
 
 
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;
 
40
  padding: 10px;
41
  border-radius: 4px;
42
  overflow-x: auto;
43
+ max-block-size: 300px;
44
  overflow-y: auto;
45
  }
46
  .event-log {
 
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; }
 
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
 
frontend/dev/real-api-test.html CHANGED
@@ -163,7 +163,7 @@
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">
@@ -185,21 +185,21 @@
185
  </div>
186
  </div>
187
  <div class="progress-bar">
188
- <div class="progress-fill" id="progressBar" style="width: 0%"></div>
189
  </div>
190
  </div>
191
 
192
  <div class="test-section">
193
  <h2>🎛️ Test Controls</h2>
194
  <div class="test-controls">
195
- <button onclick="runAllTests()" id="runAllBtn">Run All Tests</button>
196
- <button onclick="runHealthTests()">Health Tests Only</button>
197
- <button onclick="runDashboardTests()">Dashboard Tests</button>
198
- <button onclick="runDocumentTests()">Document Tests</button>
199
- <button onclick="runOCRTests()">OCR Tests</button>
200
- <button onclick="runScrapingTests()">Scraping Tests</button>
201
- <button onclick="clearResults()">Clear Results</button>
202
- <button onclick="exportResults()">Export Results</button>
203
  </div>
204
  </div>
205
 
@@ -208,7 +208,7 @@
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 onclick="document.getElementById('testFileInput').click()">Select File</button>
212
  </div>
213
  <div id="uploadResults"></div>
214
  </div>
 
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">
 
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
 
 
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>
frontend/improved_legal_dashboard.html CHANGED
@@ -88,8 +88,8 @@
88
 
89
  /* اسکرول‌بار مدرن */
90
  ::-webkit-scrollbar {
91
- width: 6px;
92
- height: 6px;
93
  }
94
 
95
  ::-webkit-scrollbar-track {
@@ -110,8 +110,8 @@
110
  /* کلاس‌های کمکی */
111
  .sr-only {
112
  position: absolute;
113
- width: 1px;
114
- height: 1px;
115
  padding: 0;
116
  margin: -1px;
117
  overflow: hidden;
@@ -123,15 +123,15 @@
123
  /* کانتینر اصلی */
124
  .dashboard-container {
125
  display: flex;
126
- min-height: 100vh;
127
- width: 100%;
128
  position: relative;
129
  overflow-x: hidden;
130
  }
131
 
132
  /* سایدبار کامپکت و بهبود یافته */
133
  .sidebar {
134
- width: var(--sidebar-width);
135
  background: linear-gradient(135deg,
136
  rgba(248, 250, 252, 0.98) 0%,
137
  rgba(241, 245, 249, 0.95) 25%,
@@ -142,9 +142,9 @@
142
  -webkit-backdrop-filter: blur(25px);
143
  padding: 1rem 0;
144
  position: fixed;
145
- height: 100vh;
146
- right: 0;
147
- top: 0;
148
  z-index: 1000;
149
  overflow-y: auto;
150
  overflow-x: hidden;
@@ -152,17 +152,17 @@
152
  0 0 0 1px rgba(59, 130, 246, 0.08),
153
  -8px 0 32px rgba(59, 130, 246, 0.12),
154
  inset 0 1px 0 rgba(255, 255, 255, 0.6);
155
- border-left: 1px solid rgba(59, 130, 246, 0.15);
156
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
157
  }
158
 
159
  .sidebar::before {
160
  content: '';
161
  position: absolute;
162
- top: 0;
163
- left: 0;
164
- right: 0;
165
- bottom: 0;
166
  background:
167
  radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.03) 0%, transparent 50%),
168
  radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.02) 0%, transparent 50%),
@@ -172,7 +172,7 @@
172
  }
173
 
174
  .sidebar.collapsed {
175
- width: 70px;
176
  }
177
 
178
  .sidebar.collapsed .logo-text,
@@ -195,8 +195,8 @@
195
  .sidebar.collapsed .nav-link:hover::after {
196
  content: attr(data-tooltip);
197
  position: absolute;
198
- left: 100%;
199
- top: 50%;
200
  transform: translateY(-50%);
201
  background: rgba(0, 0, 0, 0.8);
202
  color: white;
@@ -205,7 +205,7 @@
205
  font-size: 0.8rem;
206
  white-space: nowrap;
207
  z-index: 10001;
208
- margin-left: 0.5rem;
209
  opacity: 0;
210
  animation: fadeIn 0.2s ease forwards;
211
  }
@@ -215,7 +215,7 @@
215
  }
216
 
217
  .sidebar.collapsed .nav-icon {
218
- margin-left: 0;
219
  }
220
 
221
  .sidebar.collapsed .submenu {
@@ -231,15 +231,15 @@
231
  }
232
 
233
  .sidebar.collapsed + .main-content {
234
- margin-right: 70px;
235
- width: calc(100% - 70px);
236
- max-width: calc(100% - 70px);
237
  }
238
 
239
  .sidebar-header {
240
  padding: 0 1rem 1rem;
241
- border-bottom: 1px solid rgba(59, 130, 246, 0.12);
242
- margin-bottom: 1rem;
243
  display: flex;
244
  justify-content: space-between;
245
  align-items: center;
@@ -256,10 +256,10 @@
256
  .sidebar-header::before {
257
  content: '';
258
  position: absolute;
259
- top: 0;
260
- left: 0;
261
- right: 0;
262
- bottom: 0;
263
  background: linear-gradient(135deg,
264
  rgba(59, 130, 246, 0.05) 0%,
265
  rgba(16, 185, 129, 0.03) 100%);
@@ -278,8 +278,8 @@
278
  }
279
 
280
  .logo-icon {
281
- width: 2rem;
282
- height: 2rem;
283
  background: var(--primary-gradient);
284
  border-radius: var(--border-radius-sm);
285
  display: flex;
@@ -307,8 +307,8 @@
307
  .sidebar-toggle {
308
  background: var(--primary-gradient);
309
  border: none;
310
- width: 2rem;
311
- height: 2rem;
312
  border-radius: var(--border-radius-sm);
313
  color: white;
314
  cursor: pointer;
@@ -352,7 +352,7 @@
352
 
353
  /* منوی کامپکت */
354
  .nav-section {
355
- margin-bottom: 1rem;
356
  }
357
 
358
  .nav-title {
@@ -374,10 +374,10 @@
374
  .nav-title::after {
375
  content: '';
376
  position: absolute;
377
- bottom: 0;
378
- right: 1rem;
379
- left: 1rem;
380
- height: 1px;
381
  background: linear-gradient(90deg,
382
  transparent 0%,
383
  rgba(59, 130, 246, 0.3) 50%,
@@ -458,11 +458,11 @@
458
  .nav-link.active::after {
459
  content: '';
460
  position: absolute;
461
- right: -0.5rem;
462
- top: 50%;
463
  transform: translateY(-50%);
464
- width: 3px;
465
- height: 1.5rem;
466
  background: var(--primary-gradient);
467
  border-radius: 2px;
468
  box-shadow:
@@ -471,8 +471,8 @@
471
  }
472
 
473
  .nav-icon {
474
- margin-left: 0.6rem;
475
- width: 1rem;
476
  text-align: center;
477
  font-size: 0.9rem;
478
  opacity: 0.9;
@@ -485,7 +485,7 @@
485
  color: var(--text-secondary);
486
  padding: 0.2rem;
487
  cursor: pointer;
488
- margin-right: auto;
489
  transition: var(--transition-fast);
490
  font-size: 0.8rem;
491
  }
@@ -499,14 +499,14 @@
499
  }
500
 
501
  .submenu {
502
- max-height: 0;
503
  overflow: hidden;
504
  transition: max-height 0.25s ease-out;
505
- margin-top: 0.15rem;
506
  }
507
 
508
  .submenu.open {
509
- max-height: 800px;
510
  }
511
 
512
  .submenu .nav-link {
@@ -524,11 +524,11 @@
524
  .submenu .nav-link::before {
525
  content: '';
526
  position: absolute;
527
- right: 1.2rem;
528
- top: 50%;
529
  transform: translateY(-50%);
530
- width: 3px;
531
- height: 3px;
532
  background: var(--primary-gradient);
533
  border-radius: 50%;
534
  opacity: 0.6;
@@ -555,8 +555,8 @@
555
  border-radius: 10px;
556
  font-size: var(--font-size-xs);
557
  font-weight: 600;
558
- margin-right: auto;
559
- min-width: 1.2rem;
560
  text-align: center;
561
  box-shadow:
562
  0 2px 4px rgba(239, 68, 68, 0.3),
@@ -578,19 +578,19 @@
578
  /* محتوای اصلی */
579
  .main-content {
580
  flex: 1;
581
- margin-right: var(--sidebar-width);
582
  padding: 1rem;
583
- min-height: 100vh;
584
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
585
- width: calc(100% - var(--sidebar-width));
586
- max-width: calc(100% - var(--sidebar-width));
587
  box-sizing: border-box;
588
  }
589
 
590
  .main-content.sidebar-collapsed {
591
- margin-right: 70px;
592
- width: calc(100% - 70px);
593
- max-width: calc(100% - 70px);
594
  }
595
 
596
  /* هدر کامپکت */
@@ -598,7 +598,7 @@
598
  display: flex;
599
  justify-content: space-between;
600
  align-items: center;
601
- margin-bottom: 1.2rem;
602
  padding: 0.8rem 0;
603
  }
604
 
@@ -617,10 +617,10 @@
617
  .dashboard-title::after {
618
  content: '';
619
  position: absolute;
620
- bottom: -0.3rem;
621
- right: 0;
622
- width: 2.5rem;
623
- height: 2px;
624
  background: var(--primary-gradient);
625
  border-radius: 2px;
626
  }
@@ -642,7 +642,7 @@
642
  }
643
 
644
  .search-input {
645
- width: 280px;
646
  padding: 0.6rem 1rem 0.6rem 2.2rem;
647
  border: none;
648
  border-radius: 20px;
@@ -670,8 +670,8 @@
670
 
671
  .search-icon {
672
  position: absolute;
673
- right: 0.8rem;
674
- top: 50%;
675
  transform: translateY(-50%);
676
  color: var(--text-secondary);
677
  font-size: 0.9rem;
@@ -698,8 +698,8 @@
698
  }
699
 
700
  .user-avatar {
701
- width: 1.8rem;
702
- height: 1.8rem;
703
  border-radius: 50%;
704
  background: var(--primary-gradient);
705
  display: flex;
@@ -748,7 +748,7 @@
748
  display: grid;
749
  grid-template-columns: repeat(4, 1fr);
750
  gap: 1rem;
751
- margin-bottom: 1.2rem;
752
  }
753
 
754
  .stat-card {
@@ -766,26 +766,26 @@
766
  position: relative;
767
  overflow: hidden;
768
  transition: var(--transition-smooth);
769
- min-height: 130px;
770
  }
771
 
772
  .stat-card::before {
773
  content: '';
774
  position: absolute;
775
- top: 0;
776
- left: 0;
777
- right: 0;
778
- height: 4px;
779
  background: var(--primary-gradient);
780
  }
781
 
782
  .stat-card::after {
783
  content: '';
784
  position: absolute;
785
- top: 0;
786
- left: 0;
787
- right: 0;
788
- bottom: 0;
789
  background: linear-gradient(135deg,
790
  rgba(255, 255, 255, 0.1) 0%,
791
  rgba(255, 255, 255, 0.05) 50%,
@@ -840,14 +840,14 @@
840
  display: flex;
841
  justify-content: space-between;
842
  align-items: flex-start;
843
- margin-bottom: 0.8rem;
844
  position: relative;
845
  z-index: 1;
846
  }
847
 
848
  .stat-icon {
849
- width: 2.2rem;
850
- height: 2.2rem;
851
  border-radius: var(--border-radius-sm);
852
  display: flex;
853
  align-items: center;
@@ -895,7 +895,7 @@
895
  font-size: var(--font-size-xs);
896
  color: var(--text-secondary);
897
  font-weight: 600;
898
- margin-bottom: 0.3rem;
899
  line-height: 1.3;
900
  }
901
 
@@ -904,13 +904,13 @@
904
  font-weight: 800;
905
  color: var(--text-primary);
906
  line-height: 1;
907
- margin-bottom: 0.3rem;
908
  }
909
 
910
  .stat-extra {
911
  font-size: var(--font-size-xs);
912
  color: var(--text-muted);
913
- margin-bottom: 0.3rem;
914
  }
915
 
916
  .stat-change {
@@ -929,7 +929,7 @@
929
  display: grid;
930
  grid-template-columns: 2fr 1fr;
931
  gap: 1.5rem;
932
- margin-bottom: 1.5rem;
933
  }
934
 
935
  .chart-card {
@@ -951,10 +951,10 @@
951
  .chart-card::after {
952
  content: '';
953
  position: absolute;
954
- top: 0;
955
- left: 0;
956
- right: 0;
957
- bottom: 0;
958
  background: linear-gradient(135deg,
959
  rgba(255, 255, 255, 0.08) 0%,
960
  rgba(255, 255, 255, 0.03) 50%,
@@ -976,7 +976,7 @@
976
  display: flex;
977
  justify-content: space-between;
978
  align-items: center;
979
- margin-bottom: 1.2rem;
980
  }
981
 
982
  .chart-title {
@@ -1014,12 +1014,12 @@
1014
  }
1015
 
1016
  .chart-container {
1017
- height: 280px;
1018
  position: relative;
1019
  }
1020
 
1021
  .chart-placeholder {
1022
- height: 100%;
1023
  display: flex;
1024
  align-items: center;
1025
  justify-content: center;
@@ -1032,15 +1032,15 @@
1032
 
1033
  .chart-placeholder i {
1034
  font-size: 3rem;
1035
- margin-bottom: 1rem;
1036
  opacity: 0.3;
1037
  }
1038
 
1039
  /* Toast Notifications */
1040
  .toast-container {
1041
  position: fixed;
1042
- top: 1rem;
1043
- left: 1rem;
1044
  z-index: 10001;
1045
  display: flex;
1046
  flex-direction: column;
@@ -1052,11 +1052,11 @@
1052
  border-radius: var(--border-radius-sm);
1053
  padding: 1rem 1.5rem;
1054
  box-shadow: var(--shadow-lg);
1055
- border-left: 4px solid;
1056
  display: flex;
1057
  align-items: center;
1058
  gap: 0.8rem;
1059
- min-width: 300px;
1060
  transform: translateX(-100%);
1061
  transition: all 0.3s ease;
1062
  }
@@ -1066,19 +1066,19 @@
1066
  }
1067
 
1068
  .toast.success {
1069
- border-left-color: #10b981;
1070
  }
1071
 
1072
  .toast.error {
1073
- border-left-color: #ef4444;
1074
  }
1075
 
1076
  .toast.warning {
1077
- border-left-color: #f59e0b;
1078
  }
1079
 
1080
  .toast.info {
1081
- border-left-color: #3b82f6;
1082
  }
1083
 
1084
  .toast-icon {
@@ -1108,7 +1108,7 @@
1108
  .toast-title {
1109
  font-weight: 600;
1110
  font-size: var(--font-size-sm);
1111
- margin-bottom: 0.2rem;
1112
  }
1113
 
1114
  .toast-message {
@@ -1132,8 +1132,8 @@
1132
  /* Connection Status */
1133
  .connection-status {
1134
  position: fixed;
1135
- bottom: 1rem;
1136
- left: 1rem;
1137
  background: var(--card-bg);
1138
  border-radius: var(--border-radius-sm);
1139
  padding: 0.5rem 1rem;
@@ -1142,23 +1142,23 @@
1142
  align-items: center;
1143
  gap: 0.5rem;
1144
  font-size: var(--font-size-xs);
1145
- border-left: 3px solid;
1146
  z-index: 1000;
1147
  }
1148
 
1149
  .connection-status.online {
1150
- border-left-color: #10b981;
1151
  color: #047857;
1152
  }
1153
 
1154
  .connection-status.offline {
1155
- border-left-color: #ef4444;
1156
  color: #b91c1c;
1157
  }
1158
 
1159
  .status-indicator {
1160
- width: 8px;
1161
- height: 8px;
1162
  border-radius: 50%;
1163
  }
1164
 
@@ -1211,13 +1211,13 @@
1211
  }
1212
 
1213
  /* واکنش‌گرایی */
1214
- @media (max-width: 1200px) {
1215
  .charts-section {
1216
  grid-template-columns: 1fr;
1217
  }
1218
 
1219
  .search-input {
1220
- width: 220px;
1221
  }
1222
 
1223
  .stats-grid {
@@ -1225,7 +1225,7 @@
1225
  }
1226
  }
1227
 
1228
- @media (max-width: 992px) {
1229
  .mobile-menu-toggle {
1230
  display: block;
1231
  }
@@ -1241,16 +1241,16 @@
1241
  }
1242
 
1243
  .main-content {
1244
- margin-right: 0;
1245
- width: 100%;
1246
- max-width: 100%;
1247
  padding: 1rem;
1248
  }
1249
 
1250
  .main-content.sidebar-collapsed {
1251
- margin-right: 0;
1252
- width: 100%;
1253
- max-width: 100%;
1254
  }
1255
 
1256
  .dashboard-header {
@@ -1260,18 +1260,18 @@
1260
  }
1261
 
1262
  .header-actions {
1263
- width: 100%;
1264
  justify-content: space-between;
1265
  flex-direction: column;
1266
  gap: 0.8rem;
1267
  }
1268
 
1269
  .search-container {
1270
- width: 100%;
1271
  }
1272
 
1273
  .search-input {
1274
- width: 100%;
1275
  }
1276
 
1277
  .sidebar-toggle {
@@ -1279,12 +1279,12 @@
1279
  }
1280
  }
1281
 
1282
- @media (max-width: 768px) {
1283
  .main-content {
1284
  padding: 0.8rem;
1285
- width: 100%;
1286
- margin-right: 0;
1287
- max-width: 100%;
1288
  }
1289
 
1290
  .stats-grid {
@@ -1293,7 +1293,7 @@
1293
  }
1294
 
1295
  .stat-card {
1296
- min-height: 100px;
1297
  padding: 0.8rem;
1298
  }
1299
 
@@ -1302,11 +1302,11 @@
1302
  }
1303
 
1304
  .chart-container {
1305
- height: 220px;
1306
  }
1307
  }
1308
 
1309
- @media (max-width: 480px) {
1310
  .dashboard-title {
1311
  font-size: var(--font-size-xl);
1312
  }
@@ -1321,7 +1321,7 @@
1321
 
1322
  .main-content {
1323
  padding: 0.5rem;
1324
- width: 100%;
1325
  }
1326
  }
1327
 
@@ -1330,7 +1330,7 @@
1330
  background: var(--card-bg);
1331
  border-radius: var(--border-radius);
1332
  padding: 1.5rem;
1333
- margin-bottom: 2rem;
1334
  box-shadow: var(--shadow-sm);
1335
  }
1336
 
@@ -1378,7 +1378,7 @@
1378
  }
1379
 
1380
  .upload-queue {
1381
- margin-top: 1.5rem;
1382
  padding: 1rem;
1383
  background: #f8fafc;
1384
  border-radius: var(--border-radius);
@@ -1391,7 +1391,7 @@
1391
  padding: 0.8rem;
1392
  background: white;
1393
  border-radius: var(--border-radius-sm);
1394
- margin-bottom: 0.5rem;
1395
  box-shadow: var(--shadow-xs);
1396
  }
1397
 
@@ -1422,39 +1422,39 @@
1422
  }
1423
 
1424
  .upload-progress {
1425
- margin-top: 1rem;
1426
  padding: 1rem;
1427
  background: #f8fafc;
1428
  border-radius: var(--border-radius);
1429
  }
1430
 
1431
  .progress-bar {
1432
- width: 100%;
1433
- height: 8px;
1434
  background: #e2e8f0;
1435
  border-radius: 4px;
1436
  overflow: hidden;
1437
  }
1438
 
1439
  .progress-fill {
1440
- height: 100%;
1441
  background: var(--primary-gradient);
1442
  transition: width 0.3s ease;
1443
  }
1444
 
1445
  .progress-text {
1446
- margin-top: 0.5rem;
1447
  text-align: center;
1448
  color: var(--text-secondary);
1449
  }
1450
 
1451
  .upload-actions {
1452
- margin-top: 1rem;
1453
  text-align: center;
1454
  }
1455
 
1456
  .ocr-results {
1457
- margin-top: 2rem;
1458
  padding: 1rem;
1459
  background: #f8fafc;
1460
  border-radius: var(--border-radius);
@@ -1464,7 +1464,7 @@
1464
  background: white;
1465
  padding: 1rem;
1466
  border-radius: var(--border-radius-sm);
1467
- margin-bottom: 1rem;
1468
  box-shadow: var(--shadow-xs);
1469
  }
1470
 
@@ -1472,7 +1472,7 @@
1472
  background: #f1f5f9;
1473
  padding: 0.8rem;
1474
  border-radius: var(--border-radius-sm);
1475
- margin-top: 0.5rem;
1476
  font-family: monospace;
1477
  white-space: pre-wrap;
1478
  }
@@ -1482,7 +1482,7 @@
1482
  background: var(--card-bg);
1483
  border-radius: var(--border-radius);
1484
  padding: 1.5rem;
1485
- margin-bottom: 2rem;
1486
  box-shadow: var(--shadow-sm);
1487
  }
1488
 
@@ -1490,7 +1490,7 @@
1490
  display: flex;
1491
  justify-content: space-between;
1492
  align-items: center;
1493
- margin-bottom: 1.5rem;
1494
  }
1495
 
1496
  .section-title {
@@ -1510,7 +1510,7 @@
1510
  display: grid;
1511
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1512
  gap: 1rem;
1513
- margin-bottom: 1.5rem;
1514
  padding: 1rem;
1515
  background: #f8fafc;
1516
  border-radius: var(--border-radius);
@@ -1563,7 +1563,7 @@
1563
  display: flex;
1564
  justify-content: space-between;
1565
  align-items: center;
1566
- margin-bottom: 0.8rem;
1567
  }
1568
 
1569
  .document-title {
@@ -1653,7 +1653,7 @@
1653
  background: var(--card-bg);
1654
  border-radius: var(--border-radius);
1655
  padding: 1.5rem;
1656
- margin-bottom: 2rem;
1657
  box-shadow: var(--shadow-sm);
1658
  }
1659
 
@@ -1661,7 +1661,7 @@
1661
  display: grid;
1662
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1663
  gap: 1rem;
1664
- margin-bottom: 1.5rem;
1665
  padding: 1rem;
1666
  background: #f8fafc;
1667
  border-radius: var(--border-radius);
@@ -1697,7 +1697,7 @@
1697
  .scraping-actions {
1698
  display: flex;
1699
  gap: 1rem;
1700
- margin-bottom: 1.5rem;
1701
  flex-wrap: wrap;
1702
  }
1703
 
@@ -1705,14 +1705,14 @@
1705
  padding: 1rem;
1706
  background: #f8fafc;
1707
  border-radius: var(--border-radius);
1708
- margin-bottom: 1.5rem;
1709
  }
1710
 
1711
  .status-info {
1712
  display: flex;
1713
  align-items: center;
1714
  gap: 0.8rem;
1715
- margin-bottom: 1rem;
1716
  }
1717
 
1718
  .status-label {
@@ -1756,7 +1756,7 @@
1756
  display: grid;
1757
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1758
  gap: 1rem;
1759
- margin-top: 1rem;
1760
  }
1761
 
1762
  .stat-item {
@@ -1780,14 +1780,14 @@
1780
  }
1781
 
1782
  .scraping-results {
1783
- margin-bottom: 1.5rem;
1784
  }
1785
 
1786
  .scraping-item {
1787
  background: white;
1788
  border-radius: var(--border-radius);
1789
  padding: 1rem;
1790
- margin-bottom: 1rem;
1791
  box-shadow: var(--shadow-xs);
1792
  transition: var(--transition-smooth);
1793
  }
@@ -1800,7 +1800,7 @@
1800
  display: flex;
1801
  justify-content: space-between;
1802
  align-items: center;
1803
- margin-bottom: 0.8rem;
1804
  }
1805
 
1806
  .item-title {
@@ -1855,11 +1855,11 @@
1855
  display: flex;
1856
  justify-content: space-between;
1857
  align-items: center;
1858
- margin-bottom: 1rem;
1859
  }
1860
 
1861
  .logs-container {
1862
- max-height: 300px;
1863
  overflow-y: auto;
1864
  background: white;
1865
  border-radius: var(--border-radius-sm);
@@ -1871,7 +1871,7 @@
1871
  gap: 0.8rem;
1872
  padding: 0.5rem;
1873
  border-radius: var(--border-radius-sm);
1874
- margin-bottom: 0.3rem;
1875
  font-size: var(--font-size-sm);
1876
  }
1877
 
@@ -1892,7 +1892,7 @@
1892
 
1893
  .log-timestamp {
1894
  font-weight: 500;
1895
- min-width: 80px;
1896
  }
1897
 
1898
  .log-message {
@@ -1961,8 +1961,8 @@
1961
 
1962
  .sr-only {
1963
  position: absolute;
1964
- width: 1px;
1965
- height: 1px;
1966
  padding: 0;
1967
  margin: -1px;
1968
  overflow: hidden;
@@ -1973,18 +1973,18 @@
1973
 
1974
  /* Enhanced Analytics Dashboard Styles */
1975
  .analytics-dashboard {
1976
- margin-bottom: 2rem;
1977
  }
1978
 
1979
  .analytics-overview {
1980
- margin-bottom: 1.5rem;
1981
  }
1982
 
1983
  .analytics-grid {
1984
  display: grid;
1985
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
1986
  gap: 1.5rem;
1987
- margin-bottom: 1.5rem;
1988
  }
1989
 
1990
  .analytics-card {
@@ -2003,10 +2003,10 @@
2003
  .analytics-card::before {
2004
  content: '';
2005
  position: absolute;
2006
- top: 0;
2007
- left: 0;
2008
- right: 0;
2009
- height: 4px;
2010
  background: var(--primary-gradient);
2011
  }
2012
 
@@ -2019,7 +2019,7 @@
2019
  display: flex;
2020
  justify-content: space-between;
2021
  align-items: center;
2022
- margin-bottom: 1rem;
2023
  }
2024
 
2025
  .analytics-title {
@@ -2048,7 +2048,7 @@
2048
  display: grid;
2049
  grid-template-columns: repeat(3, 1fr);
2050
  gap: 1rem;
2051
- margin-bottom: 1rem;
2052
  }
2053
 
2054
  .overview-stat {
@@ -2063,7 +2063,7 @@
2063
  font-size: 1.5rem;
2064
  font-weight: 700;
2065
  color: var(--text-primary);
2066
- margin-bottom: 0.25rem;
2067
  }
2068
 
2069
  .stat-label {
@@ -2123,7 +2123,7 @@
2123
  display: grid;
2124
  grid-template-columns: repeat(3, 1fr);
2125
  gap: 1rem;
2126
- margin-bottom: 1rem;
2127
  }
2128
 
2129
  .quality-metric {
@@ -2136,7 +2136,7 @@
2136
  .metric-label {
2137
  font-size: var(--font-size-sm);
2138
  color: var(--text-secondary);
2139
- margin-bottom: 0.25rem;
2140
  }
2141
 
2142
  .metric-value {
@@ -2146,7 +2146,7 @@
2146
 
2147
  /* Health Status */
2148
  .health-status {
2149
- margin-bottom: 1rem;
2150
  }
2151
 
2152
  .status-indicator {
@@ -2167,7 +2167,7 @@
2167
  display: grid;
2168
  grid-template-columns: repeat(3, 1fr);
2169
  gap: 1rem;
2170
- margin-bottom: 1rem;
2171
  }
2172
 
2173
  .health-metric {
@@ -2181,7 +2181,7 @@
2181
  .clustering-summary {
2182
  display: flex;
2183
  justify-content: space-between;
2184
- margin-bottom: 1rem;
2185
  }
2186
 
2187
  .cluster-count, .cluster-avg {
@@ -2196,7 +2196,7 @@
2196
  .count-label, .avg-label {
2197
  font-size: var(--font-size-sm);
2198
  color: var(--text-secondary);
2199
- margin-bottom: 0.25rem;
2200
  }
2201
 
2202
  .count-value, .avg-value {
@@ -2208,12 +2208,12 @@
2208
  .overview-chart, .trends-chart, .predictions-chart,
2209
  .quality-chart, .health-chart, .clustering-chart,
2210
  .similarity-chart {
2211
- height: 200px;
2212
  position: relative;
2213
  }
2214
 
2215
  /* Responsive Design */
2216
- @media (max-width: 768px) {
2217
  .analytics-grid {
2218
  grid-template-columns: 1fr;
2219
  }
@@ -2632,7 +2632,7 @@
2632
  سلامت سیستم
2633
  </h3>
2634
  <div class="analytics-actions">
2635
- <button type="button" class="btn btn-sm btn-secondary" onclick="refreshHealth()">
2636
  <i class="fas fa-refresh"></i>
2637
  </button>
2638
  </div>
@@ -2672,7 +2672,7 @@
2672
  خوشه‌بندی اسناد
2673
  </h3>
2674
  <div class="analytics-actions">
2675
- <button type="button" class="btn btn-sm btn-secondary" onclick="refreshClustering()">
2676
  <i class="fas fa-refresh"></i>
2677
  </button>
2678
  </div>
@@ -2705,7 +2705,7 @@
2705
  تحلیل شباهت
2706
  </h3>
2707
  <div class="analytics-actions">
2708
- <button type="button" class="btn btn-sm btn-secondary" onclick="refreshSimilarity()">
2709
  <i class="fas fa-refresh"></i>
2710
  </button>
2711
  </div>
@@ -2730,7 +2730,7 @@
2730
  آپلود و پردازش اسناد
2731
  </h2>
2732
  </div>
2733
-
2734
  <div class="upload-container">
2735
  <div class="upload-drop-zone" id="uploadDropZone">
2736
  <div class="upload-content">
 
88
 
89
  /* اسکرول‌بار مدرن */
90
  ::-webkit-scrollbar {
91
+ inline-size: 6px;
92
+ block-size: 6px;
93
  }
94
 
95
  ::-webkit-scrollbar-track {
 
110
  /* کلاس‌های کمکی */
111
  .sr-only {
112
  position: absolute;
113
+ inline-size: 1px;
114
+ block-size: 1px;
115
  padding: 0;
116
  margin: -1px;
117
  overflow: hidden;
 
123
  /* کانتینر اصلی */
124
  .dashboard-container {
125
  display: flex;
126
+ min-block-size: 100vh;
127
+ inline-size: 100%;
128
  position: relative;
129
  overflow-x: hidden;
130
  }
131
 
132
  /* سایدبار کامپکت و بهبود یافته */
133
  .sidebar {
134
+ inline-size: var(--sidebar-width);
135
  background: linear-gradient(135deg,
136
  rgba(248, 250, 252, 0.98) 0%,
137
  rgba(241, 245, 249, 0.95) 25%,
 
142
  -webkit-backdrop-filter: blur(25px);
143
  padding: 1rem 0;
144
  position: fixed;
145
+ block-size: 100vh;
146
+ inset-inline-end: 0;
147
+ inset-block-start: 0;
148
  z-index: 1000;
149
  overflow-y: auto;
150
  overflow-x: hidden;
 
152
  0 0 0 1px rgba(59, 130, 246, 0.08),
153
  -8px 0 32px rgba(59, 130, 246, 0.12),
154
  inset 0 1px 0 rgba(255, 255, 255, 0.6);
155
+ border-inline-start: 1px solid rgba(59, 130, 246, 0.15);
156
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
157
  }
158
 
159
  .sidebar::before {
160
  content: '';
161
  position: absolute;
162
+ inset-block-start: 0;
163
+ inset-inline-start: 0;
164
+ inset-inline-end: 0;
165
+ inset-block-end: 0;
166
  background:
167
  radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.03) 0%, transparent 50%),
168
  radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.02) 0%, transparent 50%),
 
172
  }
173
 
174
  .sidebar.collapsed {
175
+ inline-size: 70px;
176
  }
177
 
178
  .sidebar.collapsed .logo-text,
 
195
  .sidebar.collapsed .nav-link:hover::after {
196
  content: attr(data-tooltip);
197
  position: absolute;
198
+ inset-inline-start: 100%;
199
+ inset-block-start: 50%;
200
  transform: translateY(-50%);
201
  background: rgba(0, 0, 0, 0.8);
202
  color: white;
 
205
  font-size: 0.8rem;
206
  white-space: nowrap;
207
  z-index: 10001;
208
+ margin-inline-start: 0.5rem;
209
  opacity: 0;
210
  animation: fadeIn 0.2s ease forwards;
211
  }
 
215
  }
216
 
217
  .sidebar.collapsed .nav-icon {
218
+ margin-inline-start: 0;
219
  }
220
 
221
  .sidebar.collapsed .submenu {
 
231
  }
232
 
233
  .sidebar.collapsed + .main-content {
234
+ margin-inline-end: 70px;
235
+ inline-size: calc(100% - 70px);
236
+ max-inline-size: calc(100% - 70px);
237
  }
238
 
239
  .sidebar-header {
240
  padding: 0 1rem 1rem;
241
+ border-block-end: 1px solid rgba(59, 130, 246, 0.12);
242
+ margin-block-end: 1rem;
243
  display: flex;
244
  justify-content: space-between;
245
  align-items: center;
 
256
  .sidebar-header::before {
257
  content: '';
258
  position: absolute;
259
+ inset-block-start: 0;
260
+ inset-inline-start: 0;
261
+ inset-inline-end: 0;
262
+ inset-block-end: 0;
263
  background: linear-gradient(135deg,
264
  rgba(59, 130, 246, 0.05) 0%,
265
  rgba(16, 185, 129, 0.03) 100%);
 
278
  }
279
 
280
  .logo-icon {
281
+ inline-size: 2rem;
282
+ block-size: 2rem;
283
  background: var(--primary-gradient);
284
  border-radius: var(--border-radius-sm);
285
  display: flex;
 
307
  .sidebar-toggle {
308
  background: var(--primary-gradient);
309
  border: none;
310
+ inline-size: 2rem;
311
+ block-size: 2rem;
312
  border-radius: var(--border-radius-sm);
313
  color: white;
314
  cursor: pointer;
 
352
 
353
  /* منوی کامپکت */
354
  .nav-section {
355
+ margin-block-end: 1rem;
356
  }
357
 
358
  .nav-title {
 
374
  .nav-title::after {
375
  content: '';
376
  position: absolute;
377
+ inset-block-end: 0;
378
+ inset-inline-end: 1rem;
379
+ inset-inline-start: 1rem;
380
+ block-size: 1px;
381
  background: linear-gradient(90deg,
382
  transparent 0%,
383
  rgba(59, 130, 246, 0.3) 50%,
 
458
  .nav-link.active::after {
459
  content: '';
460
  position: absolute;
461
+ inset-inline-end: -0.5rem;
462
+ inset-block-start: 50%;
463
  transform: translateY(-50%);
464
+ inline-size: 3px;
465
+ block-size: 1.5rem;
466
  background: var(--primary-gradient);
467
  border-radius: 2px;
468
  box-shadow:
 
471
  }
472
 
473
  .nav-icon {
474
+ margin-inline-start: 0.6rem;
475
+ inline-size: 1rem;
476
  text-align: center;
477
  font-size: 0.9rem;
478
  opacity: 0.9;
 
485
  color: var(--text-secondary);
486
  padding: 0.2rem;
487
  cursor: pointer;
488
+ margin-inline-end: auto;
489
  transition: var(--transition-fast);
490
  font-size: 0.8rem;
491
  }
 
499
  }
500
 
501
  .submenu {
502
+ max-block-size: 0;
503
  overflow: hidden;
504
  transition: max-height 0.25s ease-out;
505
+ margin-block-start: 0.15rem;
506
  }
507
 
508
  .submenu.open {
509
+ max-block-size: 800px;
510
  }
511
 
512
  .submenu .nav-link {
 
524
  .submenu .nav-link::before {
525
  content: '';
526
  position: absolute;
527
+ inset-inline-end: 1.2rem;
528
+ inset-block-start: 50%;
529
  transform: translateY(-50%);
530
+ inline-size: 3px;
531
+ block-size: 3px;
532
  background: var(--primary-gradient);
533
  border-radius: 50%;
534
  opacity: 0.6;
 
555
  border-radius: 10px;
556
  font-size: var(--font-size-xs);
557
  font-weight: 600;
558
+ margin-inline-end: auto;
559
+ min-inline-size: 1.2rem;
560
  text-align: center;
561
  box-shadow:
562
  0 2px 4px rgba(239, 68, 68, 0.3),
 
578
  /* محتوای اصلی */
579
  .main-content {
580
  flex: 1;
581
+ margin-inline-end: var(--sidebar-width);
582
  padding: 1rem;
583
+ min-block-size: 100vh;
584
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
585
+ inline-size: calc(100% - var(--sidebar-width));
586
+ max-inline-size: calc(100% - var(--sidebar-width));
587
  box-sizing: border-box;
588
  }
589
 
590
  .main-content.sidebar-collapsed {
591
+ margin-inline-end: 70px;
592
+ inline-size: calc(100% - 70px);
593
+ max-inline-size: calc(100% - 70px);
594
  }
595
 
596
  /* هدر کامپکت */
 
598
  display: flex;
599
  justify-content: space-between;
600
  align-items: center;
601
+ margin-block-end: 1.2rem;
602
  padding: 0.8rem 0;
603
  }
604
 
 
617
  .dashboard-title::after {
618
  content: '';
619
  position: absolute;
620
+ inset-block-end: -0.3rem;
621
+ inset-inline-end: 0;
622
+ inline-size: 2.5rem;
623
+ block-size: 2px;
624
  background: var(--primary-gradient);
625
  border-radius: 2px;
626
  }
 
642
  }
643
 
644
  .search-input {
645
+ inline-size: 280px;
646
  padding: 0.6rem 1rem 0.6rem 2.2rem;
647
  border: none;
648
  border-radius: 20px;
 
670
 
671
  .search-icon {
672
  position: absolute;
673
+ inset-inline-end: 0.8rem;
674
+ inset-block-start: 50%;
675
  transform: translateY(-50%);
676
  color: var(--text-secondary);
677
  font-size: 0.9rem;
 
698
  }
699
 
700
  .user-avatar {
701
+ inline-size: 1.8rem;
702
+ block-size: 1.8rem;
703
  border-radius: 50%;
704
  background: var(--primary-gradient);
705
  display: flex;
 
748
  display: grid;
749
  grid-template-columns: repeat(4, 1fr);
750
  gap: 1rem;
751
+ margin-block-end: 1.2rem;
752
  }
753
 
754
  .stat-card {
 
766
  position: relative;
767
  overflow: hidden;
768
  transition: var(--transition-smooth);
769
+ min-block-size: 130px;
770
  }
771
 
772
  .stat-card::before {
773
  content: '';
774
  position: absolute;
775
+ inset-block-start: 0;
776
+ inset-inline-start: 0;
777
+ inset-inline-end: 0;
778
+ block-size: 4px;
779
  background: var(--primary-gradient);
780
  }
781
 
782
  .stat-card::after {
783
  content: '';
784
  position: absolute;
785
+ inset-block-start: 0;
786
+ inset-inline-start: 0;
787
+ inset-inline-end: 0;
788
+ inset-block-end: 0;
789
  background: linear-gradient(135deg,
790
  rgba(255, 255, 255, 0.1) 0%,
791
  rgba(255, 255, 255, 0.05) 50%,
 
840
  display: flex;
841
  justify-content: space-between;
842
  align-items: flex-start;
843
+ margin-block-end: 0.8rem;
844
  position: relative;
845
  z-index: 1;
846
  }
847
 
848
  .stat-icon {
849
+ inline-size: 2.2rem;
850
+ block-size: 2.2rem;
851
  border-radius: var(--border-radius-sm);
852
  display: flex;
853
  align-items: center;
 
895
  font-size: var(--font-size-xs);
896
  color: var(--text-secondary);
897
  font-weight: 600;
898
+ margin-block-end: 0.3rem;
899
  line-height: 1.3;
900
  }
901
 
 
904
  font-weight: 800;
905
  color: var(--text-primary);
906
  line-height: 1;
907
+ margin-block-end: 0.3rem;
908
  }
909
 
910
  .stat-extra {
911
  font-size: var(--font-size-xs);
912
  color: var(--text-muted);
913
+ margin-block-end: 0.3rem;
914
  }
915
 
916
  .stat-change {
 
929
  display: grid;
930
  grid-template-columns: 2fr 1fr;
931
  gap: 1.5rem;
932
+ margin-block-end: 1.5rem;
933
  }
934
 
935
  .chart-card {
 
951
  .chart-card::after {
952
  content: '';
953
  position: absolute;
954
+ inset-block-start: 0;
955
+ inset-inline-start: 0;
956
+ inset-inline-end: 0;
957
+ inset-block-end: 0;
958
  background: linear-gradient(135deg,
959
  rgba(255, 255, 255, 0.08) 0%,
960
  rgba(255, 255, 255, 0.03) 50%,
 
976
  display: flex;
977
  justify-content: space-between;
978
  align-items: center;
979
+ margin-block-end: 1.2rem;
980
  }
981
 
982
  .chart-title {
 
1014
  }
1015
 
1016
  .chart-container {
1017
+ block-size: 280px;
1018
  position: relative;
1019
  }
1020
 
1021
  .chart-placeholder {
1022
+ block-size: 100%;
1023
  display: flex;
1024
  align-items: center;
1025
  justify-content: center;
 
1032
 
1033
  .chart-placeholder i {
1034
  font-size: 3rem;
1035
+ margin-block-end: 1rem;
1036
  opacity: 0.3;
1037
  }
1038
 
1039
  /* Toast Notifications */
1040
  .toast-container {
1041
  position: fixed;
1042
+ inset-block-start: 1rem;
1043
+ inset-inline-start: 1rem;
1044
  z-index: 10001;
1045
  display: flex;
1046
  flex-direction: column;
 
1052
  border-radius: var(--border-radius-sm);
1053
  padding: 1rem 1.5rem;
1054
  box-shadow: var(--shadow-lg);
1055
+ border-inline-start: 4px solid;
1056
  display: flex;
1057
  align-items: center;
1058
  gap: 0.8rem;
1059
+ min-inline-size: 300px;
1060
  transform: translateX(-100%);
1061
  transition: all 0.3s ease;
1062
  }
 
1066
  }
1067
 
1068
  .toast.success {
1069
+ border-inline-start-color: #10b981;
1070
  }
1071
 
1072
  .toast.error {
1073
+ border-inline-start-color: #ef4444;
1074
  }
1075
 
1076
  .toast.warning {
1077
+ border-inline-start-color: #f59e0b;
1078
  }
1079
 
1080
  .toast.info {
1081
+ border-inline-start-color: #3b82f6;
1082
  }
1083
 
1084
  .toast-icon {
 
1108
  .toast-title {
1109
  font-weight: 600;
1110
  font-size: var(--font-size-sm);
1111
+ margin-block-end: 0.2rem;
1112
  }
1113
 
1114
  .toast-message {
 
1132
  /* Connection Status */
1133
  .connection-status {
1134
  position: fixed;
1135
+ inset-block-end: 1rem;
1136
+ inset-inline-start: 1rem;
1137
  background: var(--card-bg);
1138
  border-radius: var(--border-radius-sm);
1139
  padding: 0.5rem 1rem;
 
1142
  align-items: center;
1143
  gap: 0.5rem;
1144
  font-size: var(--font-size-xs);
1145
+ border-inline-start: 3px solid;
1146
  z-index: 1000;
1147
  }
1148
 
1149
  .connection-status.online {
1150
+ border-inline-start-color: #10b981;
1151
  color: #047857;
1152
  }
1153
 
1154
  .connection-status.offline {
1155
+ border-inline-start-color: #ef4444;
1156
  color: #b91c1c;
1157
  }
1158
 
1159
  .status-indicator {
1160
+ inline-size: 8px;
1161
+ block-size: 8px;
1162
  border-radius: 50%;
1163
  }
1164
 
 
1211
  }
1212
 
1213
  /* واکنش‌گرایی */
1214
+ @media (max-inline-size: 1200px) {
1215
  .charts-section {
1216
  grid-template-columns: 1fr;
1217
  }
1218
 
1219
  .search-input {
1220
+ inline-size: 220px;
1221
  }
1222
 
1223
  .stats-grid {
 
1225
  }
1226
  }
1227
 
1228
+ @media (max-inline-size: 992px) {
1229
  .mobile-menu-toggle {
1230
  display: block;
1231
  }
 
1241
  }
1242
 
1243
  .main-content {
1244
+ margin-inline-end: 0;
1245
+ inline-size: 100%;
1246
+ max-inline-size: 100%;
1247
  padding: 1rem;
1248
  }
1249
 
1250
  .main-content.sidebar-collapsed {
1251
+ margin-inline-end: 0;
1252
+ inline-size: 100%;
1253
+ max-inline-size: 100%;
1254
  }
1255
 
1256
  .dashboard-header {
 
1260
  }
1261
 
1262
  .header-actions {
1263
+ inline-size: 100%;
1264
  justify-content: space-between;
1265
  flex-direction: column;
1266
  gap: 0.8rem;
1267
  }
1268
 
1269
  .search-container {
1270
+ inline-size: 100%;
1271
  }
1272
 
1273
  .search-input {
1274
+ inline-size: 100%;
1275
  }
1276
 
1277
  .sidebar-toggle {
 
1279
  }
1280
  }
1281
 
1282
+ @media (max-inline-size: 768px) {
1283
  .main-content {
1284
  padding: 0.8rem;
1285
+ inline-size: 100%;
1286
+ margin-inline-end: 0;
1287
+ max-inline-size: 100%;
1288
  }
1289
 
1290
  .stats-grid {
 
1293
  }
1294
 
1295
  .stat-card {
1296
+ min-block-size: 100px;
1297
  padding: 0.8rem;
1298
  }
1299
 
 
1302
  }
1303
 
1304
  .chart-container {
1305
+ block-size: 220px;
1306
  }
1307
  }
1308
 
1309
+ @media (max-inline-size: 480px) {
1310
  .dashboard-title {
1311
  font-size: var(--font-size-xl);
1312
  }
 
1321
 
1322
  .main-content {
1323
  padding: 0.5rem;
1324
+ inline-size: 100%;
1325
  }
1326
  }
1327
 
 
1330
  background: var(--card-bg);
1331
  border-radius: var(--border-radius);
1332
  padding: 1.5rem;
1333
+ margin-block-end: 2rem;
1334
  box-shadow: var(--shadow-sm);
1335
  }
1336
 
 
1378
  }
1379
 
1380
  .upload-queue {
1381
+ margin-block-start: 1.5rem;
1382
  padding: 1rem;
1383
  background: #f8fafc;
1384
  border-radius: var(--border-radius);
 
1391
  padding: 0.8rem;
1392
  background: white;
1393
  border-radius: var(--border-radius-sm);
1394
+ margin-block-end: 0.5rem;
1395
  box-shadow: var(--shadow-xs);
1396
  }
1397
 
 
1422
  }
1423
 
1424
  .upload-progress {
1425
+ margin-block-start: 1rem;
1426
  padding: 1rem;
1427
  background: #f8fafc;
1428
  border-radius: var(--border-radius);
1429
  }
1430
 
1431
  .progress-bar {
1432
+ inline-size: 100%;
1433
+ block-size: 8px;
1434
  background: #e2e8f0;
1435
  border-radius: 4px;
1436
  overflow: hidden;
1437
  }
1438
 
1439
  .progress-fill {
1440
+ block-size: 100%;
1441
  background: var(--primary-gradient);
1442
  transition: width 0.3s ease;
1443
  }
1444
 
1445
  .progress-text {
1446
+ margin-block-start: 0.5rem;
1447
  text-align: center;
1448
  color: var(--text-secondary);
1449
  }
1450
 
1451
  .upload-actions {
1452
+ margin-block-start: 1rem;
1453
  text-align: center;
1454
  }
1455
 
1456
  .ocr-results {
1457
+ margin-block-start: 2rem;
1458
  padding: 1rem;
1459
  background: #f8fafc;
1460
  border-radius: var(--border-radius);
 
1464
  background: white;
1465
  padding: 1rem;
1466
  border-radius: var(--border-radius-sm);
1467
+ margin-block-end: 1rem;
1468
  box-shadow: var(--shadow-xs);
1469
  }
1470
 
 
1472
  background: #f1f5f9;
1473
  padding: 0.8rem;
1474
  border-radius: var(--border-radius-sm);
1475
+ margin-block-start: 0.5rem;
1476
  font-family: monospace;
1477
  white-space: pre-wrap;
1478
  }
 
1482
  background: var(--card-bg);
1483
  border-radius: var(--border-radius);
1484
  padding: 1.5rem;
1485
+ margin-block-end: 2rem;
1486
  box-shadow: var(--shadow-sm);
1487
  }
1488
 
 
1490
  display: flex;
1491
  justify-content: space-between;
1492
  align-items: center;
1493
+ margin-block-end: 1.5rem;
1494
  }
1495
 
1496
  .section-title {
 
1510
  display: grid;
1511
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1512
  gap: 1rem;
1513
+ margin-block-end: 1.5rem;
1514
  padding: 1rem;
1515
  background: #f8fafc;
1516
  border-radius: var(--border-radius);
 
1563
  display: flex;
1564
  justify-content: space-between;
1565
  align-items: center;
1566
+ margin-block-end: 0.8rem;
1567
  }
1568
 
1569
  .document-title {
 
1653
  background: var(--card-bg);
1654
  border-radius: var(--border-radius);
1655
  padding: 1.5rem;
1656
+ margin-block-end: 2rem;
1657
  box-shadow: var(--shadow-sm);
1658
  }
1659
 
 
1661
  display: grid;
1662
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1663
  gap: 1rem;
1664
+ margin-block-end: 1.5rem;
1665
  padding: 1rem;
1666
  background: #f8fafc;
1667
  border-radius: var(--border-radius);
 
1697
  .scraping-actions {
1698
  display: flex;
1699
  gap: 1rem;
1700
+ margin-block-end: 1.5rem;
1701
  flex-wrap: wrap;
1702
  }
1703
 
 
1705
  padding: 1rem;
1706
  background: #f8fafc;
1707
  border-radius: var(--border-radius);
1708
+ margin-block-end: 1.5rem;
1709
  }
1710
 
1711
  .status-info {
1712
  display: flex;
1713
  align-items: center;
1714
  gap: 0.8rem;
1715
+ margin-block-end: 1rem;
1716
  }
1717
 
1718
  .status-label {
 
1756
  display: grid;
1757
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1758
  gap: 1rem;
1759
+ margin-block-start: 1rem;
1760
  }
1761
 
1762
  .stat-item {
 
1780
  }
1781
 
1782
  .scraping-results {
1783
+ margin-block-end: 1.5rem;
1784
  }
1785
 
1786
  .scraping-item {
1787
  background: white;
1788
  border-radius: var(--border-radius);
1789
  padding: 1rem;
1790
+ margin-block-end: 1rem;
1791
  box-shadow: var(--shadow-xs);
1792
  transition: var(--transition-smooth);
1793
  }
 
1800
  display: flex;
1801
  justify-content: space-between;
1802
  align-items: center;
1803
+ margin-block-end: 0.8rem;
1804
  }
1805
 
1806
  .item-title {
 
1855
  display: flex;
1856
  justify-content: space-between;
1857
  align-items: center;
1858
+ margin-block-end: 1rem;
1859
  }
1860
 
1861
  .logs-container {
1862
+ max-block-size: 300px;
1863
  overflow-y: auto;
1864
  background: white;
1865
  border-radius: var(--border-radius-sm);
 
1871
  gap: 0.8rem;
1872
  padding: 0.5rem;
1873
  border-radius: var(--border-radius-sm);
1874
+ margin-block-end: 0.3rem;
1875
  font-size: var(--font-size-sm);
1876
  }
1877
 
 
1892
 
1893
  .log-timestamp {
1894
  font-weight: 500;
1895
+ min-inline-size: 80px;
1896
  }
1897
 
1898
  .log-message {
 
1961
 
1962
  .sr-only {
1963
  position: absolute;
1964
+ inline-size: 1px;
1965
+ block-size: 1px;
1966
  padding: 0;
1967
  margin: -1px;
1968
  overflow: hidden;
 
1973
 
1974
  /* Enhanced Analytics Dashboard Styles */
1975
  .analytics-dashboard {
1976
+ margin-block-end: 2rem;
1977
  }
1978
 
1979
  .analytics-overview {
1980
+ margin-block-end: 1.5rem;
1981
  }
1982
 
1983
  .analytics-grid {
1984
  display: grid;
1985
  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
1986
  gap: 1.5rem;
1987
+ margin-block-end: 1.5rem;
1988
  }
1989
 
1990
  .analytics-card {
 
2003
  .analytics-card::before {
2004
  content: '';
2005
  position: absolute;
2006
+ inset-block-start: 0;
2007
+ inset-inline-start: 0;
2008
+ inset-inline-end: 0;
2009
+ block-size: 4px;
2010
  background: var(--primary-gradient);
2011
  }
2012
 
 
2019
  display: flex;
2020
  justify-content: space-between;
2021
  align-items: center;
2022
+ margin-block-end: 1rem;
2023
  }
2024
 
2025
  .analytics-title {
 
2048
  display: grid;
2049
  grid-template-columns: repeat(3, 1fr);
2050
  gap: 1rem;
2051
+ margin-block-end: 1rem;
2052
  }
2053
 
2054
  .overview-stat {
 
2063
  font-size: 1.5rem;
2064
  font-weight: 700;
2065
  color: var(--text-primary);
2066
+ margin-block-end: 0.25rem;
2067
  }
2068
 
2069
  .stat-label {
 
2123
  display: grid;
2124
  grid-template-columns: repeat(3, 1fr);
2125
  gap: 1rem;
2126
+ margin-block-end: 1rem;
2127
  }
2128
 
2129
  .quality-metric {
 
2136
  .metric-label {
2137
  font-size: var(--font-size-sm);
2138
  color: var(--text-secondary);
2139
+ margin-block-end: 0.25rem;
2140
  }
2141
 
2142
  .metric-value {
 
2146
 
2147
  /* Health Status */
2148
  .health-status {
2149
+ margin-block-end: 1rem;
2150
  }
2151
 
2152
  .status-indicator {
 
2167
  display: grid;
2168
  grid-template-columns: repeat(3, 1fr);
2169
  gap: 1rem;
2170
+ margin-block-end: 1rem;
2171
  }
2172
 
2173
  .health-metric {
 
2181
  .clustering-summary {
2182
  display: flex;
2183
  justify-content: space-between;
2184
+ margin-block-end: 1rem;
2185
  }
2186
 
2187
  .cluster-count, .cluster-avg {
 
2196
  .count-label, .avg-label {
2197
  font-size: var(--font-size-sm);
2198
  color: var(--text-secondary);
2199
+ margin-block-end: 0.25rem;
2200
  }
2201
 
2202
  .count-value, .avg-value {
 
2208
  .overview-chart, .trends-chart, .predictions-chart,
2209
  .quality-chart, .health-chart, .clustering-chart,
2210
  .similarity-chart {
2211
+ block-size: 200px;
2212
  position: relative;
2213
  }
2214
 
2215
  /* Responsive Design */
2216
+ @media (max-inline-size: 768px) {
2217
  .analytics-grid {
2218
  grid-template-columns: 1fr;
2219
  }
 
2632
  سلامت سیستم
2633
  </h3>
2634
  <div class="analytics-actions">
2635
+ <button type="button" class="btn btn-sm btn-secondary" onclick="refreshHealth()" aria-label="Refresh system health">
2636
  <i class="fas fa-refresh"></i>
2637
  </button>
2638
  </div>
 
2672
  خوشه‌بندی اسناد
2673
  </h3>
2674
  <div class="analytics-actions">
2675
+ <button type="button" class="btn btn-sm btn-secondary" onclick="refreshClustering()" aria-label="Refresh clustering analysis">
2676
  <i class="fas fa-refresh"></i>
2677
  </button>
2678
  </div>
 
2705
  تحلیل شباهت
2706
  </h3>
2707
  <div class="analytics-actions">
2708
+ <button type="button" class="btn btn-sm btn-secondary" onclick="refreshSimilarity()" aria-label="Refresh similarity analysis">
2709
  <i class="fas fa-refresh"></i>
2710
  </button>
2711
  </div>
 
2730
  آپلود و پردازش اسناد
2731
  </h2>
2732
  </div>
2733
+
2734
  <div class="upload-container">
2735
  <div class="upload-drop-zone" id="uploadDropZone">
2736
  <div class="upload-content">
frontend/index.html CHANGED
@@ -9,7 +9,7 @@
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
12
-
13
  <!-- Load API Client and Core System -->
14
  <script src="js/api-client.js"></script>
15
  <script src="js/core.js"></script>
@@ -18,7 +18,6 @@
18
  <script src="js/document-crud.js"></script>
19
  <script src="js/scraping-control.js"></script>
20
  <script src="js/notifications.js"></script>
21
-
22
  <style>
23
  :root {
24
  /* رنگ‌بندی مدرن و هارمونیک */
@@ -81,8 +80,8 @@
81
 
82
  /* اسکرول‌بار مدرن */
83
  ::-webkit-scrollbar {
84
- width: 6px;
85
- height: 6px;
86
  }
87
 
88
  ::-webkit-scrollbar-track {
@@ -98,13 +97,13 @@
98
  /* کانتینر اصلی */
99
  .dashboard-container {
100
  display: flex;
101
- min-height: 100vh;
102
- width: 100%;
103
  }
104
 
105
  /* سایدبار کامپکت */
106
  .sidebar {
107
- width: var(--sidebar-width);
108
  background: linear-gradient(135deg,
109
  rgba(248, 250, 252, 0.98) 0%,
110
  rgba(241, 245, 249, 0.95) 25%,
@@ -114,20 +113,20 @@
114
  backdrop-filter: blur(25px);
115
  padding: 1rem 0;
116
  position: fixed;
117
- height: 100vh;
118
- right: 0;
119
- top: 0;
120
  z-index: 1000;
121
  overflow-y: auto;
122
  box-shadow: -8px 0 32px rgba(59, 130, 246, 0.12);
123
- border-left: 1px solid rgba(59, 130, 246, 0.15);
124
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
125
  }
126
 
127
  .sidebar-header {
128
  padding: 0 1rem 1rem;
129
- border-bottom: 1px solid rgba(59, 130, 246, 0.12);
130
- margin-bottom: 1rem;
131
  display: flex;
132
  justify-content: space-between;
133
  align-items: center;
@@ -148,8 +147,8 @@
148
  }
149
 
150
  .logo-icon {
151
- width: 2rem;
152
- height: 2rem;
153
  background: var(--primary-gradient);
154
  border-radius: var(--border-radius-sm);
155
  display: flex;
@@ -165,11 +164,12 @@
165
  font-weight: 700;
166
  background: var(--primary-gradient);
167
  -webkit-background-clip: text;
 
168
  -webkit-text-fill-color: transparent;
169
  }
170
 
171
  .nav-section {
172
- margin-bottom: 1rem;
173
  }
174
 
175
  .nav-title {
@@ -217,8 +217,8 @@
217
  }
218
 
219
  .nav-icon {
220
- margin-left: 0.6rem;
221
- width: 1rem;
222
  text-align: center;
223
  font-size: 0.9rem;
224
  }
@@ -230,18 +230,18 @@
230
  border-radius: 10px;
231
  font-size: var(--font-size-xs);
232
  font-weight: 600;
233
- margin-right: auto;
234
- min-width: 1.2rem;
235
  text-align: center;
236
  }
237
 
238
  /* محتوای اصلی */
239
  .main-content {
240
  flex: 1;
241
- margin-right: var(--sidebar-width);
242
  padding: 1rem;
243
- min-height: 100vh;
244
- width: calc(100% - var(--sidebar-width));
245
  }
246
 
247
  /* هدر کامپکت */
@@ -249,7 +249,7 @@
249
  display: flex;
250
  justify-content: space-between;
251
  align-items: center;
252
- margin-bottom: 1.2rem;
253
  padding: 0.8rem 0;
254
  }
255
 
@@ -258,6 +258,7 @@
258
  font-weight: 800;
259
  background: var(--primary-gradient);
260
  -webkit-background-clip: text;
 
261
  -webkit-text-fill-color: transparent;
262
  display: flex;
263
  align-items: center;
@@ -275,7 +276,7 @@
275
  }
276
 
277
  .search-input {
278
- width: 280px;
279
  padding: 0.6rem 1rem 0.6rem 2.2rem;
280
  border: none;
281
  border-radius: 20px;
@@ -298,8 +299,8 @@
298
 
299
  .search-icon {
300
  position: absolute;
301
- right: 0.8rem;
302
- top: 50%;
303
  transform: translateY(-50%);
304
  color: var(--text-secondary);
305
  font-size: 0.9rem;
@@ -325,8 +326,8 @@
325
  }
326
 
327
  .user-avatar {
328
- width: 1.8rem;
329
- height: 1.8rem;
330
  border-radius: 50%;
331
  background: var(--primary-gradient);
332
  display: flex;
@@ -358,7 +359,7 @@
358
  display: grid;
359
  grid-template-columns: repeat(4, 1fr);
360
  gap: 1rem;
361
- margin-bottom: 1.2rem;
362
  }
363
 
364
  .stat-card {
@@ -371,16 +372,16 @@
371
  position: relative;
372
  overflow: hidden;
373
  transition: var(--transition-smooth);
374
- min-height: 130px;
375
  }
376
 
377
  .stat-card::before {
378
  content: '';
379
  position: absolute;
380
- top: 0;
381
- left: 0;
382
- right: 0;
383
- height: 4px;
384
  background: var(--primary-gradient);
385
  }
386
 
@@ -398,12 +399,12 @@
398
  display: flex;
399
  justify-content: space-between;
400
  align-items: flex-start;
401
- margin-bottom: 0.8rem;
402
  }
403
 
404
  .stat-icon {
405
- width: 2.2rem;
406
- height: 2.2rem;
407
  border-radius: var(--border-radius-sm);
408
  display: flex;
409
  align-items: center;
@@ -426,7 +427,7 @@
426
  font-size: var(--font-size-xs);
427
  color: var(--text-secondary);
428
  font-weight: 600;
429
- margin-bottom: 0.3rem;
430
  }
431
 
432
  .stat-value {
@@ -434,13 +435,13 @@
434
  font-weight: 800;
435
  color: var(--text-primary);
436
  line-height: 1;
437
- margin-bottom: 0.3rem;
438
  }
439
 
440
  .stat-extra {
441
  font-size: var(--font-size-xs);
442
  color: var(--text-muted);
443
- margin-bottom: 0.3rem;
444
  }
445
 
446
  .stat-change {
@@ -459,7 +460,7 @@
459
  display: grid;
460
  grid-template-columns: 2fr 1fr;
461
  gap: 1.5rem;
462
- margin-bottom: 1.5rem;
463
  }
464
 
465
  .chart-card {
@@ -481,7 +482,7 @@
481
  display: flex;
482
  justify-content: space-between;
483
  align-items: center;
484
- margin-bottom: 1.2rem;
485
  }
486
 
487
  .chart-title {
@@ -519,12 +520,12 @@
519
  }
520
 
521
  .chart-container {
522
- height: 280px;
523
  position: relative;
524
  }
525
 
526
  .chart-placeholder {
527
- height: 100%;
528
  display: flex;
529
  align-items: center;
530
  justify-content: center;
@@ -537,13 +538,13 @@
537
 
538
  .chart-placeholder i {
539
  font-size: 3rem;
540
- margin-bottom: 1rem;
541
  opacity: 0.3;
542
  }
543
 
544
  /* دسترسی سریع */
545
  .quick-access-section {
546
- margin-bottom: 1.5rem;
547
  }
548
 
549
  .quick-access-grid {
@@ -571,10 +572,10 @@
571
  .quick-access-item::before {
572
  content: '';
573
  position: absolute;
574
- top: 0;
575
- left: 0;
576
- bottom: 0;
577
- width: 4px;
578
  background: var(--primary-gradient);
579
  opacity: 0;
580
  transition: var(--transition-smooth);
@@ -592,8 +593,8 @@
592
  }
593
 
594
  .quick-access-icon {
595
- width: 3rem;
596
- height: 3rem;
597
  background: var(--primary-gradient);
598
  border-radius: var(--border-radius-sm);
599
  display: flex;
@@ -615,7 +616,7 @@
615
  font-size: var(--font-size-base);
616
  font-weight: 600;
617
  color: var(--text-primary);
618
- margin-bottom: 0.3rem;
619
  }
620
 
621
  .quick-access-content p {
@@ -627,8 +628,8 @@
627
  /* Toast Notifications */
628
  .toast-container {
629
  position: fixed;
630
- top: 1rem;
631
- left: 1rem;
632
  z-index: 10001;
633
  display: flex;
634
  flex-direction: column;
@@ -640,11 +641,11 @@
640
  border-radius: var(--border-radius-sm);
641
  padding: 1rem 1.5rem;
642
  box-shadow: var(--shadow-lg);
643
- border-left: 4px solid;
644
  display: flex;
645
  align-items: center;
646
  gap: 0.8rem;
647
- min-width: 300px;
648
  transform: translateX(-100%);
649
  transition: all 0.3s ease;
650
  }
@@ -653,10 +654,10 @@
653
  transform: translateX(0);
654
  }
655
 
656
- .toast.success { border-left-color: #10b981; }
657
- .toast.error { border-left-color: #ef4444; }
658
- .toast.warning { border-left-color: #f59e0b; }
659
- .toast.info { border-left-color: #3b82f6; }
660
 
661
  .toast-icon {
662
  font-size: 1.2rem;
@@ -674,7 +675,7 @@
674
  .toast-title {
675
  font-weight: 600;
676
  font-size: var(--font-size-sm);
677
- margin-bottom: 0.2rem;
678
  }
679
 
680
  .toast-message {
@@ -698,8 +699,8 @@
698
  /* Connection Status */
699
  .connection-status {
700
  position: fixed;
701
- bottom: 1rem;
702
- left: 1rem;
703
  background: var(--card-bg);
704
  border-radius: var(--border-radius-sm);
705
  padding: 0.5rem 1rem;
@@ -708,23 +709,23 @@
708
  align-items: center;
709
  gap: 0.5rem;
710
  font-size: var(--font-size-xs);
711
- border-left: 3px solid;
712
  z-index: 1000;
713
  }
714
 
715
  .connection-status.online {
716
- border-left-color: #10b981;
717
  color: #047857;
718
  }
719
 
720
  .connection-status.offline {
721
- border-left-color: #ef4444;
722
  color: #b91c1c;
723
  }
724
 
725
  .status-indicator {
726
- width: 8px;
727
- height: 8px;
728
  border-radius: 50%;
729
  }
730
 
@@ -761,7 +762,7 @@
761
  }
762
 
763
  /* واکنش‌گرایی */
764
- @media (max-width: 992px) {
765
  .mobile-menu-toggle {
766
  display: block;
767
  }
@@ -777,8 +778,8 @@
777
  }
778
 
779
  .main-content {
780
- margin-right: 0;
781
- width: 100%;
782
  padding: 1rem;
783
  }
784
 
@@ -796,11 +797,11 @@
796
  }
797
 
798
  .search-container {
799
- width: 100%;
800
  }
801
 
802
  .search-input {
803
- width: 100%;
804
  }
805
 
806
  .stats-grid {
@@ -812,7 +813,7 @@
812
  }
813
  }
814
 
815
- @media (max-width: 768px) {
816
  .main-content {
817
  padding: 0.8rem;
818
  }
@@ -823,7 +824,7 @@
823
  }
824
 
825
  .stat-card {
826
- min-height: 100px;
827
  padding: 0.8rem;
828
  }
829
 
@@ -832,7 +833,7 @@
832
  }
833
 
834
  .chart-container {
835
- height: 220px;
836
  }
837
  }
838
  </style>
 
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js"></script>
12
+
13
  <!-- Load API Client and Core System -->
14
  <script src="js/api-client.js"></script>
15
  <script src="js/core.js"></script>
 
18
  <script src="js/document-crud.js"></script>
19
  <script src="js/scraping-control.js"></script>
20
  <script src="js/notifications.js"></script>
 
21
  <style>
22
  :root {
23
  /* رنگ‌بندی مدرن و هارمونیک */
 
80
 
81
  /* اسکرول‌بار مدرن */
82
  ::-webkit-scrollbar {
83
+ inline-size: 6px;
84
+ block-size: 6px;
85
  }
86
 
87
  ::-webkit-scrollbar-track {
 
97
  /* کانتینر اصلی */
98
  .dashboard-container {
99
  display: flex;
100
+ min-block-size: 100vh;
101
+ inline-size: 100%;
102
  }
103
 
104
  /* سایدبار کامپکت */
105
  .sidebar {
106
+ inline-size: var(--sidebar-width);
107
  background: linear-gradient(135deg,
108
  rgba(248, 250, 252, 0.98) 0%,
109
  rgba(241, 245, 249, 0.95) 25%,
 
113
  backdrop-filter: blur(25px);
114
  padding: 1rem 0;
115
  position: fixed;
116
+ block-size: 100vh;
117
+ inset-inline-end: 0;
118
+ inset-block-start: 0;
119
  z-index: 1000;
120
  overflow-y: auto;
121
  box-shadow: -8px 0 32px rgba(59, 130, 246, 0.12);
122
+ border-inline-start: 1px solid rgba(59, 130, 246, 0.15);
123
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
124
  }
125
 
126
  .sidebar-header {
127
  padding: 0 1rem 1rem;
128
+ border-block-end: 1px solid rgba(59, 130, 246, 0.12);
129
+ margin-block-end: 1rem;
130
  display: flex;
131
  justify-content: space-between;
132
  align-items: center;
 
147
  }
148
 
149
  .logo-icon {
150
+ inline-size: 2rem;
151
+ block-size: 2rem;
152
  background: var(--primary-gradient);
153
  border-radius: var(--border-radius-sm);
154
  display: flex;
 
164
  font-weight: 700;
165
  background: var(--primary-gradient);
166
  -webkit-background-clip: text;
167
+ background-clip: text;
168
  -webkit-text-fill-color: transparent;
169
  }
170
 
171
  .nav-section {
172
+ margin-block-end: 1rem;
173
  }
174
 
175
  .nav-title {
 
217
  }
218
 
219
  .nav-icon {
220
+ margin-inline-start: 0.6rem;
221
+ inline-size: 1rem;
222
  text-align: center;
223
  font-size: 0.9rem;
224
  }
 
230
  border-radius: 10px;
231
  font-size: var(--font-size-xs);
232
  font-weight: 600;
233
+ margin-inline-end: auto;
234
+ min-inline-size: 1.2rem;
235
  text-align: center;
236
  }
237
 
238
  /* محتوای اصلی */
239
  .main-content {
240
  flex: 1;
241
+ margin-inline-end: var(--sidebar-width);
242
  padding: 1rem;
243
+ min-block-size: 100vh;
244
+ inline-size: calc(100% - var(--sidebar-width));
245
  }
246
 
247
  /* هدر کامپکت */
 
249
  display: flex;
250
  justify-content: space-between;
251
  align-items: center;
252
+ margin-block-end: 1.2rem;
253
  padding: 0.8rem 0;
254
  }
255
 
 
258
  font-weight: 800;
259
  background: var(--primary-gradient);
260
  -webkit-background-clip: text;
261
+ background-clip: text;
262
  -webkit-text-fill-color: transparent;
263
  display: flex;
264
  align-items: center;
 
276
  }
277
 
278
  .search-input {
279
+ inline-size: 280px;
280
  padding: 0.6rem 1rem 0.6rem 2.2rem;
281
  border: none;
282
  border-radius: 20px;
 
299
 
300
  .search-icon {
301
  position: absolute;
302
+ inset-inline-end: 0.8rem;
303
+ inset-block-start: 50%;
304
  transform: translateY(-50%);
305
  color: var(--text-secondary);
306
  font-size: 0.9rem;
 
326
  }
327
 
328
  .user-avatar {
329
+ inline-size: 1.8rem;
330
+ block-size: 1.8rem;
331
  border-radius: 50%;
332
  background: var(--primary-gradient);
333
  display: flex;
 
359
  display: grid;
360
  grid-template-columns: repeat(4, 1fr);
361
  gap: 1rem;
362
+ margin-block-end: 1.2rem;
363
  }
364
 
365
  .stat-card {
 
372
  position: relative;
373
  overflow: hidden;
374
  transition: var(--transition-smooth);
375
+ min-block-size: 130px;
376
  }
377
 
378
  .stat-card::before {
379
  content: '';
380
  position: absolute;
381
+ inset-block-start: 0;
382
+ inset-inline-start: 0;
383
+ inset-inline-end: 0;
384
+ block-size: 4px;
385
  background: var(--primary-gradient);
386
  }
387
 
 
399
  display: flex;
400
  justify-content: space-between;
401
  align-items: flex-start;
402
+ margin-block-end: 0.8rem;
403
  }
404
 
405
  .stat-icon {
406
+ inline-size: 2.2rem;
407
+ block-size: 2.2rem;
408
  border-radius: var(--border-radius-sm);
409
  display: flex;
410
  align-items: center;
 
427
  font-size: var(--font-size-xs);
428
  color: var(--text-secondary);
429
  font-weight: 600;
430
+ margin-block-end: 0.3rem;
431
  }
432
 
433
  .stat-value {
 
435
  font-weight: 800;
436
  color: var(--text-primary);
437
  line-height: 1;
438
+ margin-block-end: 0.3rem;
439
  }
440
 
441
  .stat-extra {
442
  font-size: var(--font-size-xs);
443
  color: var(--text-muted);
444
+ margin-block-end: 0.3rem;
445
  }
446
 
447
  .stat-change {
 
460
  display: grid;
461
  grid-template-columns: 2fr 1fr;
462
  gap: 1.5rem;
463
+ margin-block-end: 1.5rem;
464
  }
465
 
466
  .chart-card {
 
482
  display: flex;
483
  justify-content: space-between;
484
  align-items: center;
485
+ margin-block-end: 1.2rem;
486
  }
487
 
488
  .chart-title {
 
520
  }
521
 
522
  .chart-container {
523
+ block-size: 280px;
524
  position: relative;
525
  }
526
 
527
  .chart-placeholder {
528
+ block-size: 100%;
529
  display: flex;
530
  align-items: center;
531
  justify-content: center;
 
538
 
539
  .chart-placeholder i {
540
  font-size: 3rem;
541
+ margin-block-end: 1rem;
542
  opacity: 0.3;
543
  }
544
 
545
  /* دسترسی سریع */
546
  .quick-access-section {
547
+ margin-block-end: 1.5rem;
548
  }
549
 
550
  .quick-access-grid {
 
572
  .quick-access-item::before {
573
  content: '';
574
  position: absolute;
575
+ inset-block-start: 0;
576
+ inset-inline-start: 0;
577
+ inset-block-end: 0;
578
+ inline-size: 4px;
579
  background: var(--primary-gradient);
580
  opacity: 0;
581
  transition: var(--transition-smooth);
 
593
  }
594
 
595
  .quick-access-icon {
596
+ inline-size: 3rem;
597
+ block-size: 3rem;
598
  background: var(--primary-gradient);
599
  border-radius: var(--border-radius-sm);
600
  display: flex;
 
616
  font-size: var(--font-size-base);
617
  font-weight: 600;
618
  color: var(--text-primary);
619
+ margin-block-end: 0.3rem;
620
  }
621
 
622
  .quick-access-content p {
 
628
  /* Toast Notifications */
629
  .toast-container {
630
  position: fixed;
631
+ inset-block-start: 1rem;
632
+ inset-inline-start: 1rem;
633
  z-index: 10001;
634
  display: flex;
635
  flex-direction: column;
 
641
  border-radius: var(--border-radius-sm);
642
  padding: 1rem 1.5rem;
643
  box-shadow: var(--shadow-lg);
644
+ border-inline-start: 4px solid;
645
  display: flex;
646
  align-items: center;
647
  gap: 0.8rem;
648
+ min-inline-size: 300px;
649
  transform: translateX(-100%);
650
  transition: all 0.3s ease;
651
  }
 
654
  transform: translateX(0);
655
  }
656
 
657
+ .toast.success { border-inline-start-color: #10b981; }
658
+ .toast.error { border-inline-start-color: #ef4444; }
659
+ .toast.warning { border-inline-start-color: #f59e0b; }
660
+ .toast.info { border-inline-start-color: #3b82f6; }
661
 
662
  .toast-icon {
663
  font-size: 1.2rem;
 
675
  .toast-title {
676
  font-weight: 600;
677
  font-size: var(--font-size-sm);
678
+ margin-block-end: 0.2rem;
679
  }
680
 
681
  .toast-message {
 
699
  /* Connection Status */
700
  .connection-status {
701
  position: fixed;
702
+ inset-block-end: 1rem;
703
+ inset-inline-start: 1rem;
704
  background: var(--card-bg);
705
  border-radius: var(--border-radius-sm);
706
  padding: 0.5rem 1rem;
 
709
  align-items: center;
710
  gap: 0.5rem;
711
  font-size: var(--font-size-xs);
712
+ border-inline-start: 3px solid;
713
  z-index: 1000;
714
  }
715
 
716
  .connection-status.online {
717
+ border-inline-start-color: #10b981;
718
  color: #047857;
719
  }
720
 
721
  .connection-status.offline {
722
+ border-inline-start-color: #ef4444;
723
  color: #b91c1c;
724
  }
725
 
726
  .status-indicator {
727
+ inline-size: 8px;
728
+ block-size: 8px;
729
  border-radius: 50%;
730
  }
731
 
 
762
  }
763
 
764
  /* واکنش‌گرایی */
765
+ @media (max-inline-size: 992px) {
766
  .mobile-menu-toggle {
767
  display: block;
768
  }
 
778
  }
779
 
780
  .main-content {
781
+ margin-inline-end: 0;
782
+ inline-size: 100%;
783
  padding: 1rem;
784
  }
785
 
 
797
  }
798
 
799
  .search-container {
800
+ inline-size: 100%;
801
  }
802
 
803
  .search-input {
804
+ inline-size: 100%;
805
  }
806
 
807
  .stats-grid {
 
813
  }
814
  }
815
 
816
+ @media (max-inline-size: 768px) {
817
  .main-content {
818
  padding: 0.8rem;
819
  }
 
824
  }
825
 
826
  .stat-card {
827
+ min-block-size: 100px;
828
  padding: 0.8rem;
829
  }
830
 
 
833
  }
834
 
835
  .chart-container {
836
+ block-size: 220px;
837
  }
838
  }
839
  </style>
frontend/reports.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Legal Dashboard - Reports & Analytics</title>
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@@ -276,7 +276,7 @@
276
  <i class="fas fa-chart-line me-2"></i>
277
  Legal Dashboard - Reports
278
  </a>
279
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
280
  <span class="navbar-toggler-icon"></span>
281
  </button>
282
  <div class="collapse navbar-collapse" id="navbarNav">
@@ -320,20 +320,20 @@
320
  <input type="date" id="startDate" class="date-input">
321
  <span>to</span>
322
  <input type="date" id="endDate" class="date-input">
323
- <button class="refresh-btn" onclick="loadReports()">
324
  <i class="fas fa-sync-alt me-1"></i>Refresh
325
  </button>
326
  </div>
327
  </div>
328
  <div class="col-md-6">
329
  <div class="export-section">
330
- <button class="btn btn-export" onclick="exportReport('summary')">
331
  <i class="fas fa-download me-1"></i>Export Summary
332
  </button>
333
- <button class="btn btn-export" onclick="exportReport('user_activity')">
334
  <i class="fas fa-users me-1"></i>Export User Activity
335
  </button>
336
- <button class="btn btn-export" onclick="exportReport('document_analytics')">
337
  <i class="fas fa-file-alt me-1"></i>Export Document Analytics
338
  </button>
339
  </div>
@@ -474,11 +474,11 @@
474
  <table class="table table-hover" id="userActivityTable">
475
  <thead>
476
  <tr>
477
- <th>User</th>
478
- <th>Documents Processed</th>
479
- <th>Last Activity</th>
480
- <th>Success Rate</th>
481
- <th>Avg Processing Time</th>
482
  </tr>
483
  </thead>
484
  <tbody id="userActivityBody">
@@ -507,12 +507,12 @@
507
  <table class="table table-hover" id="documentAnalyticsTable">
508
  <thead>
509
  <tr>
510
- <th>Document</th>
511
- <th>Processing Time</th>
512
- <th>OCR Accuracy</th>
513
- <th>File Size</th>
514
- <th>Status</th>
515
- <th>Created</th>
516
  </tr>
517
  </thead>
518
  <tbody id="documentAnalyticsBody">
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Legal Dashboard - Reports &amp; Analytics</title>
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 
276
  <i class="fas fa-chart-line me-2"></i>
277
  Legal Dashboard - Reports
278
  </a>
279
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-label="Toggle navigation">
280
  <span class="navbar-toggler-icon"></span>
281
  </button>
282
  <div class="collapse navbar-collapse" id="navbarNav">
 
320
  <input type="date" id="startDate" class="date-input">
321
  <span>to</span>
322
  <input type="date" id="endDate" class="date-input">
323
+ <button type="button" class="refresh-btn" onclick="loadReports()">
324
  <i class="fas fa-sync-alt me-1"></i>Refresh
325
  </button>
326
  </div>
327
  </div>
328
  <div class="col-md-6">
329
  <div class="export-section">
330
+ <button type="button" class="btn btn-export" onclick="exportReport('summary')">
331
  <i class="fas fa-download me-1"></i>Export Summary
332
  </button>
333
+ <button type="button" class="btn btn-export" onclick="exportReport('user_activity')">
334
  <i class="fas fa-users me-1"></i>Export User Activity
335
  </button>
336
+ <button type="button" class="btn btn-export" onclick="exportReport('document_analytics')">
337
  <i class="fas fa-file-alt me-1"></i>Export Document Analytics
338
  </button>
339
  </div>
 
474
  <table class="table table-hover" id="userActivityTable">
475
  <thead>
476
  <tr>
477
+ <th scope="col">User</th>
478
+ <th scope="col">Documents Processed</th>
479
+ <th scope="col">Last Activity</th>
480
+ <th scope="col">Success Rate</th>
481
+ <th scope="col">Avg Processing Time</th>
482
  </tr>
483
  </thead>
484
  <tbody id="userActivityBody">
 
507
  <table class="table table-hover" id="documentAnalyticsTable">
508
  <thead>
509
  <tr>
510
+ <th scope="col">Document</th>
511
+ <th scope="col">Processing Time</th>
512
+ <th scope="col">OCR Accuracy</th>
513
+ <th scope="col">File Size</th>
514
+ <th scope="col">Status</th>
515
+ <th scope="col">Created</th>
516
  </tr>
517
  </thead>
518
  <tbody id="documentAnalyticsBody">
frontend/scraping.html CHANGED
@@ -8,13 +8,13 @@
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
-
12
  <!-- Load API Client and Core System -->
13
  <script src="js/api-client.js"></script>
14
  <script src="js/core.js"></script>
15
  <script src="js/notifications.js"></script>
16
  <script src="js/scraping-control.js"></script>
17
-
18
  <style>
19
  :root {
20
  /* رنگ‌بندی مدرن و هارمونیک - منطبق با index */
@@ -81,8 +81,8 @@
81
 
82
  /* اسکرول‌بار مدرن */
83
  ::-webkit-scrollbar {
84
- width: 6px;
85
- height: 6px;
86
  }
87
 
88
  ::-webkit-scrollbar-track {
@@ -356,6 +356,10 @@
356
  margin-bottom: 0.3rem;
357
  }
358
 
 
 
 
 
359
  .stat-extra {
360
  font-size: var(--font-size-xs);
361
  color: var(--text-muted);
@@ -1199,7 +1203,7 @@
1199
  <div class="stat-header">
1200
  <div class="stat-content">
1201
  <div class="stat-title">آخرین بروزرسانی</div>
1202
- <div class="stat-value" id="lastUpdate" style="font-size: var(--font-size-base);">--</div>
1203
  <div class="stat-extra">زمان آخرین فعالیت</div>
1204
  <div class="stat-change positive">
1205
  <i class="fas fa-clock"></i>
@@ -1237,36 +1241,36 @@
1237
  کنترل عملیات
1238
  </h2>
1239
  </div>
1240
-
1241
  <div class="action-buttons">
1242
- <button class="btn btn-auto" onclick="startAutoScraping()" id="autoScrapingBtn">
1243
  <i class="fas fa-magic"></i>
1244
  اسکراپینگ خودکار
1245
  </button>
1246
- <button class="btn btn-primary" onclick="startSelectedScraping()" id="selectedScrapingBtn">
1247
  <i class="fas fa-play"></i>
1248
  اسکراپینگ انتخابی
1249
  </button>
1250
- <button class="btn btn-warning" onclick="stopScraping()" id="stopScrapingBtn" disabled>
1251
  <i class="fas fa-stop"></i>
1252
  توقف
1253
  </button>
1254
- <button class="btn btn-secondary" onclick="refreshData()">
1255
  <i class="fas fa-sync-alt"></i>
1256
  بروزرسانی
1257
  </button>
1258
  </div>
1259
 
1260
  <div class="action-buttons">
1261
- <button class="btn btn-secondary" onclick="selectAllSources()">
1262
  <i class="fas fa-check-double"></i>
1263
  انتخاب همه
1264
  </button>
1265
- <button class="btn btn-secondary" onclick="clearAllSources()">
1266
  <i class="fas fa-times"></i>
1267
  پاک کردن انتخاب
1268
  </button>
1269
- <button class="btn btn-secondary" onclick="checkSourcesStatus()">
1270
  <i class="fas fa-wifi"></i>
1271
  بررسی وضعیت
1272
  </button>
@@ -1324,11 +1328,11 @@
1324
  نتایج اسکراپینگ
1325
  </h3>
1326
  <div class="results-actions">
1327
- <button class="btn btn-success btn-sm" onclick="exportResults()">
1328
  <i class="fas fa-download"></i>
1329
  دانلود JSON
1330
  </button>
1331
- <button class="btn btn-primary btn-sm" onclick="saveToDatabase()">
1332
  <i class="fas fa-save"></i>
1333
  ذخیره
1334
  </button>
 
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+
12
  <!-- Load API Client and Core System -->
13
  <script src="js/api-client.js"></script>
14
  <script src="js/core.js"></script>
15
  <script src="js/notifications.js"></script>
16
  <script src="js/scraping-control.js"></script>
17
+
18
  <style>
19
  :root {
20
  /* رنگ‌بندی مدرن و هارمونیک - منطبق با index */
 
81
 
82
  /* اسکرول‌بار مدرن */
83
  ::-webkit-scrollbar {
84
+ inline-size: 6px;
85
+ block-size: 6px;
86
  }
87
 
88
  ::-webkit-scrollbar-track {
 
356
  margin-bottom: 0.3rem;
357
  }
358
 
359
+ .stat-value.small {
360
+ font-size: var(--font-size-base);
361
+ }
362
+
363
  .stat-extra {
364
  font-size: var(--font-size-xs);
365
  color: var(--text-muted);
 
1203
  <div class="stat-header">
1204
  <div class="stat-content">
1205
  <div class="stat-title">آخرین بروزرسانی</div>
1206
+ <div class="stat-value small" id="lastUpdate">--</div>
1207
  <div class="stat-extra">زمان آخرین فعالیت</div>
1208
  <div class="stat-change positive">
1209
  <i class="fas fa-clock"></i>
 
1241
  کنترل عملیات
1242
  </h2>
1243
  </div>
1244
+
1245
  <div class="action-buttons">
1246
+ <button type="button" class="btn btn-auto" onclick="startAutoScraping()" id="autoScrapingBtn">
1247
  <i class="fas fa-magic"></i>
1248
  اسکراپینگ خودکار
1249
  </button>
1250
+ <button type="button" class="btn btn-primary" onclick="startSelectedScraping()" id="selectedScrapingBtn">
1251
  <i class="fas fa-play"></i>
1252
  اسکراپینگ انتخابی
1253
  </button>
1254
+ <button type="button" class="btn btn-warning" onclick="stopScraping()" id="stopScrapingBtn" disabled>
1255
  <i class="fas fa-stop"></i>
1256
  توقف
1257
  </button>
1258
+ <button type="button" class="btn btn-secondary" onclick="refreshData()">
1259
  <i class="fas fa-sync-alt"></i>
1260
  بروزرسانی
1261
  </button>
1262
  </div>
1263
 
1264
  <div class="action-buttons">
1265
+ <button type="button" class="btn btn-secondary" onclick="selectAllSources()">
1266
  <i class="fas fa-check-double"></i>
1267
  انتخاب همه
1268
  </button>
1269
+ <button type="button" class="btn btn-secondary" onclick="clearAllSources()">
1270
  <i class="fas fa-times"></i>
1271
  پاک کردن انتخاب
1272
  </button>
1273
+ <button type="button" class="btn btn-secondary" onclick="checkSourcesStatus()">
1274
  <i class="fas fa-wifi"></i>
1275
  بررسی وضعیت
1276
  </button>
 
1328
  نتایج اسکراپینگ
1329
  </h3>
1330
  <div class="results-actions">
1331
+ <button type="button" class="btn btn-success btn-sm" onclick="exportResults()">
1332
  <i class="fas fa-download"></i>
1333
  دانلود JSON
1334
  </button>
1335
+ <button type="button" class="btn btn-primary btn-sm" onclick="saveToDatabase()">
1336
  <i class="fas fa-save"></i>
1337
  ذخیره
1338
  </button>
frontend/upload.html CHANGED
@@ -8,7 +8,7 @@
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
-
12
  <!-- Load API Client -->
13
  <script src="/static/js/api-client.js"></script>
14
  <script src="/static/js/core.js"></script>
@@ -64,13 +64,13 @@
64
 
65
  .dashboard-container {
66
  display: flex;
67
- min-height: 100vh;
68
- width: 100%;
69
  }
70
 
71
  /* سایدبار کامپکت */
72
  .sidebar {
73
- width: var(--sidebar-width);
74
  background: linear-gradient(135deg,
75
  rgba(248, 250, 252, 0.98) 0%,
76
  rgba(241, 245, 249, 0.95) 25%,
@@ -80,19 +80,19 @@
80
  backdrop-filter: blur(25px);
81
  padding: 1rem 0;
82
  position: fixed;
83
- height: 100vh;
84
- right: 0;
85
- top: 0;
86
  z-index: 1000;
87
  overflow-y: auto;
88
  box-shadow: -8px 0 32px rgba(59, 130, 246, 0.12);
89
- border-left: 1px solid rgba(59, 130, 246, 0.15);
90
  }
91
 
92
  .sidebar-header {
93
  padding: 0 1rem 1rem;
94
- border-bottom: 1px solid rgba(59, 130, 246, 0.12);
95
- margin-bottom: 1rem;
96
  display: flex;
97
  align-items: center;
98
  background: linear-gradient(135deg,
@@ -112,8 +112,8 @@
112
  }
113
 
114
  .logo-icon {
115
- width: 2rem;
116
- height: 2rem;
117
  background: var(--primary-gradient);
118
  border-radius: var(--border-radius-sm);
119
  display: flex;
@@ -132,7 +132,7 @@
132
  }
133
 
134
  .nav-section {
135
- margin-bottom: 1rem;
136
  }
137
 
138
  .nav-title {
@@ -179,8 +179,8 @@
179
  }
180
 
181
  .nav-icon {
182
- margin-left: 0.6rem;
183
- width: 1rem;
184
  text-align: center;
185
  font-size: 0.9rem;
186
  }
@@ -192,27 +192,27 @@
192
  border-radius: 10px;
193
  font-size: var(--font-size-xs);
194
  font-weight: 600;
195
- margin-right: auto;
196
- min-width: 1.2rem;
197
  text-align: center;
198
  }
199
 
200
  /* محتوای اصلی */
201
  .main-content {
202
  flex: 1;
203
- margin-right: var(--sidebar-width);
204
  padding: 1rem;
205
- min-height: 100vh;
206
- width: calc(100% - var(--sidebar-width));
207
  }
208
 
209
  .page-header {
210
  display: flex;
211
  justify-content: space-between;
212
  align-items: center;
213
- margin-bottom: 2rem;
214
  padding: 1rem 0;
215
- border-bottom: 1px solid rgba(0, 0, 0, 0.1);
216
  }
217
 
218
  .page-title {
@@ -263,7 +263,7 @@
263
  backdrop-filter: blur(10px);
264
  border-radius: var(--border-radius);
265
  padding: 2rem;
266
- margin-bottom: 2rem;
267
  box-shadow: var(--shadow-md);
268
  border: 1px solid rgba(255, 255, 255, 0.3);
269
  }
 
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+
12
  <!-- Load API Client -->
13
  <script src="/static/js/api-client.js"></script>
14
  <script src="/static/js/core.js"></script>
 
64
 
65
  .dashboard-container {
66
  display: flex;
67
+ min-block-size: 100vh;
68
+ inline-size: 100%;
69
  }
70
 
71
  /* سایدبار کامپکت */
72
  .sidebar {
73
+ inline-size: var(--sidebar-width);
74
  background: linear-gradient(135deg,
75
  rgba(248, 250, 252, 0.98) 0%,
76
  rgba(241, 245, 249, 0.95) 25%,
 
80
  backdrop-filter: blur(25px);
81
  padding: 1rem 0;
82
  position: fixed;
83
+ block-size: 100vh;
84
+ inset-inline-end: 0;
85
+ inset-block-start: 0;
86
  z-index: 1000;
87
  overflow-y: auto;
88
  box-shadow: -8px 0 32px rgba(59, 130, 246, 0.12);
89
+ border-inline-start: 1px solid rgba(59, 130, 246, 0.15);
90
  }
91
 
92
  .sidebar-header {
93
  padding: 0 1rem 1rem;
94
+ border-block-end: 1px solid rgba(59, 130, 246, 0.12);
95
+ margin-block-end: 1rem;
96
  display: flex;
97
  align-items: center;
98
  background: linear-gradient(135deg,
 
112
  }
113
 
114
  .logo-icon {
115
+ inline-size: 2rem;
116
+ block-size: 2rem;
117
  background: var(--primary-gradient);
118
  border-radius: var(--border-radius-sm);
119
  display: flex;
 
132
  }
133
 
134
  .nav-section {
135
+ margin-block-end: 1rem;
136
  }
137
 
138
  .nav-title {
 
179
  }
180
 
181
  .nav-icon {
182
+ margin-inline-start: 0.6rem;
183
+ inline-size: 1rem;
184
  text-align: center;
185
  font-size: 0.9rem;
186
  }
 
192
  border-radius: 10px;
193
  font-size: var(--font-size-xs);
194
  font-weight: 600;
195
+ margin-inline-end: auto;
196
+ min-inline-size: 1.2rem;
197
  text-align: center;
198
  }
199
 
200
  /* محتوای اصلی */
201
  .main-content {
202
  flex: 1;
203
+ margin-inline-end: var(--sidebar-width);
204
  padding: 1rem;
205
+ min-block-size: 100vh;
206
+ inline-size: calc(100% - var(--sidebar-width));
207
  }
208
 
209
  .page-header {
210
  display: flex;
211
  justify-content: space-between;
212
  align-items: center;
213
+ margin-block-end: 2rem;
214
  padding: 1rem 0;
215
+ border-block-end: 1px solid rgba(0, 0, 0, 0.1);
216
  }
217
 
218
  .page-title {
 
263
  backdrop-filter: blur(10px);
264
  border-radius: var(--border-radius);
265
  padding: 2rem;
266
+ margin-block-end: 2rem;
267
  box-shadow: var(--shadow-md);
268
  border: 1px solid rgba(255, 255, 255, 0.3);
269
  }
requirements.txt CHANGED
@@ -1,64 +1,65 @@
1
- # FastAPI and Web Framework
2
- fastapi==0.104.1
3
- uvicorn[standard]==0.24.0
4
- python-multipart==0.0.6
5
- aiofiles==23.2.1
6
-
7
- # AI and Machine Learning
8
- transformers==4.35.2
9
- torch==2.1.1
10
- torchvision==0.16.1
11
- numpy==1.24.3
12
- scikit-learn==1.3.2
13
- pandas==2.1.4
14
-
15
- # PDF Processing
16
- PyMuPDF==1.23.8
17
- pdf2image==1.16.3
18
- Pillow==10.1.0
19
-
20
- # OCR and Image Processing
21
- opencv-python==4.8.1.78
22
- pytesseract==0.3.10
23
-
24
- # Database and Data Handling
25
- pydantic==2.5.0
26
- dataclasses-json==0.6.3
27
-
28
- # HTTP and Networking
29
- requests==2.31.0
30
- aiohttp==3.9.1
31
- httpx==0.25.2
32
-
33
- # Web Scraping and Data Processing
34
- beautifulsoup4==4.12.2
35
- lxml==4.9.3
36
- html5lib==1.1
37
-
38
- # Utilities
39
- python-dotenv==1.0.0
40
- python-jose[cryptography]==3.3.0
41
- passlib[bcrypt]==1.7.4
42
-
43
- # Development and Testing
44
- pytest==7.4.3
45
- pytest-asyncio==0.21.1
46
-
47
- # Hugging Face Integration
48
- huggingface-hub==0.19.4
49
- tokenizers==0.15.0
50
-
51
- # Tokenizer Dependencies (Fix for sentencepiece conversion errors)
52
- sentencepiece==0.1.99
53
- protobuf<5
54
-
55
- # Additional Dependencies
56
- websockets==12.0
57
-
58
- # Production Dependencies
59
- redis==5.0.1
60
- celery==5.3.4
61
- flower==2.0.1
62
- prometheus-client==0.19.0
63
- structlog==23.2.0
64
- email-validator==2.1.0
 
 
1
+ # Core FastAPI dependencies
2
+ fastapi==0.104.1
3
+ uvicorn[standard]==0.24.0
4
+ pydantic==2.5.0
5
+ pydantic[email]==2.5.0
6
+
7
+ # Authentication & Security
8
+ python-jose[cryptography]==3.3.0
9
+ passlib[bcrypt]==1.7.4
10
+ python-multipart==0.0.6
11
+
12
+ # Database
13
+ sqlite3 # Built-in with Python
14
+ sqlalchemy==2.0.23
15
+
16
+ # Gradio for HF Spaces interface
17
+ gradio==4.8.0
18
+
19
+ # HTTP requests
20
+ requests==2.31.0
21
+ httpx==0.25.2
22
+
23
+ # File processing
24
+ python-docx==1.1.0
25
+ PyPDF2==3.0.1
26
+ pdf2image==1.16.3
27
+ Pillow==10.1.0
28
+
29
+ # AI/ML dependencies (lightweight versions for HF Spaces)
30
+ transformers==4.36.0
31
+ torch==2.1.1 --index-url https://download.pytorch.org/whl/cpu
32
+ tokenizers==0.15.0
33
+ sentence-transformers==2.2.2
34
+
35
+ # Text processing
36
+ spacy==3.7.2
37
+ nltk==3.8.1
38
+
39
+ # Utilities
40
+ python-dotenv==1.0.0
41
+ pathlib2==2.3.7
42
+ typing-extensions==4.8.0
43
+
44
+ # Logging and monitoring
45
+ structlog==23.2.0
46
+
47
+ # Date and time
48
+ python-dateutil==2.8.2
49
+
50
+ # Environment and configuration
51
+ pydantic-settings==2.1.0
52
+
53
+ # Optional: Redis for caching (if using external Redis)
54
+ redis==5.0.1
55
+
56
+ # Development dependencies (optional)
57
+ pytest==7.4.3
58
+ pytest-asyncio==0.21.1
59
+
60
+ # Hugging Face specific
61
+ huggingface-hub==0.19.4
62
+
63
+ # Additional utilities
64
+ aiofiles==23.2.1
65
+ jinja2==3.1.2
start.sh CHANGED
@@ -1,12 +1,146 @@
1
  #!/bin/bash
2
 
3
- # Create writable directories for Hugging Face cache and data
4
- mkdir -p /tmp/hf_cache /tmp/data
5
 
6
- # Set environment variables
7
- export TRANSFORMERS_CACHE=/tmp/hf_cache
8
- export HF_HOME=/tmp/hf_cache
9
- export DATABASE_PATH=/tmp/data/legal_dashboard.db
10
 
11
- # Start the application
12
- exec uvicorn app.main:app --host 0.0.0.0 --port 7860
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  #!/bin/bash
2
 
3
+ # Start script for Legal Dashboard
4
+ # Suitable for Hugging Face Spaces and Docker environments
5
 
6
+ echo "🚀 Starting Legal Dashboard..."
 
 
 
7
 
8
+ # Set default environment variables
9
+ export PYTHONPATH=/app
10
+ export PYTHONUNBUFFERED=1
11
+ export DATABASE_DIR=${DATABASE_DIR:-/app/data}
12
+ export LOG_LEVEL=${LOG_LEVEL:-INFO}
13
+
14
+ # Create necessary directories
15
+ echo "📁 Creating directories..."
16
+ mkdir -p /app/data /app/cache /app/logs /app/uploads /app/backups /tmp/app_fallback
17
+
18
+ # Set permissions if possible (ignore errors)
19
+ chmod -R 755 /app/data /app/cache /app/logs /app/uploads /app/backups 2>/dev/null || true
20
+ chmod -R 777 /tmp/app_fallback 2>/dev/null || true
21
+
22
+ # Function to check if port is available
23
+ check_port() {
24
+ local port=${1:-8000}
25
+ if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
26
+ echo "⚠️ Port $port is already in use"
27
+ return 1
28
+ else
29
+ echo "✅ Port $port is available"
30
+ return 0
31
+ fi
32
+ }
33
+
34
+ # Function to wait for dependencies
35
+ wait_for_deps() {
36
+ echo "⏳ Checking dependencies..."
37
+
38
+ # Check if we can create database
39
+ python3 -c "
40
+ import sqlite3
41
+ import os
42
+ db_path = os.environ.get('DATABASE_DIR', '/app/data') + '/test.db'
43
+ try:
44
+ conn = sqlite3.connect(db_path)
45
+ conn.close()
46
+ os.remove(db_path)
47
+ print('✅ Database access OK')
48
+ except Exception as e:
49
+ print(f'⚠️ Database access issue: {e}')
50
+ " || echo "Database check completed with warnings"
51
+ }
52
+
53
+ # Check environment
54
+ echo "🔍 Environment check..."
55
+ echo " - Python: $(python3 --version)"
56
+ echo " - Working directory: $(pwd)"
57
+ echo " - Database dir: $DATABASE_DIR"
58
+ echo " - User: $(whoami)"
59
+ echo " - UID: $(id -u)"
60
+
61
+ # Wait for dependencies
62
+ wait_for_deps
63
+
64
+ # Check port availability
65
+ PORT=${PORT:-8000}
66
+ if ! check_port $PORT; then
67
+ echo "🔄 Trying alternative port..."
68
+ PORT=$((PORT + 1))
69
+ check_port $PORT || PORT=7860 # HF Spaces default
70
+ fi
71
+
72
+ echo "🌐 Using port: $PORT"
73
+
74
+ # Health check function
75
+ health_check() {
76
+ local max_attempts=30
77
+ local attempt=1
78
+
79
+ echo "🏥 Starting health check..."
80
+
81
+ while [ $attempt -le $max_attempts ]; do
82
+ if curl -fs http://localhost:$PORT/health >/dev/null 2>&1; then
83
+ echo "✅ Application is healthy!"
84
+ return 0
85
+ fi
86
+
87
+ echo "⏳ Health check attempt $attempt/$max_attempts..."
88
+ sleep 2
89
+ attempt=$((attempt + 1))
90
+ done
91
+
92
+ echo "❌ Health check failed after $max_attempts attempts"
93
+ return 1
94
+ }
95
+
96
+ # Start application
97
+ echo "🎯 Starting application on port $PORT..."
98
+
99
+ # For Hugging Face Spaces
100
+ if [ "$SPACE_ID" != "" ]; then
101
+ echo "🤗 Running in Hugging Face Spaces environment"
102
+
103
+ # Use gradio or fastapi depending on setup
104
+ if [ -f "app.py" ]; then
105
+ echo "📱 Starting with Gradio interface..."
106
+ python3 app.py
107
+ else
108
+ echo "🚀 Starting FastAPI server..."
109
+ uvicorn app.main:app --host 0.0.0.0 --port $PORT --workers 1
110
+ fi
111
+ else
112
+ # Standard Docker/local environment
113
+ echo "🐳 Running in standard environment"
114
+
115
+ # Start with uvicorn
116
+ if command -v uvicorn >/dev/null 2>&1; then
117
+ echo "🚀 Starting with uvicorn..."
118
+ uvicorn app.main:app \
119
+ --host 0.0.0.0 \
120
+ --port $PORT \
121
+ --workers 1 \
122
+ --access-log \
123
+ --log-level info &
124
+
125
+ # Store PID
126
+ APP_PID=$!
127
+ echo "📝 Application PID: $APP_PID"
128
+
129
+ # Wait a bit then check health
130
+ sleep 5
131
+ if health_check; then
132
+ echo "🎉 Application started successfully!"
133
+ wait $APP_PID
134
+ else
135
+ echo "💥 Application failed to start properly"
136
+ kill $APP_PID 2>/dev/null
137
+ exit 1
138
+ fi
139
+ else
140
+ echo "❌ uvicorn not found, trying with python..."
141
+ python3 -c "
142
+ import uvicorn
143
+ uvicorn.run('app.main:app', host='0.0.0.0', port=$PORT, workers=1)
144
+ "
145
+ fi
146
+ fi