sameerbanchhor commited on
Commit
2064c2f
Β·
verified Β·
1 Parent(s): a55cbbf

Upload folder using huggingface_hub

Browse files
.gitignore CHANGED
@@ -15,4 +15,7 @@ uploaded_files
15
 
16
  # vs code config
17
  .vscode
18
- backend.code-workspace
 
 
 
 
15
 
16
  # vs code config
17
  .vscode
18
+ backend.code-workspace
19
+
20
+ llm_backend_structure_documentation.md
21
+ raw_api_docs.md
app/main.py CHANGED
@@ -63,7 +63,204 @@ app.include_router(bgremover.router) # Background Remover
63
  @app.get("/", response_class=HTMLResponse)
64
  def greet_json():
65
  return """
66
- <h2>Go to the Swagger docs πŸ‘‡</h2>
67
- <a href="/docs">Click here for API Docs</a>
68
- <h3>created by sameer banchhor</h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  """
 
63
  @app.get("/", response_class=HTMLResponse)
64
  def greet_json():
65
  return """
66
+ <!DOCTYPE html>
67
+ <html lang="en" data-theme="light">
68
+ <head>
69
+ <meta charset="UTF-8">
70
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
71
+ <title>Easy Tools Hub | by Sam</title>
72
+ <link rel="preconnect" href="https://fonts.googleapis.com">
73
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
74
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
75
+
76
+ <style>
77
+ :root {
78
+ --primary: #6366f1;
79
+ --primary-hover: #4f46e5;
80
+ --bg-body: #f8fafc;
81
+ --bg-card: #ffffff;
82
+ --text-main: #0f172a;
83
+ --text-sub: #64748b;
84
+ --border: #e2e8f0;
85
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
86
+ --gradient-1: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
87
+ }
88
+
89
+ [data-theme="dark"] {
90
+ --primary: #818cf8;
91
+ --primary-hover: #6366f1;
92
+ --bg-body: #0f172a;
93
+ --bg-card: #1e293b;
94
+ --text-main: #f8fafc;
95
+ --text-sub: #94a3b8;
96
+ --border: #334155;
97
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
98
+ --gradient-1: linear-gradient(135deg, #4f46e5 0%, #7e22ce 100%);
99
+ }
100
+
101
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: all 0.3s ease; }
102
+
103
+ body {
104
+ font-family: 'Plus Jakarta Sans', sans-serif;
105
+ background-color: var(--bg-body);
106
+ color: var(--text-main);
107
+ min-height: 100vh;
108
+ display: flex;
109
+ flex-direction: column;
110
+ align-items: center;
111
+ padding: 2rem 1rem;
112
+ }
113
+
114
+ /* Header */
115
+ .header-container {
116
+ text-align: center; margin-bottom: 3rem; animation: fadeIn 0.8s ease-out;
117
+ }
118
+ .title {
119
+ font-size: 2.5rem; font-weight: 800; margin-bottom: 0.5rem;
120
+ background: var(--gradient-1); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
121
+ }
122
+ .subtitle { color: var(--text-sub); font-size: 1.1rem; }
123
+
124
+ .theme-toggle {
125
+ position: absolute; top: 1rem; right: 1rem;
126
+ background: var(--bg-card); border: 1px solid var(--border);
127
+ color: var(--text-main); padding: 10px; border-radius: 50%; cursor: pointer;
128
+ }
129
+
130
+ /* Grid */
131
+ .tools-grid {
132
+ display: grid;
133
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
134
+ gap: 1.5rem;
135
+ width: 100%; max-width: 1000px;
136
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
137
+ }
138
+
139
+ /* Cards */
140
+ .tool-card {
141
+ background: var(--bg-card);
142
+ border-radius: 20px;
143
+ padding: 2rem;
144
+ border: 1px solid var(--border);
145
+ box-shadow: var(--shadow);
146
+ text-decoration: none;
147
+ color: var(--text-main);
148
+ display: flex; flex-direction: column; align-items: center; text-align: center;
149
+ transition: transform 0.2s, box-shadow 0.2s;
150
+ position: relative; overflow: hidden;
151
+ }
152
+ .tool-card:hover {
153
+ transform: translateY(-5px);
154
+ box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 10px 10px -5px rgb(0 0 0 / 0.04);
155
+ border-color: var(--primary);
156
+ }
157
+
158
+ .icon { font-size: 3rem; margin-bottom: 1rem; }
159
+ .card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 0.5rem; }
160
+ .card-desc { font-size: 0.9rem; color: var(--text-sub); line-height: 1.5; }
161
+
162
+ /* Footer */
163
+ .dev-footer {
164
+ margin-top: 4rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
165
+ padding: 2rem; width: 100%; max-width: 600px; border-top: 1px solid var(--border);
166
+ animation: fadeIn 1.2s ease-out;
167
+ }
168
+ .dev-badge {
169
+ display: inline-block; background: var(--bg-card); padding: 8px 20px;
170
+ border-radius: 30px; border: 1px solid var(--border); font-weight: 600; margin-top: 10px;
171
+ }
172
+ .dev-name { color: var(--primary); font-weight: 800; }
173
+ .github-link {
174
+ display: inline-flex; align-items: center; gap: 8px; margin-top: 15px;
175
+ color: var(--text-main); text-decoration: none; font-weight: 600;
176
+ padding: 8px 16px; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border);
177
+ }
178
+ .github-link:hover { background: var(--bg-body); }
179
+
180
+ @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
181
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
182
+ </style>
183
+ </head>
184
+ <body>
185
+
186
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">πŸŒ™</button>
187
+
188
+ <div class="header-container">
189
+ <div class="title">Easy Tools Suite</div>
190
+ <div class="subtitle">Sameer Banchhor</div>
191
+ </div>
192
+
193
+ <div class="tools-grid">
194
+
195
+ <a href="/image/ui" class="tool-card">
196
+ <div class="icon">πŸ–ΌοΈ</div>
197
+ <div class="card-title">Image Tools</div>
198
+ <div class="card-desc">Compress JPEGs to specific sizes & remove backgrounds instantly using AI.</div>
199
+ </a>
200
+
201
+ <a href="/pdf/ui" class="tool-card">
202
+ <div class="icon">πŸ“„</div>
203
+ <div class="card-title">PDF Master</div>
204
+ <div class="card-desc">Merge multiple documents or rotate pages with a simple drag & drop interface.</div>
205
+ </a>
206
+
207
+ <a href="/drive/storage/ui" class="tool-card">
208
+ <div class="icon">☁️</div>
209
+ <div class="card-title">Cloud Drive</div>
210
+ <div class="card-desc">Upload files locally or use Aria2 for high-speed remote server downloads.</div>
211
+ </a>
212
+
213
+ <a href="/security/password-generator/ui" class="tool-card">
214
+ <div class="icon">πŸ”</div>
215
+ <div class="card-title">Security</div>
216
+ <div class="card-desc">Generate cryptographically strong passwords with advanced custom rules.</div>
217
+ </a>
218
+
219
+ <a href="/server-status/ui" class="tool-card">
220
+ <div class="icon">πŸ“Š</div>
221
+ <div class="card-title">Server Monitor</div>
222
+ <div class="card-desc">Real-time dashboard for CPU, RAM, Disk usage, and Network traffic.</div>
223
+ </a>
224
+
225
+ <a href="/docs" class="tool-card" style="border-style: dashed;">
226
+ <div class="icon">⚑</div>
227
+ <div class="card-title">API Docs</div>
228
+ <div class="card-desc">Explore the raw Swagger UI for testing API endpoints directly.</div>
229
+ </a>
230
+
231
+ </div>
232
+
233
+ <footer class="dev-footer">
234
+ <div>Designed & Developed by</div>
235
+ <div class="dev-badge">
236
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
237
+ </div>
238
+ <br>
239
+ <div style="font-size: 0.85rem; margin-top: 5px; opacity: 0.8;">MSc in System Design, Kalyan PG College</div>
240
+
241
+ <a href="https://github.com/sameerbanchhor-git" target="_blank" class="github-link">
242
+ <svg height="20" width="20" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
243
+ GitHub Profile
244
+ </a>
245
+ </footer>
246
+
247
+ <script>
248
+ // Theme Logic
249
+ const html = document.documentElement;
250
+ const themeBtn = document.getElementById('themeBtn');
251
+
252
+ function toggleTheme() {
253
+ const current = html.getAttribute('data-theme');
254
+ const next = current === 'light' ? 'dark' : 'light';
255
+ html.setAttribute('data-theme', next);
256
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
257
+ localStorage.setItem('theme', next);
258
+ }
259
+
260
+ const savedTheme = localStorage.getItem('theme') || 'light';
261
+ html.setAttribute('data-theme', savedTheme);
262
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
263
+ </script>
264
+ </body>
265
+ </html>
266
  """
app/routers/drive/storage.py CHANGED
@@ -12,9 +12,16 @@ from fastapi import (
12
  WebSocket,
13
  WebSocketDisconnect
14
  )
 
15
 
16
- # Import Auth System
17
- from app.routers.auth.system import get_current_user
 
 
 
 
 
 
18
 
19
  # Router Configuration
20
  router = APIRouter(
@@ -27,9 +34,8 @@ UPLOAD_DIR = "uploaded_files"
27
  MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024 # 1GB Limit
28
 
29
  # πŸ” Auth Toggle
30
- AUTH_ENABLED = False # <<< set False to disable authentication
31
 
32
- # Auth Wrapper (returns dependency or None)
33
  def auth_dependency():
34
  return Depends(get_current_user) if AUTH_ENABLED else None
35
 
@@ -40,12 +46,10 @@ os.makedirs(UPLOAD_DIR, exist_ok=True)
40
  # πŸ› οΈ Helper: Safe Username Extraction
41
  # ==========================================
42
  def get_safe_username(user_obj):
43
- if not user_obj:
44
- return "anonymous"
45
- if hasattr(user_obj, "username"):
46
- return user_obj.username
47
- return user_obj.get("username", "anonymous")
48
-
49
 
50
  # ==========================================
51
  # πŸ“€ Standard File Upload
@@ -80,46 +84,8 @@ async def upload_file(
80
  "message": "File uploaded successfully"
81
  }
82
 
83
-
84
- # ==========================================
85
- # πŸš€ Remote Upload (Aria2)
86
- # ==========================================
87
- @router.post("/remote-upload/")
88
- async def remote_url_upload(
89
- url: str,
90
- custom_filename: str = None,
91
- current_user: object = auth_dependency()
92
- ):
93
- username = get_safe_username(current_user)
94
-
95
- command = ["aria2c", "-x", "6", "-s", "6", "-d", UPLOAD_DIR, url]
96
- if custom_filename:
97
- command.extend(["-o", custom_filename])
98
-
99
- try:
100
- process = await asyncio.create_subprocess_exec(
101
- *command,
102
- stdout=asyncio.subprocess.PIPE,
103
- stderr=asyncio.subprocess.PIPE
104
- )
105
- stdout, stderr = await process.communicate()
106
-
107
- if process.returncode != 0:
108
- raise HTTPException(status_code=500, detail=f"Aria2 Error: {stderr.decode()}")
109
-
110
- except Exception as e:
111
- raise HTTPException(status_code=500, detail=f"System Error: {str(e)}")
112
-
113
- return {
114
- "user": username,
115
- "source": url,
116
- "filename": custom_filename or "Auto-detected",
117
- "message": "Download completed via Aria2"
118
- }
119
-
120
-
121
  # ==========================================
122
- # πŸ“‘ WebSocket Remote Upload (No Auth Applied)
123
  # ==========================================
124
  @router.websocket("/ws/remote-upload/")
125
  async def websocket_remote_upload(websocket: WebSocket):
@@ -135,11 +101,17 @@ async def websocket_remote_upload(websocket: WebSocket):
135
  await websocket.close()
136
  return
137
 
138
- command = ["aria2c", "-x", "6", "-s", "6", "-d", UPLOAD_DIR, target_url]
 
 
 
 
 
 
139
  if custom_filename:
140
  command.extend(["-o", custom_filename])
141
 
142
- await websocket.send_text(f"πŸš€ Starting download: {target_url}")
143
 
144
  process = await asyncio.create_subprocess_exec(
145
  *command,
@@ -147,6 +119,7 @@ async def websocket_remote_upload(websocket: WebSocket):
147
  stderr=asyncio.subprocess.PIPE
148
  )
149
 
 
150
  while True:
151
  line = await process.stdout.readline()
152
  if not line:
@@ -160,10 +133,12 @@ async def websocket_remote_upload(websocket: WebSocket):
160
  await websocket.send_text(decoded_line)
161
 
162
  await process.wait()
 
163
  if process.returncode == 0:
164
  await websocket.send_text("βœ… Download Complete.")
165
  else:
166
- await websocket.send_text("❌ Download Failed.")
 
167
 
168
  await websocket.close()
169
 
@@ -175,3 +150,339 @@ async def websocket_remote_upload(websocket: WebSocket):
175
  await websocket.close()
176
  except:
177
  pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  WebSocket,
13
  WebSocketDisconnect
14
  )
15
+ from fastapi.responses import HTMLResponse
16
 
17
+ # Import Auth System (Assuming this exists in your project structure)
18
+ # from app.routers.auth.system import get_current_user
19
+
20
+ # Mocking auth for standalone functionality if import fails
21
+ try:
22
+ from app.routers.auth.system import get_current_user
23
+ except ImportError:
24
+ def get_current_user(): return "anonymous"
25
 
26
  # Router Configuration
27
  router = APIRouter(
 
34
  MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024 # 1GB Limit
35
 
36
  # πŸ” Auth Toggle
37
+ AUTH_ENABLED = False
38
 
 
39
  def auth_dependency():
40
  return Depends(get_current_user) if AUTH_ENABLED else None
41
 
 
46
  # πŸ› οΈ Helper: Safe Username Extraction
47
  # ==========================================
48
  def get_safe_username(user_obj):
49
+ if not user_obj: return "anonymous"
50
+ if hasattr(user_obj, "username"): return user_obj.username
51
+ if isinstance(user_obj, dict): return user_obj.get("username", "anonymous")
52
+ return str(user_obj)
 
 
53
 
54
  # ==========================================
55
  # πŸ“€ Standard File Upload
 
84
  "message": "File uploaded successfully"
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  # ==========================================
88
+ # πŸ“‘ WebSocket Remote Upload
89
  # ==========================================
90
  @router.websocket("/ws/remote-upload/")
91
  async def websocket_remote_upload(websocket: WebSocket):
 
101
  await websocket.close()
102
  return
103
 
104
+ # Check if aria2c is installed
105
+ if shutil.which("aria2c") is None:
106
+ await websocket.send_text("❌ Error: 'aria2c' is not installed on the server.")
107
+ await websocket.close()
108
+ return
109
+
110
+ command = ["aria2c", "-x", "8", "-s", "8", "-d", UPLOAD_DIR, target_url]
111
  if custom_filename:
112
  command.extend(["-o", custom_filename])
113
 
114
+ await websocket.send_text(f"πŸš€ Starting download: {target_url}...")
115
 
116
  process = await asyncio.create_subprocess_exec(
117
  *command,
 
119
  stderr=asyncio.subprocess.PIPE
120
  )
121
 
122
+ # Stream stdout line by line
123
  while True:
124
  line = await process.stdout.readline()
125
  if not line:
 
133
  await websocket.send_text(decoded_line)
134
 
135
  await process.wait()
136
+
137
  if process.returncode == 0:
138
  await websocket.send_text("βœ… Download Complete.")
139
  else:
140
+ stderr_data = await process.stderr.read()
141
+ await websocket.send_text(f"❌ Download Failed: {stderr_data.decode()}")
142
 
143
  await websocket.close()
144
 
 
150
  await websocket.close()
151
  except:
152
  pass
153
+
154
+ # ==========================================
155
+ # 🎨 Modern UI Endpoint
156
+ # ==========================================
157
+ # Path becomes: /drive/storage/ui
158
+ @router.get("/storage/ui", response_class=HTMLResponse)
159
+ async def drive_ui():
160
+ html_content = """
161
+ <!DOCTYPE html>
162
+ <html lang="en" data-theme="light">
163
+ <head>
164
+ <meta charset="UTF-8">
165
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
+ <title>Cloud Drive | by Sam</title>
167
+ <link rel="preconnect" href="https://fonts.googleapis.com">
168
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
169
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
170
+
171
+ <style>
172
+ /* --- Shared Theme Config --- */
173
+ :root {
174
+ --primary: #3b82f6;
175
+ --primary-hover: #2563eb;
176
+ --bg-body: #f8fafc;
177
+ --bg-card: #ffffff;
178
+ --text-main: #0f172a;
179
+ --text-sub: #64748b;
180
+ --border: #e2e8f0;
181
+ --drop-bg: #f1f5f9;
182
+ --drop-hover: #dbeafe;
183
+ --term-bg: #1e293b;
184
+ --term-text: #34d399;
185
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
186
+ }
187
+
188
+ [data-theme="dark"] {
189
+ --primary: #60a5fa;
190
+ --primary-hover: #3b82f6;
191
+ --bg-body: #0f172a;
192
+ --bg-card: #1e293b;
193
+ --text-main: #f8fafc;
194
+ --text-sub: #94a3b8;
195
+ --border: #334155;
196
+ --drop-bg: #1e293b;
197
+ --drop-hover: #1e3a8a;
198
+ --term-bg: #0f172a;
199
+ --term-text: #34d399;
200
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
201
+ }
202
+
203
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
204
+
205
+ body {
206
+ font-family: 'Plus Jakarta Sans', sans-serif;
207
+ background-color: var(--bg-body);
208
+ color: var(--text-main);
209
+ min-height: 100vh;
210
+ display: flex;
211
+ flex-direction: column;
212
+ align-items: center;
213
+ justify-content: center;
214
+ padding: 1rem;
215
+ }
216
+
217
+ /* --- Navbar --- */
218
+ .navbar {
219
+ width: 100%; max-width: 1000px; display: flex;
220
+ justify-content: space-between; align-items: center; margin-bottom: 2rem;
221
+ animation: fadeIn 0.8s ease-out;
222
+ }
223
+ .brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
224
+ .brand span { color: var(--primary); }
225
+
226
+ .theme-toggle {
227
+ background: none; border: none; cursor: pointer; color: var(--text-main);
228
+ font-size: 1.2rem; padding: 8px; border-radius: 50%;
229
+ background-color: var(--bg-card); border: 1px solid var(--border);
230
+ }
231
+
232
+ /* --- Container & Tabs --- */
233
+ .container {
234
+ background: var(--bg-card); width: 100%; max-width: 800px;
235
+ border-radius: 24px; box-shadow: var(--shadow);
236
+ border: 1px solid var(--border); overflow: hidden;
237
+ }
238
+
239
+ .tabs { display: flex; border-bottom: 1px solid var(--border); }
240
+ .tab-btn {
241
+ flex: 1; padding: 1rem; border: none; background: transparent;
242
+ font-family: inherit; font-weight: 600; color: var(--text-sub);
243
+ cursor: pointer; transition: all 0.2s; border-bottom: 3px solid transparent;
244
+ }
245
+ .tab-btn:hover { background-color: var(--drop-bg); }
246
+ .tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
247
+
248
+ .tab-content { padding: 2.5rem; display: none; animation: fadeIn 0.4s ease; }
249
+ .tab-content.active { display: block; }
250
+
251
+ /* --- Forms --- */
252
+ .input-group { margin-bottom: 1.5rem; }
253
+ .input-group label { display: block; font-size: 0.9rem; font-weight: 600; color: var(--text-sub); margin-bottom: 0.5rem; }
254
+ .input-field {
255
+ width: 100%; padding: 0.8rem; border-radius: 12px; border: 1px solid var(--border);
256
+ background-color: var(--bg-body); color: var(--text-main); font-family: inherit;
257
+ }
258
+ .input-field:focus { outline: 2px solid var(--primary); border-color: transparent; }
259
+
260
+ /* --- Upload Zone --- */
261
+ .drop-area {
262
+ border: 2px dashed var(--border); border-radius: 16px; padding: 3rem 1rem;
263
+ text-align: center; cursor: pointer; background-color: var(--drop-bg);
264
+ transition: all 0.3s; margin-bottom: 1rem;
265
+ }
266
+ .drop-area:hover, .drop-area.dragover {
267
+ border-color: var(--primary); background-color: var(--drop-hover); transform: scale(1.01);
268
+ }
269
+ .drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; }
270
+
271
+ /* --- Terminal --- */
272
+ .terminal {
273
+ background-color: var(--term-bg); color: var(--term-text);
274
+ font-family: 'JetBrains Mono', monospace; font-size: 0.85rem;
275
+ padding: 1rem; border-radius: 12px; height: 250px;
276
+ overflow-y: auto; margin-top: 1.5rem; display: none;
277
+ border: 1px solid var(--border); box-shadow: inset 0 2px 4px rgba(0,0,0,0.3);
278
+ }
279
+ .terminal-line { margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px; }
280
+
281
+ /* --- Buttons --- */
282
+ .btn {
283
+ padding: 0.8rem 2rem; border-radius: 12px; font-weight: 600; width: 100%;
284
+ cursor: pointer; border: none; font-size: 1rem; background-color: var(--primary); color: white;
285
+ transition: transform 0.2s, opacity 0.2s;
286
+ }
287
+ .btn:hover { opacity: 0.9; transform: translateY(-1px); }
288
+ .btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
289
+
290
+ /* --- Footer --- */
291
+ .dev-footer {
292
+ margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
293
+ padding: 1rem; border-top: 1px solid var(--border); width: 100%; max-width: 600px;
294
+ }
295
+ .dev-badge {
296
+ display: inline-block; background: var(--bg-card); padding: 5px 15px;
297
+ border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
298
+ }
299
+ .dev-name { color: var(--primary); font-weight: 700; }
300
+
301
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
302
+ </style>
303
+ </head>
304
+ <body>
305
+
306
+ <nav class="navbar">
307
+ <div class="brand">☁️ Cloud Storage <span>Manager</span></div>
308
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">πŸŒ™</button>
309
+ </nav>
310
+
311
+ <div class="container">
312
+ <div class="tabs">
313
+ <button class="tab-btn active" onclick="switchTab('upload')">File Upload</button>
314
+ <button class="tab-btn" onclick="switchTab('remote')">Remote URL (Aria2)</button>
315
+ </div>
316
+
317
+ <div id="tab-upload" class="tab-content active">
318
+ <div class="drop-area" id="dropArea">
319
+ <input type="file" id="fileInput" hidden>
320
+ <span class="drop-icon">πŸ“‚</span>
321
+ <h3 id="dropText" style="font-weight: 600; margin-bottom: 0.5rem;">Click to upload or drag & drop</h3>
322
+ <p style="color: var(--text-sub); font-size: 0.9rem;">Max file size: 1GB</p>
323
+ </div>
324
+ <button class="btn" id="uploadBtn" onclick="uploadFile()" disabled>Upload File</button>
325
+ <p id="uploadStatus" style="text-align: center; margin-top: 1rem; font-weight: 500;"></p>
326
+ </div>
327
+
328
+ <div id="tab-remote" class="tab-content">
329
+ <div class="input-group">
330
+ <label>Target URL</label>
331
+ <input type="text" id="remoteUrl" class="input-field" placeholder="https://example.com/file.zip">
332
+ </div>
333
+ <div class="input-group">
334
+ <label>Custom Filename (Optional)</label>
335
+ <input type="text" id="remoteName" class="input-field" placeholder="my_downloaded_file.zip">
336
+ </div>
337
+ <button class="btn" onclick="startRemoteUpload()">Start Remote Download</button>
338
+
339
+ <div class="terminal" id="terminal"></div>
340
+ </div>
341
+ </div>
342
+
343
+ <footer class="dev-footer">
344
+ <div>Designed & Developed by</div>
345
+ <div class="dev-badge">
346
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
347
+ </div>
348
+ </footer>
349
+
350
+ <script>
351
+ // --- Theme Logic ---
352
+ const html = document.documentElement;
353
+ const themeBtn = document.getElementById('themeBtn');
354
+ function toggleTheme() {
355
+ const current = html.getAttribute('data-theme');
356
+ const next = current === 'light' ? 'dark' : 'light';
357
+ html.setAttribute('data-theme', next);
358
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
359
+ localStorage.setItem('theme', next);
360
+ }
361
+ const savedTheme = localStorage.getItem('theme') || 'light';
362
+ html.setAttribute('data-theme', savedTheme);
363
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
364
+
365
+ // --- Tab Logic ---
366
+ function switchTab(tab) {
367
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
368
+ document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
369
+
370
+ document.getElementById('tab-' + tab).classList.add('active');
371
+ // Find button by text roughly or index - simple hack for 2 tabs
372
+ const btns = document.querySelectorAll('.tab-btn');
373
+ if(tab === 'upload') btns[0].classList.add('active');
374
+ else btns[1].classList.add('active');
375
+ }
376
+
377
+ // --- Local Upload Logic ---
378
+ const dropArea = document.getElementById('dropArea');
379
+ const fileInput = document.getElementById('fileInput');
380
+ const uploadBtn = document.getElementById('uploadBtn');
381
+ const dropText = document.getElementById('dropText');
382
+ const uploadStatus = document.getElementById('uploadStatus');
383
+
384
+ dropArea.addEventListener('click', () => fileInput.click());
385
+
386
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
387
+ dropArea.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }, false);
388
+ });
389
+ dropArea.addEventListener('dragover', () => dropArea.classList.add('dragover'));
390
+ dropArea.addEventListener('dragleave', () => dropArea.classList.remove('dragover'));
391
+ dropArea.addEventListener('drop', (e) => {
392
+ if(e.dataTransfer.files.length) {
393
+ fileInput.files = e.dataTransfer.files;
394
+ handleFileSelect();
395
+ }
396
+ });
397
+
398
+ fileInput.addEventListener('change', handleFileSelect);
399
+
400
+ function handleFileSelect() {
401
+ const file = fileInput.files[0];
402
+ if(file) {
403
+ dropText.textContent = "Selected: " + file.name;
404
+ uploadBtn.disabled = false;
405
+ uploadStatus.textContent = "";
406
+ }
407
+ }
408
+
409
+ async function uploadFile() {
410
+ const file = fileInput.files[0];
411
+ if(!file) return;
412
+
413
+ uploadBtn.disabled = true;
414
+ uploadBtn.textContent = "Uploading...";
415
+
416
+ const formData = new FormData();
417
+ formData.append('file', file);
418
+
419
+ try {
420
+ const res = await fetch('/drive/upload/', { method: 'POST', body: formData });
421
+ const data = await res.json();
422
+
423
+ if(res.ok) {
424
+ uploadStatus.style.color = "var(--primary)";
425
+ uploadStatus.textContent = "βœ… " + data.message;
426
+ } else {
427
+ throw new Error(data.detail || "Upload failed");
428
+ }
429
+ } catch(err) {
430
+ uploadStatus.style.color = "#ef4444";
431
+ uploadStatus.textContent = "❌ Error: " + err.message;
432
+ } finally {
433
+ uploadBtn.disabled = false;
434
+ uploadBtn.textContent = "Upload File";
435
+ }
436
+ }
437
+
438
+ // --- Remote Upload Logic (WebSocket) ---
439
+ const terminal = document.getElementById('terminal');
440
+
441
+ function logToTerminal(msg) {
442
+ terminal.style.display = 'block';
443
+ const div = document.createElement('div');
444
+ div.className = 'terminal-line';
445
+ div.textContent = "> " + msg;
446
+ terminal.appendChild(div);
447
+ terminal.scrollTop = terminal.scrollHeight;
448
+ }
449
+
450
+ function startRemoteUpload() {
451
+ const url = document.getElementById('remoteUrl').value;
452
+ const filename = document.getElementById('remoteName').value;
453
+
454
+ if(!url) { alert("Please enter a URL"); return; }
455
+
456
+ terminal.innerHTML = ""; // Clear logs
457
+ logToTerminal("Initializing WebSocket connection...");
458
+
459
+ // Determine protocol (ws or wss)
460
+ const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
461
+ // Note: The websocket endpoint remains the same, logic is handled by router prefix
462
+ const wsUrl = `${protocol}://${window.location.host}/drive/ws/remote-upload/`;
463
+
464
+ const ws = new WebSocket(wsUrl);
465
+
466
+ ws.onopen = () => {
467
+ logToTerminal("Connected. Sending download command...");
468
+ ws.send(JSON.stringify({ url: url, filename: filename }));
469
+ };
470
+
471
+ ws.onmessage = (event) => {
472
+ logToTerminal(event.data);
473
+ };
474
+
475
+ ws.onerror = (error) => {
476
+ logToTerminal("❌ WebSocket Error");
477
+ console.error(error);
478
+ };
479
+
480
+ ws.onclose = () => {
481
+ logToTerminal("Connection closed.");
482
+ };
483
+ }
484
+ </script>
485
+ </body>
486
+ </html>
487
+ """
488
+ return html_content
app/routers/image/bgremover.py CHANGED
@@ -1,5 +1,6 @@
 
1
  from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
2
- from fastapi.responses import Response
3
  from rembg import remove
4
  from PIL import Image
5
  import io
@@ -15,32 +16,416 @@ router = APIRouter(
15
  # ==========================================
16
  # πŸ” Auth Toggle
17
  # ==========================================
18
- AUTH_ENABLED = False # <<< set False to disable authentication
19
 
20
- # Logic to conditionally apply authentication
21
- # If True, the endpoint will require a valid token.
22
- # If False, the dependency list is empty.
23
  security_dependencies = [Depends(get_current_user)] if AUTH_ENABLED else []
24
 
25
  @router.post("/remove-bg", dependencies=security_dependencies)
26
  async def remove_background(file: UploadFile = File(...)):
27
- """
28
- Upload an image and receive it back with the background removed (PNG).
29
- """
30
- # 1. Validate file type
31
  if not file.content_type.startswith("image/"):
32
  raise HTTPException(status_code=400, detail="Invalid file type. Please upload an image.")
33
-
34
  try:
35
- # 2. Read image data
36
  image_data = await file.read()
37
-
38
- # 3. Process image using rembg
39
- # We pass the raw bytes to remove() and it returns bytes (PNG)
40
  output_data = remove(image_data)
41
-
42
- # 4. Return the result as a raw image response
43
  return Response(content=output_data, media_type="image/png")
44
-
45
  except Exception as e:
46
- raise HTTPException(status_code=500, detail=f"Image processing failed: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from plistlib import UID
2
  from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
3
+ from fastapi.responses import Response, HTMLResponse
4
  from rembg import remove
5
  from PIL import Image
6
  import io
 
16
  # ==========================================
17
  # πŸ” Auth Toggle
18
  # ==========================================
19
+ AUTH_ENABLED = False
20
 
 
 
 
21
  security_dependencies = [Depends(get_current_user)] if AUTH_ENABLED else []
22
 
23
  @router.post("/remove-bg", dependencies=security_dependencies)
24
  async def remove_background(file: UploadFile = File(...)):
 
 
 
 
25
  if not file.content_type.startswith("image/"):
26
  raise HTTPException(status_code=400, detail="Invalid file type. Please upload an image.")
 
27
  try:
 
28
  image_data = await file.read()
 
 
 
29
  output_data = remove(image_data)
 
 
30
  return Response(content=output_data, media_type="image/png")
 
31
  except Exception as e:
32
+ raise HTTPException(status_code=500, detail=f"Image processing failed: {str(e)}")
33
+
34
+
35
+ # =================================
36
+ # MODERN UI ENDPOINT
37
+ # =================================
38
+
39
+ @router.get("/ui", response_class=HTMLResponse)
40
+ async def image_ui():
41
+ html_content = """
42
+ <!DOCTYPE html>
43
+ <html lang="en" data-theme="light">
44
+ <head>
45
+ <meta charset="UTF-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
47
+ <title>RemBG Pro | AI Background Remover</title>
48
+ <link rel="preconnect" href="https://fonts.googleapis.com">
49
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
51
+
52
+ <style>
53
+ /* --- CSS Variables & Theme Config --- */
54
+ :root {
55
+ --primary: #6366f1;
56
+ --primary-hover: #4f46e5;
57
+ --bg-body: #f8fafc;
58
+ --bg-card: #ffffff;
59
+ --text-main: #0f172a;
60
+ --text-sub: #64748b;
61
+ --border: #e2e8f0;
62
+ --drop-bg: #f1f5f9;
63
+ --drop-hover: #e0e7ff;
64
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
65
+ --checker-1: #e2e8f0;
66
+ --checker-2: transparent;
67
+ }
68
+
69
+ [data-theme="dark"] {
70
+ --primary: #818cf8;
71
+ --primary-hover: #6366f1;
72
+ --bg-body: #0f172a;
73
+ --bg-card: #1e293b;
74
+ --text-main: #f8fafc;
75
+ --text-sub: #94a3b8;
76
+ --border: #334155;
77
+ --drop-bg: #1e293b;
78
+ --drop-hover: #312e81;
79
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
80
+ --checker-1: #334155;
81
+ --checker-2: #1e293b;
82
+ }
83
+
84
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
85
+
86
+ body {
87
+ font-family: 'Plus Jakarta Sans', sans-serif;
88
+ background-color: var(--bg-body);
89
+ color: var(--text-main);
90
+ min-height: 100vh;
91
+ display: flex;
92
+ flex-direction: column;
93
+ align-items: center;
94
+ justify-content: center;
95
+ padding: 1rem;
96
+ }
97
+
98
+ /* --- Animations --- */
99
+ @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
100
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
101
+ @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); } 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); } }
102
+
103
+ /* --- Layout Components --- */
104
+ .navbar {
105
+ width: 100%;
106
+ max-width: 1000px;
107
+ display: flex;
108
+ justify-content: space-between;
109
+ align-items: center;
110
+ margin-bottom: 2rem;
111
+ animation: fadeIn 0.8s ease-out;
112
+ }
113
+ .brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
114
+ .brand span { color: var(--primary); }
115
+
116
+ .theme-toggle {
117
+ background: none; border: none; cursor: pointer; color: var(--text-main);
118
+ font-size: 1.2rem; padding: 8px; border-radius: 50%;
119
+ background-color: var(--bg-card); border: 1px solid var(--border);
120
+ }
121
+ .theme-toggle:hover { background-color: var(--drop-bg); }
122
+
123
+ .container {
124
+ background: var(--bg-card);
125
+ width: 100%;
126
+ max-width: 900px;
127
+ border-radius: 24px;
128
+ box-shadow: var(--shadow);
129
+ padding: 2.5rem;
130
+ border: 1px solid var(--border);
131
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
132
+ }
133
+
134
+ h1 { font-size: 2rem; font-weight: 700; text-align: center; margin-bottom: 0.5rem; }
135
+ p.subtitle { text-align: center; color: var(--text-sub); margin-bottom: 2rem; }
136
+
137
+ /* --- Drop Zone --- */
138
+ .drop-area {
139
+ border: 2px dashed var(--border);
140
+ border-radius: 16px;
141
+ padding: 3rem 1rem;
142
+ text-align: center;
143
+ cursor: pointer;
144
+ background-color: var(--drop-bg);
145
+ position: relative;
146
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
147
+ }
148
+ .drop-area:hover, .drop-area.dragover {
149
+ border-color: var(--primary);
150
+ background-color: var(--drop-hover);
151
+ transform: scale(1.01);
152
+ }
153
+ .drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; animation: bounce 2s infinite; }
154
+
155
+ /* --- Buttons --- */
156
+ .btn-action {
157
+ margin-top: 1.5rem;
158
+ display: flex; justify-content: center; gap: 1rem;
159
+ }
160
+ .btn {
161
+ padding: 0.8rem 2rem;
162
+ border-radius: 12px;
163
+ font-weight: 600;
164
+ cursor: pointer;
165
+ border: none;
166
+ font-size: 1rem;
167
+ transition: transform 0.2s;
168
+ display: inline-flex; align-items: center; gap: 8px;
169
+ }
170
+ .btn-primary {
171
+ background-color: var(--primary);
172
+ color: white;
173
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
174
+ }
175
+ .btn-primary:hover { background-color: var(--primary-hover); transform: translateY(-2px); }
176
+ .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
177
+
178
+ .btn-download {
179
+ background-color: #10b981; color: white; text-decoration: none;
180
+ }
181
+ .btn-download:hover { background-color: #059669; }
182
+
183
+ /* --- Results --- */
184
+ .results-container {
185
+ display: none;
186
+ grid-template-columns: 1fr 1fr;
187
+ gap: 1.5rem;
188
+ margin-top: 2rem;
189
+ animation: fadeIn 0.5s ease;
190
+ }
191
+ .img-card {
192
+ background: var(--bg-body);
193
+ padding: 1rem;
194
+ border-radius: 16px;
195
+ border: 1px solid var(--border);
196
+ }
197
+ .card-header { font-size: 0.9rem; font-weight: 600; color: var(--text-sub); margin-bottom: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
198
+
199
+ .img-wrapper {
200
+ width: 100%; height: 250px;
201
+ border-radius: 12px;
202
+ overflow: hidden;
203
+ display: flex; align-items: center; justify-content: center;
204
+ /* Checkered pattern */
205
+ background-image:
206
+ linear-gradient(45deg, var(--checker-1) 25%, var(--checker-2) 25%),
207
+ linear-gradient(-45deg, var(--checker-1) 25%, var(--checker-2) 25%),
208
+ linear-gradient(45deg, var(--checker-2) 75%, var(--checker-1) 75%),
209
+ linear-gradient(-45deg, var(--checker-2) 75%, var(--checker-1) 75%);
210
+ background-size: 20px 20px;
211
+ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
212
+ }
213
+ .img-wrapper img { max-width: 100%; max-height: 100%; object-fit: contain; }
214
+
215
+ /* --- Footer --- */
216
+ .dev-footer {
217
+ margin-top: 3rem;
218
+ text-align: center;
219
+ font-size: 0.9rem;
220
+ color: var(--text-sub);
221
+ padding: 1rem;
222
+ border-top: 1px solid var(--border);
223
+ width: 100%;
224
+ max-width: 600px;
225
+ animation: fadeIn 1.2s ease-out;
226
+ }
227
+ .dev-badge {
228
+ display: inline-block;
229
+ background: var(--bg-card);
230
+ padding: 5px 15px;
231
+ border-radius: 20px;
232
+ border: 1px solid var(--border);
233
+ font-weight: 500;
234
+ margin-top: 5px;
235
+ }
236
+ .dev-name { color: var(--primary); font-weight: 700; }
237
+
238
+ /* --- Loaders --- */
239
+ .loading-spinner {
240
+ display: none; width: 24px; height: 24px;
241
+ border: 3px solid rgba(255,255,255,0.3);
242
+ border-radius: 50%; border-top-color: white;
243
+ animation: spin 1s ease-in-out infinite;
244
+ }
245
+ @keyframes spin { to { transform: rotate(360deg); } }
246
+
247
+ /* --- Responsive Design --- */
248
+ @media (max-width: 768px) {
249
+ .results-container { grid-template-columns: 1fr; }
250
+ h1 { font-size: 1.5rem; }
251
+ .container { padding: 1.5rem; }
252
+ }
253
+ </style>
254
+ </head>
255
+ <body>
256
+
257
+ <nav class="navbar">
258
+ <div class="brand">✨ Background removal tool <span>by sam</span></div>
259
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn" title="Toggle Dark Mode">
260
+ πŸŒ™
261
+ </button>
262
+ </nav>
263
+
264
+ <div class="container">
265
+ <h1>Remove Backgrounds Instantly</h1>
266
+ <p class="subtitle">Upload your image and let our AI handle the magic.</p>
267
+
268
+ <div class="drop-area" id="dropArea">
269
+ <input type="file" id="fileInput" accept="image/*" hidden>
270
+ <span class="drop-icon">πŸ“€</span>
271
+ <h3 style="font-weight: 600; margin-bottom: 0.5rem;">Click to upload or drag & drop</h3>
272
+ <p style="color: var(--text-sub); font-size: 0.9rem;">SVG, PNG, JPG or GIF</p>
273
+ </div>
274
+
275
+ <div class="btn-action">
276
+ <button class="btn btn-primary" id="processBtn" onclick="processImage()" disabled>
277
+ <div class="loading-spinner" id="spinner"></div>
278
+ <span id="btnText">Remove Background</span>
279
+ </button>
280
+ </div>
281
+
282
+ <p id="errorMsg" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
283
+
284
+ <div class="results-container" id="resultsGrid">
285
+ <div class="img-card">
286
+ <div class="card-header">Original</div>
287
+ <div class="img-wrapper">
288
+ <img id="originalImg" src="" alt="Original">
289
+ </div>
290
+ </div>
291
+ <div class="img-card">
292
+ <div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
293
+ <span>Result</span>
294
+ <a id="downloadLink" href="#" download="rembg_result.png" class="btn btn-download" style="padding: 0.3rem 0.8rem; font-size: 0.8rem; border-radius: 6px;">
295
+ ⬇ Save
296
+ </a>
297
+ </div>
298
+ <div class="img-wrapper">
299
+ <img id="processedImg" src="" alt="Processed">
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ <footer class="dev-footer">
306
+ <div>Designed & Developed by</div>
307
+ <div class="dev-badge">
308
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
309
+ </div>
310
+ </footer>
311
+
312
+ <script>
313
+ // --- DOM Elements ---
314
+ const dropArea = document.getElementById('dropArea');
315
+ const fileInput = document.getElementById('fileInput');
316
+ const processBtn = document.getElementById('processBtn');
317
+ const spinner = document.getElementById('spinner');
318
+ const btnText = document.getElementById('btnText');
319
+ const resultsGrid = document.getElementById('resultsGrid');
320
+ const originalImg = document.getElementById('originalImg');
321
+ const processedImg = document.getElementById('processedImg');
322
+ const downloadLink = document.getElementById('downloadLink');
323
+ const errorMsg = document.getElementById('errorMsg');
324
+ const themeBtn = document.getElementById('themeBtn');
325
+ const html = document.documentElement;
326
+
327
+ // --- Theme Logic ---
328
+ function toggleTheme() {
329
+ const current = html.getAttribute('data-theme');
330
+ const next = current === 'light' ? 'dark' : 'light';
331
+ html.setAttribute('data-theme', next);
332
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
333
+ localStorage.setItem('theme', next);
334
+ }
335
+
336
+ // Init Theme
337
+ const savedTheme = localStorage.getItem('theme') || 'light';
338
+ html.setAttribute('data-theme', savedTheme);
339
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
340
+
341
+ // --- Upload Logic ---
342
+ dropArea.addEventListener('click', () => fileInput.click());
343
+
344
+ fileInput.addEventListener('change', handleFileSelect);
345
+
346
+ // Drag & Drop events
347
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
348
+ dropArea.addEventListener(eventName, preventDefaults, false);
349
+ });
350
+
351
+ function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
352
+
353
+ ['dragenter', 'dragover'].forEach(eventName => {
354
+ dropArea.addEventListener(eventName, () => dropArea.classList.add('dragover'), false);
355
+ });
356
+
357
+ ['dragleave', 'drop'].forEach(eventName => {
358
+ dropArea.addEventListener(eventName, () => dropArea.classList.remove('dragover'), false);
359
+ });
360
+
361
+ dropArea.addEventListener('drop', (e) => {
362
+ const dt = e.dataTransfer;
363
+ const files = dt.files;
364
+ if(files.length) {
365
+ fileInput.files = files;
366
+ handleFileSelect();
367
+ }
368
+ });
369
+
370
+ function handleFileSelect() {
371
+ const file = fileInput.files[0];
372
+ if (file) {
373
+ originalImg.src = URL.createObjectURL(file);
374
+ processBtn.disabled = false;
375
+ processBtn.style.animation = "pulse 1.5s infinite";
376
+ resultsGrid.style.display = 'none'; // Hide old results
377
+ errorMsg.style.display = 'none';
378
+
379
+ // Update Drop Area Text to show selection
380
+ dropArea.querySelector('h3').textContent = "Selected: " + file.name;
381
+ }
382
+ }
383
+
384
+ // --- API Logic ---
385
+ async function processImage() {
386
+ const file = fileInput.files[0];
387
+ if (!file) return;
388
+
389
+ // Loading State
390
+ processBtn.disabled = true;
391
+ processBtn.style.animation = "none";
392
+ spinner.style.display = 'block';
393
+ btnText.textContent = 'Processing...';
394
+ errorMsg.style.display = 'none';
395
+
396
+ const formData = new FormData();
397
+ formData.append('file', file);
398
+
399
+ try {
400
+ const response = await fetch('/image/remove-bg', {
401
+ method: 'POST',
402
+ body: formData
403
+ });
404
+
405
+ if (!response.ok) throw new Error('Failed to process image');
406
+
407
+ const blob = await response.blob();
408
+ const objectURL = URL.createObjectURL(blob);
409
+
410
+ processedImg.src = objectURL;
411
+ downloadLink.href = objectURL;
412
+
413
+ // Show result with animation
414
+ resultsGrid.style.display = 'grid';
415
+ resultsGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
416
+
417
+ } catch (err) {
418
+ console.error(err);
419
+ errorMsg.textContent = "Error: " + err.message;
420
+ errorMsg.style.display = 'block';
421
+ } finally {
422
+ processBtn.disabled = false;
423
+ spinner.style.display = 'none';
424
+ btnText.textContent = 'Remove Background';
425
+ }
426
+ }
427
+ </script>
428
+ </body>
429
+ </html>
430
+ """
431
+ return html_content
app/routers/image/jpgcompressor.py CHANGED
@@ -1,5 +1,5 @@
1
  from fastapi import APIRouter, UploadFile, File, Form, HTTPException
2
- from fastapi.responses import StreamingResponse
3
  from PIL import Image
4
  from io import BytesIO
5
 
@@ -8,46 +8,29 @@ from io import BytesIO
8
  # ==========================
9
  router = APIRouter(
10
  prefix="/image",
11
- tags=["Image Processing"]
12
  )
13
 
14
  # ==========================
15
- # πŸ“¦ JPG Target Size Endpoint
16
  # ==========================
17
- @router.post("/compress_jpg_to_size")
18
  async def compress_jpg_to_size(
19
  file: UploadFile = File(..., description="The JPG image file to compress."),
20
  target_size_kb: int = Form(..., description="Target file size in KB (e.g., 240).")
21
  ):
22
  """
23
  πŸ”§ Compresses a JPEG image to fit within a specific file size (in KB).
24
- Uses binary search to find the best quality and, if necessary, resizes dimensions.
25
  """
26
-
27
- # --------------------------
28
- # πŸ›‘ Validation Checks
29
- # --------------------------
30
  if file.content_type not in ["image/jpeg", "image/jpg"]:
31
- raise HTTPException(
32
- status_code=400,
33
- detail="Invalid file type. Only JPEG files are supported."
34
- )
35
-
36
  if target_size_kb <= 0:
37
- raise HTTPException(
38
- status_code=400,
39
- detail="Target size must be greater than 0 KB."
40
- )
41
 
42
- # Calculate target bytes
43
  target_bytes = target_size_kb * 1024
44
-
45
- # --------------------------
46
- # πŸ“₯ Read Image Bytes
47
- # --------------------------
48
  content = await file.read()
49
-
50
- # ⚑ Optimization: If original is already smaller than target, return original
51
  if len(content) <= target_bytes:
52
  return StreamingResponse(
53
  BytesIO(content),
@@ -59,86 +42,415 @@ async def compress_jpg_to_size(
59
 
60
  try:
61
  img = Image.open(input_buffer)
62
-
63
- # Convert to RGB if necessary (e.g., if input was RGBA)
64
  if img.mode != 'RGB':
65
  img = img.convert('RGB')
66
 
67
  output_buffer = BytesIO()
68
 
69
- # --------------------------
70
- # πŸ“‰ Binary Search Algorithm
71
- # --------------------------
72
- # We search for the best quality between 1 and 95
73
- min_quality = 1
74
- max_quality = 95
75
  best_buffer = None
76
 
77
  while min_quality <= max_quality:
78
  quality = (min_quality + max_quality) // 2
79
-
80
- # Clear buffer and save with current quality
81
  output_buffer.seek(0)
82
  output_buffer.truncate()
83
  img.save(output_buffer, format="JPEG", quality=quality)
84
 
85
- size = output_buffer.tell()
86
-
87
- if size <= target_bytes:
88
- # This fits! But can we get better quality?
89
- best_buffer = BytesIO(output_buffer.getvalue()) # Store success
90
- min_quality = quality + 1 # Try higher quality
91
  else:
92
- # Too big, reduce quality
93
  max_quality = quality - 1
94
 
95
- # --------------------------
96
- # ⚠️ Fallback: Resize
97
- # --------------------------
98
- # If even quality=1 is too big, we must reduce image dimensions
99
  if best_buffer is None:
100
  resize_factor = 0.9
101
  while True:
102
  width, height = img.size
103
- new_width = int(width * resize_factor)
104
- new_height = int(height * resize_factor)
105
 
106
- # Stop if image gets too tiny (sanity check)
107
- if new_width < 10 or new_height < 10:
108
- break
109
 
110
  img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
111
-
112
  output_buffer.seek(0)
113
  output_buffer.truncate()
114
- img.save(output_buffer, format="JPEG", quality=5) # Low quality + resize
115
 
116
  if output_buffer.tell() <= target_bytes:
117
  best_buffer = output_buffer
118
  break
119
-
120
- resize_factor *= 0.9 # Reduce size by another 10%
121
 
122
- # If we still failed (extremely rare), just return the last attempt
123
  if best_buffer is None:
124
  output_buffer.seek(0)
125
  best_buffer = output_buffer
126
 
127
  best_buffer.seek(0)
128
-
129
- # --------------------------
130
- # πŸ“€ Return as Stream
131
- # --------------------------
132
  return StreamingResponse(
133
  best_buffer,
134
  media_type="image/jpeg",
135
- headers={
136
- "Content-Disposition": f"attachment; filename=compressed_{file.filename}"
137
- }
138
  )
139
 
140
  except Exception as e:
141
- raise HTTPException(
142
- status_code=500,
143
- detail=f"Image processing error: {e}"
144
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import APIRouter, UploadFile, File, Form, HTTPException
2
+ from fastapi.responses import StreamingResponse, HTMLResponse
3
  from PIL import Image
4
  from io import BytesIO
5
 
 
8
  # ==========================
9
  router = APIRouter(
10
  prefix="/image",
11
+ tags=["Image Compression"]
12
  )
13
 
14
  # ==========================
15
+ # πŸ“¦ API Endpoint (Backend)
16
  # ==========================
17
+ @router.post("/compress-jpg")
18
  async def compress_jpg_to_size(
19
  file: UploadFile = File(..., description="The JPG image file to compress."),
20
  target_size_kb: int = Form(..., description="Target file size in KB (e.g., 240).")
21
  ):
22
  """
23
  πŸ”§ Compresses a JPEG image to fit within a specific file size (in KB).
 
24
  """
 
 
 
 
25
  if file.content_type not in ["image/jpeg", "image/jpg"]:
26
+ raise HTTPException(status_code=400, detail="Invalid file type. Only JPEG files are supported.")
27
+
 
 
 
28
  if target_size_kb <= 0:
29
+ raise HTTPException(status_code=400, detail="Target size must be greater than 0 KB.")
 
 
 
30
 
 
31
  target_bytes = target_size_kb * 1024
 
 
 
 
32
  content = await file.read()
33
+
 
34
  if len(content) <= target_bytes:
35
  return StreamingResponse(
36
  BytesIO(content),
 
42
 
43
  try:
44
  img = Image.open(input_buffer)
 
 
45
  if img.mode != 'RGB':
46
  img = img.convert('RGB')
47
 
48
  output_buffer = BytesIO()
49
 
50
+ # Binary Search
51
+ min_quality, max_quality = 1, 95
 
 
 
 
52
  best_buffer = None
53
 
54
  while min_quality <= max_quality:
55
  quality = (min_quality + max_quality) // 2
 
 
56
  output_buffer.seek(0)
57
  output_buffer.truncate()
58
  img.save(output_buffer, format="JPEG", quality=quality)
59
 
60
+ if output_buffer.tell() <= target_bytes:
61
+ best_buffer = BytesIO(output_buffer.getvalue())
62
+ min_quality = quality + 1
 
 
 
63
  else:
 
64
  max_quality = quality - 1
65
 
66
+ # Resize Fallback
 
 
 
67
  if best_buffer is None:
68
  resize_factor = 0.9
69
  while True:
70
  width, height = img.size
71
+ new_width, new_height = int(width * resize_factor), int(height * resize_factor)
 
72
 
73
+ if new_width < 10 or new_height < 10: break
 
 
74
 
75
  img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
 
76
  output_buffer.seek(0)
77
  output_buffer.truncate()
78
+ img.save(output_buffer, format="JPEG", quality=5)
79
 
80
  if output_buffer.tell() <= target_bytes:
81
  best_buffer = output_buffer
82
  break
83
+ resize_factor *= 0.9
 
84
 
 
85
  if best_buffer is None:
86
  output_buffer.seek(0)
87
  best_buffer = output_buffer
88
 
89
  best_buffer.seek(0)
 
 
 
 
90
  return StreamingResponse(
91
  best_buffer,
92
  media_type="image/jpeg",
93
+ headers={"Content-Disposition": f"attachment; filename=compressed_{file.filename}"}
 
 
94
  )
95
 
96
  except Exception as e:
97
+ raise HTTPException(status_code=500, detail=f"Image processing error: {str(e)}")
98
+
99
+
100
+ # ==========================
101
+ # 🎨 Modern UI Endpoint
102
+ # ==========================
103
+ @router.get("/compress-jpg/ui", response_class=HTMLResponse)
104
+ async def compressor_ui():
105
+ html_content = """
106
+ <!DOCTYPE html>
107
+ <html lang="en" data-theme="light">
108
+ <head>
109
+ <meta charset="UTF-8">
110
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
111
+ <title>JPEG Compressor | by Sam</title>
112
+ <link rel="preconnect" href="https://fonts.googleapis.com">
113
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
114
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
115
+
116
+ <style>
117
+ /* --- Shared Theme Config --- */
118
+ :root {
119
+ --primary: #10b981;
120
+ --primary-hover: #059669;
121
+ --bg-body: #f8fafc;
122
+ --bg-card: #ffffff;
123
+ --text-main: #0f172a;
124
+ --text-sub: #64748b;
125
+ --border: #e2e8f0;
126
+ --drop-bg: #f1f5f9;
127
+ --drop-hover: #d1fae5;
128
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
129
+ }
130
+
131
+ [data-theme="dark"] {
132
+ --primary: #34d399;
133
+ --primary-hover: #10b981;
134
+ --bg-body: #0f172a;
135
+ --bg-card: #1e293b;
136
+ --text-main: #f8fafc;
137
+ --text-sub: #94a3b8;
138
+ --border: #334155;
139
+ --drop-bg: #1e293b;
140
+ --drop-hover: #064e3b;
141
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
142
+ }
143
+
144
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
145
+
146
+ body {
147
+ font-family: 'Plus Jakarta Sans', sans-serif;
148
+ background-color: var(--bg-body);
149
+ color: var(--text-main);
150
+ min-height: 100vh;
151
+ display: flex;
152
+ flex-direction: column;
153
+ align-items: center;
154
+ justify-content: center;
155
+ padding: 1rem;
156
+ }
157
+
158
+ /* --- Animations --- */
159
+ @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
160
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
161
+
162
+ /* --- Navbar --- */
163
+ .navbar {
164
+ width: 100%; max-width: 1000px; display: flex;
165
+ justify-content: space-between; align-items: center; margin-bottom: 2rem;
166
+ animation: fadeIn 0.8s ease-out;
167
+ }
168
+ .brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
169
+ .brand span { color: var(--primary); }
170
+
171
+ .theme-toggle {
172
+ background: none; border: none; cursor: pointer; color: var(--text-main);
173
+ font-size: 1.2rem; padding: 8px; border-radius: 50%;
174
+ background-color: var(--bg-card); border: 1px solid var(--border);
175
+ }
176
+
177
+ /* --- Main Container --- */
178
+ .container {
179
+ background: var(--bg-card); width: 100%; max-width: 900px;
180
+ border-radius: 24px; box-shadow: var(--shadow); padding: 2.5rem;
181
+ border: 1px solid var(--border);
182
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
183
+ }
184
+
185
+ h1 { font-size: 2rem; font-weight: 700; text-align: center; margin-bottom: 0.5rem; }
186
+ p.subtitle { text-align: center; color: var(--text-sub); margin-bottom: 2rem; }
187
+
188
+ /* --- Controls --- */
189
+ .controls {
190
+ display: flex; justify-content: center; gap: 1rem; margin-bottom: 1.5rem;
191
+ align-items: center; flex-wrap: wrap;
192
+ }
193
+ .input-group {
194
+ display: flex; flex-direction: column; gap: 0.5rem;
195
+ }
196
+ .input-group label { font-size: 0.9rem; font-weight: 600; color: var(--text-sub); }
197
+ .input-field {
198
+ padding: 0.8rem; border-radius: 12px; border: 1px solid var(--border);
199
+ background-color: var(--bg-body); color: var(--text-main);
200
+ font-family: inherit; font-size: 1rem; width: 150px;
201
+ }
202
+
203
+ /* --- Drop Zone --- */
204
+ .drop-area {
205
+ border: 2px dashed var(--border); border-radius: 16px; padding: 2rem 1rem;
206
+ text-align: center; cursor: pointer; background-color: var(--drop-bg);
207
+ transition: all 0.3s;
208
+ }
209
+ .drop-area:hover, .drop-area.dragover {
210
+ border-color: var(--primary); background-color: var(--drop-hover); transform: scale(1.01);
211
+ }
212
+ .drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; }
213
+
214
+ /* --- Buttons --- */
215
+ .btn-action { margin-top: 1.5rem; display: flex; justify-content: center; }
216
+ .btn {
217
+ padding: 0.8rem 2rem; border-radius: 12px; font-weight: 600;
218
+ cursor: pointer; border: none; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px;
219
+ transition: transform 0.2s;
220
+ }
221
+ .btn-primary {
222
+ background-color: var(--primary); color: white;
223
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
224
+ }
225
+ .btn-primary:hover { background-color: var(--primary-hover); transform: translateY(-2px); }
226
+ .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
227
+
228
+ .btn-download {
229
+ background-color: var(--primary); color: white; text-decoration: none;
230
+ padding: 0.4rem 1rem; border-radius: 8px; font-size: 0.9rem;
231
+ }
232
+ .btn-download:hover { opacity: 0.9; }
233
+
234
+ /* --- Results --- */
235
+ .results-container {
236
+ display: none; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 2rem;
237
+ animation: fadeIn 0.5s ease;
238
+ }
239
+ .img-card {
240
+ background: var(--bg-body); padding: 1rem; border-radius: 16px; border: 1px solid var(--border);
241
+ }
242
+ .card-header {
243
+ font-size: 0.9rem; font-weight: 600; color: var(--text-sub);
244
+ margin-bottom: 0.8rem; text-transform: uppercase;
245
+ display: flex; justify-content: space-between; align-items: center;
246
+ }
247
+ .img-wrapper {
248
+ width: 100%; height: 250px; border-radius: 12px; overflow: hidden;
249
+ display: flex; align-items: center; justify-content: center;
250
+ background-color: var(--drop-bg);
251
+ }
252
+ .img-wrapper img { max-width: 100%; max-height: 100%; object-fit: contain; }
253
+
254
+ .loading-spinner {
255
+ display: none; width: 24px; height: 24px;
256
+ border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white;
257
+ animation: spin 1s infinite linear;
258
+ }
259
+ @keyframes spin { to { transform: rotate(360deg); } }
260
+
261
+ /* --- Developer Footer --- */
262
+ .dev-footer {
263
+ margin-top: 3rem;
264
+ text-align: center;
265
+ font-size: 0.9rem;
266
+ color: var(--text-sub);
267
+ padding: 1rem;
268
+ border-top: 1px solid var(--border);
269
+ width: 100%;
270
+ max-width: 600px;
271
+ animation: fadeIn 1.2s ease-out;
272
+ }
273
+ .dev-badge {
274
+ display: inline-block;
275
+ background: var(--bg-card);
276
+ padding: 5px 15px;
277
+ border-radius: 20px;
278
+ border: 1px solid var(--border);
279
+ font-weight: 500;
280
+ margin-top: 5px;
281
+ }
282
+ .dev-name { color: var(--primary); font-weight: 700; }
283
+
284
+ @media (max-width: 768px) {
285
+ .results-container { grid-template-columns: 1fr; }
286
+ }
287
+ </style>
288
+ </head>
289
+ <body>
290
+ <nav class="navbar">
291
+ <div class="brand">⚑ JPEG Compressor <span>by sam</span></div>
292
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">πŸŒ™</button>
293
+ </nav>
294
+
295
+ <div class="container">
296
+ <h1>Smart Image Compression</h1>
297
+ <p class="subtitle">Reduce file size without sacrificing quality.</p>
298
+
299
+ <div class="controls">
300
+ <div class="input-group">
301
+ <label for="sizeInput">Target Size (KB)</label>
302
+ <input type="number" id="sizeInput" class="input-field" value="200" min="10" placeholder="e.g. 200">
303
+ </div>
304
+ </div>
305
+
306
+ <div class="drop-area" id="dropArea">
307
+ <input type="file" id="fileInput" accept="image/jpeg, image/jpg" hidden>
308
+ <span class="drop-icon">πŸ“¦</span>
309
+ <h3 id="dropText" style="font-weight: 600; margin-bottom: 0.5rem;">Click to upload or drag & drop</h3>
310
+ <p style="color: var(--text-sub); font-size: 0.9rem;">JPG or JPEG only</p>
311
+ </div>
312
+
313
+ <div class="btn-action">
314
+ <button class="btn btn-primary" id="processBtn" onclick="processImage()" disabled>
315
+ <div class="loading-spinner" id="spinner"></div>
316
+ <span id="btnText">Compress Image</span>
317
+ </button>
318
+ </div>
319
+
320
+ <p id="errorMsg" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
321
+
322
+ <div class="results-container" id="resultsGrid">
323
+ <div class="img-card">
324
+ <div class="card-header">Original</div>
325
+ <div class="img-wrapper">
326
+ <img id="originalImg" src="" alt="Original">
327
+ </div>
328
+ </div>
329
+ <div class="img-card">
330
+ <div class="card-header">
331
+ <span>Compressed</span>
332
+ <a id="downloadLink" href="#" class="btn-download">⬇ Save</a>
333
+ </div>
334
+ <div class="img-wrapper">
335
+ <img id="processedImg" src="" alt="Compressed">
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+
341
+ <footer class="dev-footer">
342
+ <div>Designed & Developed by</div>
343
+ <div class="dev-badge">
344
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
345
+ </div>
346
+ </footer>
347
+
348
+ <script>
349
+ // Elements
350
+ const dropArea = document.getElementById('dropArea');
351
+ const fileInput = document.getElementById('fileInput');
352
+ const processBtn = document.getElementById('processBtn');
353
+ const sizeInput = document.getElementById('sizeInput');
354
+ const spinner = document.getElementById('spinner');
355
+ const btnText = document.getElementById('btnText');
356
+ const resultsGrid = document.getElementById('resultsGrid');
357
+ const originalImg = document.getElementById('originalImg');
358
+ const processedImg = document.getElementById('processedImg');
359
+ const downloadLink = document.getElementById('downloadLink');
360
+ const errorMsg = document.getElementById('errorMsg');
361
+ const dropText = document.getElementById('dropText');
362
+
363
+ // Theme Logic
364
+ const html = document.documentElement;
365
+ const themeBtn = document.getElementById('themeBtn');
366
+
367
+ function toggleTheme() {
368
+ const current = html.getAttribute('data-theme');
369
+ const next = current === 'light' ? 'dark' : 'light';
370
+ html.setAttribute('data-theme', next);
371
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
372
+ localStorage.setItem('theme', next);
373
+ }
374
+ const savedTheme = localStorage.getItem('theme') || 'light';
375
+ html.setAttribute('data-theme', savedTheme);
376
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
377
+
378
+ // Drag & Drop
379
+ dropArea.addEventListener('click', () => fileInput.click());
380
+
381
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
382
+ dropArea.addEventListener(eventName, (e) => {
383
+ e.preventDefault(); e.stopPropagation();
384
+ }, false);
385
+ });
386
+
387
+ ['dragenter', 'dragover'].forEach(name => dropArea.classList.add('dragover'));
388
+ ['dragleave', 'drop'].forEach(name => dropArea.classList.remove('dragover'));
389
+
390
+ dropArea.addEventListener('drop', (e) => {
391
+ if(e.dataTransfer.files.length) {
392
+ fileInput.files = e.dataTransfer.files;
393
+ handleFileSelect();
394
+ }
395
+ });
396
+
397
+ fileInput.addEventListener('change', handleFileSelect);
398
+
399
+ function handleFileSelect() {
400
+ const file = fileInput.files[0];
401
+ if (file) {
402
+ originalImg.src = URL.createObjectURL(file);
403
+ processBtn.disabled = false;
404
+ dropText.textContent = "Selected: " + file.name;
405
+ resultsGrid.style.display = 'none';
406
+ errorMsg.style.display = 'none';
407
+ }
408
+ }
409
+
410
+ // Process Logic
411
+ async function processImage() {
412
+ const file = fileInput.files[0];
413
+ const kbSize = sizeInput.value;
414
+ if (!file || !kbSize) return;
415
+
416
+ processBtn.disabled = true;
417
+ spinner.style.display = 'block';
418
+ btnText.textContent = 'Compressing...';
419
+
420
+ const formData = new FormData();
421
+ formData.append('file', file);
422
+ formData.append('target_size_kb', kbSize);
423
+
424
+ try {
425
+ const response = await fetch('/image/compress-jpg', {
426
+ method: 'POST',
427
+ body: formData
428
+ });
429
+
430
+ if (!response.ok) throw new Error(await response.text());
431
+
432
+ const blob = await response.blob();
433
+ const objectURL = URL.createObjectURL(blob);
434
+
435
+ processedImg.src = objectURL;
436
+ downloadLink.href = objectURL;
437
+ downloadLink.download = "compressed_" + file.name;
438
+
439
+ resultsGrid.style.display = 'grid';
440
+ resultsGrid.scrollIntoView({ behavior: 'smooth' });
441
+
442
+ } catch (err) {
443
+ console.error(err);
444
+ errorMsg.textContent = "Error: " + (err.message || "Compression failed");
445
+ errorMsg.style.display = 'block';
446
+ } finally {
447
+ processBtn.disabled = false;
448
+ spinner.style.display = 'none';
449
+ btnText.textContent = 'Compress Image';
450
+ }
451
+ }
452
+ </script>
453
+ </body>
454
+ </html>
455
+ """
456
+ return html_content
app/routers/pdf/pdf_tools.py CHANGED
@@ -1,9 +1,10 @@
1
- from fastapi import APIRouter, UploadFile, File, HTTPException, Security
2
- from fastapi.responses import FileResponse
3
  from pypdf import PdfReader, PdfWriter
4
  import os
5
  import shutil
6
  import tempfile
 
7
 
8
  # Define the APIRouter object
9
  router = APIRouter(
@@ -11,8 +12,11 @@ router = APIRouter(
11
  tags=["PDF Manipulation"],
12
  )
13
 
 
 
 
14
  @router.post("/merge")
15
- async def merge_pdfs(files: list[UploadFile] = File(description="List of PDFs to merge")):
16
  """
17
  Merges multiple uploaded PDF files into a single PDF.
18
  """
@@ -22,9 +26,13 @@ async def merge_pdfs(files: list[UploadFile] = File(description="List of PDFs to
22
  pdf_writer = PdfWriter()
23
 
24
  # Create a temporary directory to store files
25
- with tempfile.TemporaryDirectory() as temp_dir:
26
- output_path = os.path.join(temp_dir, "merged_output.pdf")
27
-
 
 
 
 
28
  for file in files:
29
  # Save the uploaded file temporarily
30
  temp_file_path = os.path.join(temp_dir, file.filename)
@@ -43,13 +51,19 @@ async def merge_pdfs(files: list[UploadFile] = File(description="List of PDFs to
43
  with open(output_path, "wb") as output_file:
44
  pdf_writer.write(output_file)
45
 
46
- # Return the merged file as a response
47
  return FileResponse(output_path, media_type="application/pdf", filename="merged_document.pdf")
 
 
 
 
48
 
 
 
 
49
  @router.post("/rotate")
50
  async def rotate_pdf(
51
- file: UploadFile = File(description="PDF file to rotate"),
52
- degrees: int = 90 # Default rotation is 90 degrees clockwise
53
  ):
54
  """
55
  Rotates all pages of a single PDF by a specified degree (90, 180, or 270).
@@ -59,10 +73,11 @@ async def rotate_pdf(
59
 
60
  pdf_writer = PdfWriter()
61
 
62
- with tempfile.TemporaryDirectory() as temp_dir:
63
- input_path = os.path.join(temp_dir, file.filename)
64
- output_path = os.path.join(temp_dir, "rotated_output.pdf")
65
 
 
66
  # Save the uploaded file temporarily
67
  with open(input_path, "wb") as buffer:
68
  shutil.copyfileobj(file.file, buffer)
@@ -80,4 +95,382 @@ async def rotate_pdf(
80
  with open(output_path, "wb") as output_file:
81
  pdf_writer.write(output_file)
82
 
83
- return FileResponse(output_path, media_type="application/pdf", filename=f"rotated_{degrees}.pdf")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, UploadFile, File, HTTPException, Form
2
+ from fastapi.responses import FileResponse, HTMLResponse
3
  from pypdf import PdfReader, PdfWriter
4
  import os
5
  import shutil
6
  import tempfile
7
+ from typing import List
8
 
9
  # Define the APIRouter object
10
  router = APIRouter(
 
12
  tags=["PDF Manipulation"],
13
  )
14
 
15
+ # ==========================
16
+ # 🧩 PDF Merge Endpoint
17
+ # ==========================
18
  @router.post("/merge")
19
+ async def merge_pdfs(files: List[UploadFile] = File(..., description="List of PDFs to merge")):
20
  """
21
  Merges multiple uploaded PDF files into a single PDF.
22
  """
 
26
  pdf_writer = PdfWriter()
27
 
28
  # Create a temporary directory to store files
29
+ # Note: We rely on the OS to clean up the temp dir eventually,
30
+ # or we return a FileResponse which keeps the file handle open briefly.
31
+ # For a robust production app, consider BackgroundTasks for cleanup.
32
+ temp_dir = tempfile.mkdtemp()
33
+ output_path = os.path.join(temp_dir, "merged_output.pdf")
34
+
35
+ try:
36
  for file in files:
37
  # Save the uploaded file temporarily
38
  temp_file_path = os.path.join(temp_dir, file.filename)
 
51
  with open(output_path, "wb") as output_file:
52
  pdf_writer.write(output_file)
53
 
 
54
  return FileResponse(output_path, media_type="application/pdf", filename="merged_document.pdf")
55
+
56
+ except Exception as e:
57
+ shutil.rmtree(temp_dir) # Cleanup on error
58
+ raise HTTPException(status_code=500, detail=str(e))
59
 
60
+ # ==========================
61
+ # πŸ”„ PDF Rotate Endpoint
62
+ # ==========================
63
  @router.post("/rotate")
64
  async def rotate_pdf(
65
+ file: UploadFile = File(..., description="PDF file to rotate"),
66
+ degrees: int = 90
67
  ):
68
  """
69
  Rotates all pages of a single PDF by a specified degree (90, 180, or 270).
 
73
 
74
  pdf_writer = PdfWriter()
75
 
76
+ temp_dir = tempfile.mkdtemp()
77
+ input_path = os.path.join(temp_dir, file.filename)
78
+ output_path = os.path.join(temp_dir, f"rotated_{degrees}.pdf")
79
 
80
+ try:
81
  # Save the uploaded file temporarily
82
  with open(input_path, "wb") as buffer:
83
  shutil.copyfileobj(file.file, buffer)
 
95
  with open(output_path, "wb") as output_file:
96
  pdf_writer.write(output_file)
97
 
98
+ return FileResponse(output_path, media_type="application/pdf", filename=f"rotated_{degrees}.pdf")
99
+
100
+ except Exception as e:
101
+ shutil.rmtree(temp_dir)
102
+ raise HTTPException(status_code=500, detail=str(e))
103
+
104
+
105
+ # ==========================
106
+ # 🎨 Modern UI Endpoint
107
+ # ==========================
108
+ @router.get("/ui", response_class=HTMLResponse)
109
+ async def pdf_tools_ui():
110
+ html_content = """
111
+ <!DOCTYPE html>
112
+ <html lang="en" data-theme="light">
113
+ <head>
114
+ <meta charset="UTF-8">
115
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
116
+ <title>PDF Tools | by Sam</title>
117
+ <link rel="preconnect" href="https://fonts.googleapis.com">
118
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
119
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
120
+
121
+ <style>
122
+ /* --- Shared Theme Config --- */
123
+ :root {
124
+ --primary: #ef4444; /* PDF Red */
125
+ --primary-hover: #dc2626;
126
+ --bg-body: #f8fafc;
127
+ --bg-card: #ffffff;
128
+ --text-main: #0f172a;
129
+ --text-sub: #64748b;
130
+ --border: #e2e8f0;
131
+ --drop-bg: #fef2f2;
132
+ --drop-hover: #fee2e2;
133
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
134
+ }
135
+
136
+ [data-theme="dark"] {
137
+ --primary: #f87171;
138
+ --primary-hover: #ef4444;
139
+ --bg-body: #0f172a;
140
+ --bg-card: #1e293b;
141
+ --text-main: #f8fafc;
142
+ --text-sub: #94a3b8;
143
+ --border: #334155;
144
+ --drop-bg: #1e293b;
145
+ --drop-hover: #450a0a;
146
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
147
+ }
148
+
149
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
150
+
151
+ body {
152
+ font-family: 'Plus Jakarta Sans', sans-serif;
153
+ background-color: var(--bg-body);
154
+ color: var(--text-main);
155
+ min-height: 100vh;
156
+ display: flex;
157
+ flex-direction: column;
158
+ align-items: center;
159
+ justify-content: center;
160
+ padding: 1rem;
161
+ }
162
+
163
+ /* --- Navbar --- */
164
+ .navbar {
165
+ width: 100%; max-width: 1000px; display: flex;
166
+ justify-content: space-between; align-items: center; margin-bottom: 2rem;
167
+ animation: fadeIn 0.8s ease-out;
168
+ }
169
+ .brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
170
+ .brand span { color: var(--primary); }
171
+ .theme-toggle { background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 8px; border-radius: 50%; background-color: var(--bg-card); border: 1px solid var(--border); }
172
+
173
+ /* --- Container & Tabs --- */
174
+ .container {
175
+ background: var(--bg-card); width: 100%; max-width: 800px;
176
+ border-radius: 24px; box-shadow: var(--shadow);
177
+ border: 1px solid var(--border); overflow: hidden;
178
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
179
+ }
180
+ .tabs { display: flex; border-bottom: 1px solid var(--border); }
181
+ .tab-btn {
182
+ flex: 1; padding: 1rem; border: none; background: transparent;
183
+ font-family: inherit; font-weight: 600; color: var(--text-sub);
184
+ cursor: pointer; transition: all 0.2s; border-bottom: 3px solid transparent;
185
+ }
186
+ .tab-btn:hover { background-color: var(--drop-bg); }
187
+ .tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
188
+
189
+ .tab-content { padding: 2.5rem; display: none; animation: fadeIn 0.4s ease; }
190
+ .tab-content.active { display: block; }
191
+
192
+ /* --- Controls --- */
193
+ .drop-area {
194
+ border: 2px dashed var(--border); border-radius: 16px; padding: 3rem 1rem;
195
+ text-align: center; cursor: pointer; background-color: var(--drop-bg);
196
+ transition: all 0.3s; margin-bottom: 1.5rem;
197
+ }
198
+ .drop-area:hover, .drop-area.dragover { border-color: var(--primary); background-color: var(--drop-hover); transform: scale(1.01); }
199
+ .drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; }
200
+
201
+ .file-list { margin-bottom: 1rem; text-align: left; background: var(--bg-body); padding: 1rem; border-radius: 8px; display: none; }
202
+ .file-item { font-size: 0.9rem; color: var(--text-sub); padding: 4px 0; border-bottom: 1px solid var(--border); }
203
+
204
+ .rotate-options { display: flex; gap: 1rem; justify-content: center; margin-bottom: 1.5rem; }
205
+ .rotate-btn {
206
+ padding: 0.5rem 1rem; border: 1px solid var(--border); border-radius: 8px;
207
+ background: var(--bg-body); color: var(--text-sub); cursor: pointer;
208
+ }
209
+ .rotate-btn.selected { background: var(--primary); color: white; border-color: var(--primary); }
210
+
211
+ .btn {
212
+ padding: 0.8rem 2rem; border-radius: 12px; font-weight: 600; width: 100%;
213
+ cursor: pointer; border: none; font-size: 1rem; background-color: var(--primary); color: white;
214
+ display: flex; justify-content: center; align-items: center; gap: 10px;
215
+ }
216
+ .btn:disabled { opacity: 0.6; cursor: not-allowed; }
217
+
218
+ /* --- Footer --- */
219
+ .dev-footer {
220
+ margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
221
+ padding: 1rem; border-top: 1px solid var(--border); width: 100%; max-width: 600px;
222
+ }
223
+ .dev-badge {
224
+ display: inline-block; background: var(--bg-card); padding: 5px 15px;
225
+ border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
226
+ }
227
+ .dev-name { color: var(--primary); font-weight: 700; }
228
+
229
+ .spinner { width: 20px; height: 20px; border: 2px solid #ffffff; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; display: none; }
230
+ @keyframes spin { to { transform: rotate(360deg); } }
231
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
232
+ @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
233
+
234
+ </style>
235
+ </head>
236
+ <body>
237
+
238
+ <nav class="navbar">
239
+ <div class="brand">πŸ“„ PDF Tools <span>Suite</span></div>
240
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">πŸŒ™</button>
241
+ </nav>
242
+
243
+ <div class="container">
244
+ <div class="tabs">
245
+ <button class="tab-btn active" onclick="switchTab('merge')">Merge PDFs</button>
246
+ <button class="tab-btn" onclick="switchTab('rotate')">Rotate PDF</button>
247
+ </div>
248
+
249
+ <div id="tab-merge" class="tab-content active">
250
+ <p style="text-align: center; color: var(--text-sub); margin-bottom: 1rem;">Combine multiple PDF files into one.</p>
251
+
252
+ <div class="drop-area" id="mergeDrop">
253
+ <input type="file" id="mergeInput" accept="application/pdf" multiple hidden>
254
+ <span class="drop-icon">πŸ“‘</span>
255
+ <h3 style="font-weight: 600;">Drag & Drop PDFs here</h3>
256
+ <p style="color: var(--text-sub); font-size: 0.9rem;">or click to browse</p>
257
+ </div>
258
+
259
+ <div class="file-list" id="mergeFileList"></div>
260
+
261
+ <button class="btn" id="mergeBtn" onclick="processMerge()" disabled>
262
+ <div class="spinner" id="mergeSpinner"></div>
263
+ <span id="mergeBtnText">Merge Files</span>
264
+ </button>
265
+ <p id="mergeError" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
266
+ </div>
267
+
268
+ <div id="tab-rotate" class="tab-content">
269
+ <p style="text-align: center; color: var(--text-sub); margin-bottom: 1rem;">Rotate all pages in a PDF document.</p>
270
+
271
+ <div class="drop-area" id="rotateDrop">
272
+ <input type="file" id="rotateInput" accept="application/pdf" hidden>
273
+ <span class="drop-icon">πŸ”„</span>
274
+ <h3 id="rotateText" style="font-weight: 600;">Upload PDF to Rotate</h3>
275
+ </div>
276
+
277
+ <div class="rotate-options">
278
+ <button class="rotate-btn selected" onclick="setRotation(90)">90Β° CW</button>
279
+ <button class="rotate-btn" onclick="setRotation(180)">180Β°</button>
280
+ <button class="rotate-btn" onclick="setRotation(270)">270Β° CW</button>
281
+ </div>
282
+
283
+ <button class="btn" id="rotateBtn" onclick="processRotate()" disabled>
284
+ <div class="spinner" id="rotateSpinner"></div>
285
+ <span id="rotateBtnText">Rotate PDF</span>
286
+ </button>
287
+ <p id="rotateError" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
288
+ </div>
289
+ </div>
290
+
291
+ <footer class="dev-footer">
292
+ <div>Designed & Developed by</div>
293
+ <div class="dev-badge">
294
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
295
+ </div>
296
+ </footer>
297
+
298
+ <script>
299
+ // --- Theme & Tab Logic ---
300
+ const html = document.documentElement;
301
+ const themeBtn = document.getElementById('themeBtn');
302
+ function toggleTheme() {
303
+ const current = html.getAttribute('data-theme');
304
+ const next = current === 'light' ? 'dark' : 'light';
305
+ html.setAttribute('data-theme', next);
306
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
307
+ localStorage.setItem('theme', next);
308
+ }
309
+ const savedTheme = localStorage.getItem('theme') || 'light';
310
+ html.setAttribute('data-theme', savedTheme);
311
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
312
+
313
+ function switchTab(tab) {
314
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
315
+ document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
316
+ document.getElementById('tab-' + tab).classList.add('active');
317
+ // Simple active class toggle for buttons
318
+ const btns = document.querySelectorAll('.tab-btn');
319
+ if(tab === 'merge') btns[0].classList.add('active');
320
+ else btns[1].classList.add('active');
321
+ }
322
+
323
+ // --- Merge Logic ---
324
+ const mergeDrop = document.getElementById('mergeDrop');
325
+ const mergeInput = document.getElementById('mergeInput');
326
+ const mergeFileList = document.getElementById('mergeFileList');
327
+ const mergeBtn = document.getElementById('mergeBtn');
328
+ let mergeFiles = [];
329
+
330
+ mergeDrop.addEventListener('click', () => mergeInput.click());
331
+
332
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
333
+ mergeDrop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); });
334
+ });
335
+ mergeDrop.addEventListener('drop', (e) => {
336
+ if(e.dataTransfer.files.length) handleMergeFiles(e.dataTransfer.files);
337
+ });
338
+ mergeInput.addEventListener('change', () => handleMergeFiles(mergeInput.files));
339
+
340
+ function handleMergeFiles(files) {
341
+ mergeFiles = Array.from(files); // Convert FileList to Array
342
+ if(mergeFiles.length < 2) {
343
+ alert("Please select at least 2 PDF files.");
344
+ return;
345
+ }
346
+ updateFileList();
347
+ mergeBtn.disabled = false;
348
+ }
349
+
350
+ function updateFileList() {
351
+ mergeFileList.style.display = 'block';
352
+ mergeFileList.innerHTML = mergeFiles.map((f, i) =>
353
+ `<div class="file-item">${i+1}. ${f.name} (${(f.size/1024/1024).toFixed(2)} MB)</div>`
354
+ ).join('');
355
+ }
356
+
357
+ async function processMerge() {
358
+ if(mergeFiles.length < 2) return;
359
+ setLoading('merge', true);
360
+
361
+ const formData = new FormData();
362
+ mergeFiles.forEach(file => formData.append('files', file));
363
+
364
+ try {
365
+ const res = await fetch('/pdf/merge', { method: 'POST', body: formData });
366
+ if(!res.ok) throw new Error("Merge failed");
367
+
368
+ const blob = await res.blob();
369
+ downloadBlob(blob, 'merged_document.pdf');
370
+ } catch(e) {
371
+ showError('merge', e.message);
372
+ } finally {
373
+ setLoading('merge', false);
374
+ }
375
+ }
376
+
377
+ // --- Rotate Logic ---
378
+ const rotateDrop = document.getElementById('rotateDrop');
379
+ const rotateInput = document.getElementById('rotateInput');
380
+ const rotateText = document.getElementById('rotateText');
381
+ const rotateBtn = document.getElementById('rotateBtn');
382
+ let rotateFile = null;
383
+ let currentRotation = 90;
384
+
385
+ rotateDrop.addEventListener('click', () => rotateInput.click());
386
+
387
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
388
+ rotateDrop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); });
389
+ });
390
+ rotateDrop.addEventListener('drop', (e) => {
391
+ if(e.dataTransfer.files.length) handleRotateFile(e.dataTransfer.files[0]);
392
+ });
393
+ rotateInput.addEventListener('change', () => handleRotateFile(rotateInput.files[0]));
394
+
395
+ function handleRotateFile(file) {
396
+ if(!file || file.type !== 'application/pdf') {
397
+ alert("Please select a valid PDF."); return;
398
+ }
399
+ rotateFile = file;
400
+ rotateText.textContent = "Selected: " + file.name;
401
+ rotateBtn.disabled = false;
402
+ }
403
+
404
+ function setRotation(deg) {
405
+ currentRotation = deg;
406
+ document.querySelectorAll('.rotate-btn').forEach(b => b.classList.remove('selected'));
407
+ // Identify button by onclick text content or order.
408
+ // Hardcoding logic for simplicity:
409
+ const btns = document.querySelectorAll('.rotate-btn');
410
+ if(deg === 90) btns[0].classList.add('selected');
411
+ if(deg === 180) btns[1].classList.add('selected');
412
+ if(deg === 270) btns[2].classList.add('selected');
413
+ }
414
+
415
+ async function processRotate() {
416
+ if(!rotateFile) return;
417
+ setLoading('rotate', true);
418
+
419
+ const formData = new FormData();
420
+ formData.append('file', rotateFile);
421
+
422
+ try {
423
+ const res = await fetch(`/pdf/rotate?degrees=${currentRotation}`, {
424
+ method: 'POST',
425
+ body: formData
426
+ });
427
+ if(!res.ok) throw new Error("Rotation failed");
428
+
429
+ const blob = await res.blob();
430
+ downloadBlob(blob, `rotated_${currentRotation}_${rotateFile.name}`);
431
+ } catch(e) {
432
+ showError('rotate', e.message);
433
+ } finally {
434
+ setLoading('rotate', false);
435
+ }
436
+ }
437
+
438
+ // --- Helpers ---
439
+ function downloadBlob(blob, filename) {
440
+ const url = window.URL.createObjectURL(blob);
441
+ const a = document.createElement('a');
442
+ a.href = url;
443
+ a.download = filename;
444
+ document.body.appendChild(a);
445
+ a.click();
446
+ a.remove();
447
+ }
448
+
449
+ function setLoading(type, isLoading) {
450
+ const btn = document.getElementById(`${type}Btn`);
451
+ const spinner = document.getElementById(`${type}Spinner`);
452
+ const text = document.getElementById(`${type}BtnText`);
453
+ const error = document.getElementById(`${type}Error`);
454
+
455
+ error.style.display = 'none';
456
+ if(isLoading) {
457
+ btn.disabled = true;
458
+ spinner.style.display = 'block';
459
+ text.textContent = 'Processing...';
460
+ } else {
461
+ btn.disabled = false;
462
+ spinner.style.display = 'none';
463
+ text.textContent = type === 'merge' ? 'Merge Files' : 'Rotate PDF';
464
+ }
465
+ }
466
+
467
+ function showError(type, msg) {
468
+ const el = document.getElementById(`${type}Error`);
469
+ el.textContent = "Error: " + msg;
470
+ el.style.display = 'block';
471
+ }
472
+ </script>
473
+ </body>
474
+ </html>
475
+ """
476
+ return html_content
app/routers/security/password_generator.py CHANGED
@@ -1,44 +1,432 @@
1
  import random
2
  import string
3
- from fastapi import APIRouter, Query, Depends # Added 'Depends'
4
- from app.routers.auth.system import get_current_user # Import our Auth System
 
5
 
6
- # Define the APIRouter object as per documentation standards
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  router = APIRouter(
8
- prefix="/security", # Endpoints will start with /security
9
  tags=["Password Tools"],
10
  )
11
 
 
 
 
12
  @router.get("/generate-password")
13
  def generate_password(
14
- length: int = Query(12, ge=8, le=64, description="Length of the password (8-64)"),
15
  include_uppercase: bool = True,
 
16
  include_numbers: bool = True,
17
  include_symbols: bool = True,
18
- # πŸ‘‡ This line locks the endpoint!
19
- current_user: dict = Depends(get_current_user)
20
  ):
21
  """
22
- Generates a secure, random password.
23
- **Requires Authentication.**
24
  """
25
- # Base character set
26
- chars = string.ascii_lowercase
27
-
28
- # Advanced logic to append character sets based on flags
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  if include_uppercase:
30
- chars += string.ascii_uppercase
 
31
  if include_numbers:
32
- chars += string.digits
 
33
  if include_symbols:
34
- chars += string.punctuation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- # Generate the password
37
- generated_password = "".join(random.choice(chars) for _ in range(length))
38
 
39
  return {
40
- "requested_by": current_user.username, # We can now see who asked for it!
41
- "password": generated_password,
42
  "length": length,
43
- "complexity": "Advanced"
44
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import random
2
  import string
3
+ from fastapi import APIRouter, Query, Depends, HTTPException
4
+ from fastapi.responses import HTMLResponse
5
+ from typing import Optional
6
 
7
+ # ==========================
8
+ # πŸ” Auth & Configuration
9
+ # ==========================
10
+ # Try to import the auth system, otherwise mock it for standalone use
11
+ try:
12
+ from app.routers.auth.system import get_current_user
13
+ except ImportError:
14
+ def get_current_user(): return {"username": "Anonymous"}
15
+
16
+ AUTH_ENABLED = False # Set to True to enforce login
17
+
18
+ def auth_dependency():
19
+ return Depends(get_current_user) if AUTH_ENABLED else None
20
+
21
+ # ==========================
22
+ # πŸ› οΈ Router Setup
23
+ # ==========================
24
  router = APIRouter(
25
+ prefix="/security",
26
  tags=["Password Tools"],
27
  )
28
 
29
+ # ==========================
30
+ # 🎲 Password Logic (API)
31
+ # ==========================
32
  @router.get("/generate-password")
33
  def generate_password(
34
+ length: int = Query(16, ge=8, le=128, description="Length of the password"),
35
  include_uppercase: bool = True,
36
+ include_lowercase: bool = True,
37
  include_numbers: bool = True,
38
  include_symbols: bool = True,
39
+ exclude_ambiguous: bool = False,
40
+ current_user: dict = auth_dependency()
41
  ):
42
  """
43
+ Generates a cryptographically strong random password with advanced options.
 
44
  """
45
+ # Character Sets
46
+ lower = string.ascii_lowercase
47
+ upper = string.ascii_uppercase
48
+ nums = string.digits
49
+ syms = "!@#$%^&*()_+-=[]{}|;:,.<>?"
50
+ ambiguous = "il1Lo0O"
51
+
52
+ # Filter ambiguous characters if requested
53
+ if exclude_ambiguous:
54
+ lower = "".join([c for c in lower if c not in ambiguous])
55
+ upper = "".join([c for c in upper if c not in ambiguous])
56
+ nums = "".join([c for c in nums if c not in ambiguous])
57
+ syms = "".join([c for c in syms if c not in ambiguous])
58
+
59
+ # Build Pool
60
+ pool = ""
61
+ guaranteed_chars = []
62
+
63
+ if include_lowercase:
64
+ pool += lower
65
+ guaranteed_chars.append(random.choice(lower))
66
  if include_uppercase:
67
+ pool += upper
68
+ guaranteed_chars.append(random.choice(upper))
69
  if include_numbers:
70
+ pool += nums
71
+ guaranteed_chars.append(random.choice(nums))
72
  if include_symbols:
73
+ pool += syms
74
+ guaranteed_chars.append(random.choice(syms))
75
+
76
+ if not pool:
77
+ raise HTTPException(status_code=400, detail="At least one character type must be selected.")
78
+
79
+ # Generate remaining characters
80
+ remaining_length = length - len(guaranteed_chars)
81
+ if remaining_length < 0:
82
+ password_chars = guaranteed_chars[:length]
83
+ else:
84
+ password_chars = guaranteed_chars + [random.choice(pool) for _ in range(remaining_length)]
85
+
86
+ # Shuffle to remove predictable patterns
87
+ random.shuffle(password_chars)
88
+ final_password = "".join(password_chars)
89
+
90
+ # Calculate basic entropy/strength
91
+ pool_size = len(pool)
92
+ entropy = length * (len(bin(pool_size)) - 2)
93
+
94
+ strength = "Weak"
95
+ if entropy > 50: strength = "Medium"
96
+ if entropy > 80: strength = "Strong"
97
+ if entropy > 120: strength = "Very Strong"
98
 
99
+ user_info = current_user.get("username", "Anonymous") if current_user else "Anonymous"
 
100
 
101
  return {
102
+ "requested_by": user_info,
103
+ "password": final_password,
104
  "length": length,
105
+ "strength": strength,
106
+ "entropy_bits": int(entropy)
107
+ }
108
+
109
+
110
+ # ==========================
111
+ # 🎨 Modern UI Endpoint
112
+ # ==========================
113
+ # Path becomes: /security/password-generator/ui
114
+ @router.get("/password-generator/ui", response_class=HTMLResponse)
115
+ async def password_ui():
116
+ html_content = """
117
+ <!DOCTYPE html>
118
+ <html lang="en" data-theme="light">
119
+ <head>
120
+ <meta charset="UTF-8">
121
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
122
+ <title>Password Tool</title>
123
+ <link rel="preconnect" href="https://fonts.googleapis.com">
124
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
125
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
126
+
127
+ <style>
128
+ :root {
129
+ --primary: #6366f1;
130
+ --primary-hover: #4f46e5;
131
+ --bg-body: #f8fafc;
132
+ --bg-card: #ffffff;
133
+ --text-main: #0f172a;
134
+ --text-sub: #64748b;
135
+ --border: #e2e8f0;
136
+ --input-bg: #f1f5f9;
137
+ --success: #10b981;
138
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
139
+ }
140
+
141
+ [data-theme="dark"] {
142
+ --primary: #818cf8;
143
+ --primary-hover: #6366f1;
144
+ --bg-body: #0f172a;
145
+ --bg-card: #1e293b;
146
+ --text-main: #f8fafc;
147
+ --text-sub: #94a3b8;
148
+ --border: #334155;
149
+ --input-bg: #0f172a;
150
+ --success: #34d399;
151
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
152
+ }
153
+
154
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s, color 0.3s; }
155
+
156
+ body {
157
+ font-family: 'Plus Jakarta Sans', sans-serif;
158
+ background-color: var(--bg-body);
159
+ color: var(--text-main);
160
+ min-height: 100vh;
161
+ display: flex;
162
+ flex-direction: column;
163
+ align-items: center;
164
+ justify-content: center;
165
+ padding: 1rem;
166
+ }
167
+
168
+ /* Navbar */
169
+ .navbar {
170
+ width: 100%; max-width: 600px; display: flex;
171
+ justify-content: space-between; align-items: center; margin-bottom: 2rem;
172
+ }
173
+ .brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
174
+ .brand span { color: var(--primary); }
175
+ .theme-toggle { background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 8px; border-radius: 50%; background-color: var(--bg-card); border: 1px solid var(--border); }
176
+
177
+ /* Card */
178
+ .container {
179
+ background: var(--bg-card); width: 100%; max-width: 500px;
180
+ border-radius: 24px; box-shadow: var(--shadow);
181
+ border: 1px solid var(--border); padding: 2rem;
182
+ }
183
+
184
+ /* Password Display */
185
+ .password-box {
186
+ background: var(--input-bg);
187
+ border-radius: 12px;
188
+ padding: 1.5rem;
189
+ margin-bottom: 2rem;
190
+ position: relative;
191
+ border: 1px solid var(--border);
192
+ display: flex; align-items: center; justify-content: space-between;
193
+ }
194
+ .password-text {
195
+ font-family: 'JetBrains Mono', monospace;
196
+ font-size: 1.5rem;
197
+ word-break: break-all;
198
+ color: var(--text-main);
199
+ font-weight: 500;
200
+ }
201
+ .copy-btn {
202
+ background: var(--bg-card);
203
+ border: 1px solid var(--border);
204
+ border-radius: 8px;
205
+ padding: 0.5rem;
206
+ cursor: pointer;
207
+ color: var(--text-sub);
208
+ transition: all 0.2s;
209
+ margin-left: 1rem; flex-shrink: 0;
210
+ }
211
+ .copy-btn:hover { border-color: var(--primary); color: var(--primary); }
212
+ .copy-btn.copied { background-color: var(--success); color: white; border-color: var(--success); }
213
+
214
+ /* Controls */
215
+ .controls { display: grid; gap: 1rem; }
216
+
217
+ .range-group { margin-bottom: 1rem; }
218
+ .range-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-weight: 600; font-size: 0.9rem; }
219
+ input[type="range"] { width: 100%; cursor: pointer; accent-color: var(--primary); }
220
+
221
+ .options-grid {
222
+ display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem;
223
+ }
224
+ .checkbox-wrapper {
225
+ display: flex; align-items: center; gap: 0.5rem;
226
+ font-size: 0.9rem; color: var(--text-sub);
227
+ cursor: pointer;
228
+ }
229
+ input[type="checkbox"] { width: 1.2rem; height: 1.2rem; accent-color: var(--primary); cursor: pointer; }
230
+
231
+ /* Action Button */
232
+ .btn-generate {
233
+ width: 100%;
234
+ margin-top: 1.5rem;
235
+ padding: 1rem;
236
+ border-radius: 12px;
237
+ background-color: var(--primary);
238
+ color: white;
239
+ font-weight: 700;
240
+ font-size: 1rem;
241
+ border: none;
242
+ cursor: pointer;
243
+ transition: transform 0.1s, background-color 0.2s;
244
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
245
+ }
246
+ .btn-generate:hover { background-color: var(--primary-hover); transform: translateY(-2px); }
247
+ .btn-generate:active { transform: scale(0.98); }
248
+
249
+ /* Footer */
250
+ .dev-footer {
251
+ margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
252
+ padding: 1rem; width: 100%;
253
+ }
254
+ .dev-badge {
255
+ display: inline-block; background: var(--bg-card); padding: 5px 15px;
256
+ border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
257
+ }
258
+ .dev-name { color: var(--primary); font-weight: 700; }
259
+
260
+ .strength-tag {
261
+ font-size: 0.75rem; padding: 2px 8px; border-radius: 4px;
262
+ background: #e2e8f0; color: #64748b; font-weight: 600; margin-top: 5px; display: inline-block;
263
+ }
264
+ </style>
265
+ </head>
266
+ <body>
267
+
268
+ <nav class="navbar">
269
+ <div class="brand">πŸ” password generator</div>
270
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">πŸŒ™</button>
271
+ </nav>
272
+
273
+ <div class="container">
274
+ <div class="password-box">
275
+ <div style="width: 100%;">
276
+ <div id="passwordResult" class="password-text">...</div>
277
+ <div id="strengthLabel" class="strength-tag">Strength: -</div>
278
+ </div>
279
+ <button class="copy-btn" id="copyBtn" onclick="copyPassword()" title="Copy to Clipboard">
280
+ πŸ“‹
281
+ </button>
282
+ </div>
283
+
284
+ <div class="controls">
285
+
286
+ <div class="range-group">
287
+ <div class="range-header">
288
+ <label>Password Length</label>
289
+ <span id="lengthVal" style="color: var(--primary);">16</span>
290
+ </div>
291
+ <input type="range" id="lengthInput" min="8" max="64" value="16" oninput="updateLength(this.value)">
292
+ </div>
293
+
294
+ <div class="options-grid">
295
+ <label class="checkbox-wrapper">
296
+ <input type="checkbox" id="chkUpper" checked> Uppercase (A-Z)
297
+ </label>
298
+ <label class="checkbox-wrapper">
299
+ <input type="checkbox" id="chkLower" checked> Lowercase (a-z)
300
+ </label>
301
+ <label class="checkbox-wrapper">
302
+ <input type="checkbox" id="chkNumbers" checked> Numbers (0-9)
303
+ </label>
304
+ <label class="checkbox-wrapper">
305
+ <input type="checkbox" id="chkSymbols" checked> Symbols (!@#)
306
+ </label>
307
+ <label class="checkbox-wrapper">
308
+ <input type="checkbox" id="chkAmbiguous"> Exclude Ambiguous
309
+ </label>
310
+ </div>
311
+
312
+ <button class="btn-generate" onclick="generate()">⚑ Generate Password</button>
313
+ </div>
314
+ </div>
315
+
316
+ <footer class="dev-footer">
317
+ <div>Designed & Developed by</div>
318
+ <div class="dev-badge">
319
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
320
+ </div>
321
+ </footer>
322
+
323
+ <script>
324
+ // --- Theme Logic ---
325
+ const html = document.documentElement;
326
+ const themeBtn = document.getElementById('themeBtn');
327
+ function toggleTheme() {
328
+ const current = html.getAttribute('data-theme');
329
+ const next = current === 'light' ? 'dark' : 'light';
330
+ html.setAttribute('data-theme', next);
331
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
332
+ localStorage.setItem('theme', next);
333
+ }
334
+ const savedTheme = localStorage.getItem('theme') || 'light';
335
+ html.setAttribute('data-theme', savedTheme);
336
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
337
+
338
+ // --- UI Logic ---
339
+ function updateLength(val) {
340
+ document.getElementById('lengthVal').textContent = val;
341
+ }
342
+
343
+ async function generate() {
344
+ const length = document.getElementById('lengthInput').value;
345
+ const upper = document.getElementById('chkUpper').checked;
346
+ const lower = document.getElementById('chkLower').checked;
347
+ const nums = document.getElementById('chkNumbers').checked;
348
+ const syms = document.getElementById('chkSymbols').checked;
349
+ const ambig = document.getElementById('chkAmbiguous').checked;
350
+
351
+ // Validation
352
+ if(!upper && !lower && !nums && !syms) {
353
+ alert("Please select at least one character type.");
354
+ return;
355
+ }
356
+
357
+ const params = new URLSearchParams({
358
+ length: length,
359
+ include_uppercase: upper,
360
+ include_lowercase: lower,
361
+ include_numbers: nums,
362
+ include_symbols: syms,
363
+ exclude_ambiguous: ambig
364
+ });
365
+
366
+ try {
367
+ const res = await fetch(`/security/generate-password?${params}`);
368
+ const data = await res.json();
369
+
370
+ if(res.ok) {
371
+ const pwd = data.password;
372
+ const strength = data.strength;
373
+
374
+ // Animate text change
375
+ const resultEl = document.getElementById('passwordResult');
376
+ resultEl.style.opacity = 0;
377
+ setTimeout(() => {
378
+ resultEl.textContent = pwd;
379
+ document.getElementById('strengthLabel').textContent = `Strength: ${strength}`;
380
+ updateStrengthColor(strength);
381
+ resultEl.style.opacity = 1;
382
+ }, 150);
383
+
384
+ // Reset copy button
385
+ const copyBtn = document.getElementById('copyBtn');
386
+ copyBtn.classList.remove('copied');
387
+ copyBtn.textContent = "πŸ“‹";
388
+ } else {
389
+ alert(data.detail || "Error generating password");
390
+ }
391
+ } catch(e) {
392
+ console.error(e);
393
+ alert("Network error");
394
+ }
395
+ }
396
+
397
+ function updateStrengthColor(strength) {
398
+ const badge = document.getElementById('strengthLabel');
399
+ let color = "#64748b";
400
+ let bg = "#e2e8f0";
401
+
402
+ if(strength === "Strong") { color = "#10b981"; bg = "#d1fae5"; }
403
+ if(strength === "Very Strong") { color = "#6366f1"; bg = "#e0e7ff"; }
404
+ if(strength === "Weak") { color = "#ef4444"; bg = "#fee2e2"; }
405
+
406
+ badge.style.color = color;
407
+ badge.style.backgroundColor = bg;
408
+ }
409
+
410
+ function copyPassword() {
411
+ const text = document.getElementById('passwordResult').textContent;
412
+ if(text === "...") return;
413
+
414
+ navigator.clipboard.writeText(text).then(() => {
415
+ const btn = document.getElementById('copyBtn');
416
+ btn.classList.add('copied');
417
+ btn.textContent = "βœ…";
418
+
419
+ setTimeout(() => {
420
+ btn.classList.remove('copied');
421
+ btn.textContent = "πŸ“‹";
422
+ }, 2000);
423
+ });
424
+ }
425
+
426
+ // Generate one on load
427
+ window.onload = generate;
428
+ </script>
429
+ </body>
430
+ </html>
431
+ """
432
+ return html_content
app/routers/testers/help.py CHANGED
@@ -1,22 +1,245 @@
1
- from fastapi import APIRouter , Depends
2
- from app.routers.auth.system import get_current_user # Import our Auth System
3
 
4
- # Create the router for help/info endpoints
 
 
5
  router = APIRouter(
6
  prefix="/help",
7
  tags=["Information"]
8
  )
9
 
 
 
 
10
  @router.get("/")
11
- def get_api_info(current_user: dict = Depends(get_current_user) ):
12
  """
13
  Returns general information about the API and available endpoints.
 
14
  """
15
  return {
16
- "requested_by": current_user.username,
17
- "api_name": "My Awesome FastAPI",
18
- "version": "1.0",
19
- "available_sections": ["/random", "/help"],
20
- "message": "Use the /docs endpoint for interactive API documentation."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+ from fastapi.responses import HTMLResponse
3
 
4
+ # ==========================
5
+ # πŸ› οΈ Router Setup
6
+ # ==========================
7
  router = APIRouter(
8
  prefix="/help",
9
  tags=["Information"]
10
  )
11
 
12
+ # ==========================
13
+ # ℹ️ API Info Endpoint
14
+ # ==========================
15
  @router.get("/")
16
+ def get_api_info():
17
  """
18
  Returns general information about the API and available endpoints.
19
+ No authentication required.
20
  """
21
  return {
22
+ "requested_by": "Guest",
23
+ "api_name": "Easy Tools API",
24
+ "developer": "Sameer Banchhor",
25
+ "version": "1.0.0",
26
+ "available_tools": [
27
+ "/image (Compressor & BG Remover)",
28
+ "/pdf (Merge & Rotate)",
29
+ "/drive (File Storage & Aria2)",
30
+ "/security (Password Generator)",
31
+ "/server-status (System Monitor)"
32
+ ],
33
+ "message": "Use the /help/ui endpoint to access the visual dashboard.",
34
+ "developer_info": "Created by Sameer Banchhor, Data Scientist (MSc in System Design, Kalyan PG College).",
35
+ "git_link": "https://github.com/sameerbanchhor-git"
36
+ }
37
+
38
+ # ==========================
39
+ # 🧭 Central Hub UI
40
+ # ==========================
41
+ @router.get("/ui", response_class=HTMLResponse)
42
+ async def help_ui():
43
+ html_content = """
44
+ <!DOCTYPE html>
45
+ <html lang="en" data-theme="light">
46
+ <head>
47
+ <meta charset="UTF-8">
48
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
49
+ <title>Easy Tools Hub | by Sam</title>
50
+ <link rel="preconnect" href="https://fonts.googleapis.com">
51
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
52
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
53
 
54
+ <style>
55
+ :root {
56
+ --primary: #6366f1;
57
+ --primary-hover: #4f46e5;
58
+ --bg-body: #f8fafc;
59
+ --bg-card: #ffffff;
60
+ --text-main: #0f172a;
61
+ --text-sub: #64748b;
62
+ --border: #e2e8f0;
63
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
64
+ --gradient-1: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
65
+ }
66
+
67
+ [data-theme="dark"] {
68
+ --primary: #818cf8;
69
+ --primary-hover: #6366f1;
70
+ --bg-body: #0f172a;
71
+ --bg-card: #1e293b;
72
+ --text-main: #f8fafc;
73
+ --text-sub: #94a3b8;
74
+ --border: #334155;
75
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
76
+ --gradient-1: linear-gradient(135deg, #4f46e5 0%, #7e22ce 100%);
77
+ }
78
+
79
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: all 0.3s ease; }
80
+
81
+ body {
82
+ font-family: 'Plus Jakarta Sans', sans-serif;
83
+ background-color: var(--bg-body);
84
+ color: var(--text-main);
85
+ min-height: 100vh;
86
+ display: flex;
87
+ flex-direction: column;
88
+ align-items: center;
89
+ padding: 2rem 1rem;
90
+ }
91
+
92
+ /* Header */
93
+ .header-container {
94
+ text-align: center; margin-bottom: 3rem; animation: fadeIn 0.8s ease-out;
95
+ }
96
+ .title {
97
+ font-size: 2.5rem; font-weight: 800; margin-bottom: 0.5rem;
98
+ background: var(--gradient-1); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
99
+ }
100
+ .subtitle { color: var(--text-sub); font-size: 1.1rem; }
101
+
102
+ .theme-toggle {
103
+ position: absolute; top: 1rem; right: 1rem;
104
+ background: var(--bg-card); border: 1px solid var(--border);
105
+ color: var(--text-main); padding: 10px; border-radius: 50%; cursor: pointer;
106
+ }
107
+
108
+ /* Grid */
109
+ .tools-grid {
110
+ display: grid;
111
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
112
+ gap: 1.5rem;
113
+ width: 100%; max-width: 1000px;
114
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
115
+ }
116
+
117
+ /* Cards */
118
+ .tool-card {
119
+ background: var(--bg-card);
120
+ border-radius: 20px;
121
+ padding: 2rem;
122
+ border: 1px solid var(--border);
123
+ box-shadow: var(--shadow);
124
+ text-decoration: none;
125
+ color: var(--text-main);
126
+ display: flex; flex-direction: column; align-items: center; text-align: center;
127
+ transition: transform 0.2s, box-shadow 0.2s;
128
+ position: relative; overflow: hidden;
129
+ }
130
+ .tool-card:hover {
131
+ transform: translateY(-5px);
132
+ box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 10px 10px -5px rgb(0 0 0 / 0.04);
133
+ border-color: var(--primary);
134
+ }
135
+
136
+ .icon { font-size: 3rem; margin-bottom: 1rem; }
137
+ .card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 0.5rem; }
138
+ .card-desc { font-size: 0.9rem; color: var(--text-sub); line-height: 1.5; }
139
+
140
+ /* Footer */
141
+ .dev-footer {
142
+ margin-top: 4rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
143
+ padding: 2rem; width: 100%; max-width: 600px; border-top: 1px solid var(--border);
144
+ animation: fadeIn 1.2s ease-out;
145
+ }
146
+ .dev-badge {
147
+ display: inline-block; background: var(--bg-card); padding: 8px 20px;
148
+ border-radius: 30px; border: 1px solid var(--border); font-weight: 600; margin-top: 10px;
149
+ }
150
+ .dev-name { color: var(--primary); font-weight: 800; }
151
+ .github-link {
152
+ display: inline-flex; align-items: center; gap: 8px; margin-top: 15px;
153
+ color: var(--text-main); text-decoration: none; font-weight: 600;
154
+ padding: 8px 16px; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border);
155
+ }
156
+ .github-link:hover { background: var(--bg-body); }
157
+
158
+ @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
159
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
160
+ </style>
161
+ </head>
162
+ <body>
163
+
164
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">πŸŒ™</button>
165
+
166
+ <div class="header-container">
167
+ <div class="title">Easy Tools Suite</div>
168
+ <div class="subtitle">All your essential utilities in one place.</div>
169
+ </div>
170
+
171
+ <div class="tools-grid">
172
+
173
+ <a href="/image/ui" class="tool-card">
174
+ <div class="icon">πŸ–ΌοΈ</div>
175
+ <div class="card-title">Image Tools</div>
176
+ <div class="card-desc">Compress JPEGs to specific sizes & remove backgrounds instantly using AI.</div>
177
+ </a>
178
+
179
+ <a href="/pdf/ui" class="tool-card">
180
+ <div class="icon">πŸ“„</div>
181
+ <div class="card-title">PDF Master</div>
182
+ <div class="card-desc">Merge multiple documents or rotate pages with a simple drag & drop interface.</div>
183
+ </a>
184
+
185
+ <a href="/drive/storage/ui" class="tool-card">
186
+ <div class="icon">☁️</div>
187
+ <div class="card-title">Cloud Drive</div>
188
+ <div class="card-desc">Upload files locally or use Aria2 for high-speed remote server downloads.</div>
189
+ </a>
190
+
191
+ <a href="/security/password-generator/ui" class="tool-card">
192
+ <div class="icon">πŸ”</div>
193
+ <div class="card-title">Security</div>
194
+ <div class="card-desc">Generate cryptographically strong passwords with advanced custom rules.</div>
195
+ </a>
196
+
197
+ <a href="/server-status/ui" class="tool-card">
198
+ <div class="icon">πŸ“Š</div>
199
+ <div class="card-title">Server Monitor</div>
200
+ <div class="card-desc">Real-time dashboard for CPU, RAM, Disk usage, and Network traffic.</div>
201
+ </a>
202
+
203
+ <a href="/docs" class="tool-card" style="border-style: dashed;">
204
+ <div class="icon">⚑</div>
205
+ <div class="card-title">API Docs</div>
206
+ <div class="card-desc">Explore the raw Swagger UI for testing API endpoints directly.</div>
207
+ </a>
208
+
209
+ </div>
210
+
211
+ <footer class="dev-footer">
212
+ <div>Designed & Developed by</div>
213
+ <div class="dev-badge">
214
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
215
+ </div>
216
+ <br>
217
+ <div style="font-size: 0.85rem; margin-top: 5px; opacity: 0.8;">MSc in System Design, Kalyan PG College</div>
218
+
219
+ <a href="https://github.com/sameerbanchhor-git" target="_blank" class="github-link">
220
+ <svg height="20" width="20" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
221
+ GitHub Profile
222
+ </a>
223
+ </footer>
224
+
225
+ <script>
226
+ // Theme Logic
227
+ const html = document.documentElement;
228
+ const themeBtn = document.getElementById('themeBtn');
229
+
230
+ function toggleTheme() {
231
+ const current = html.getAttribute('data-theme');
232
+ const next = current === 'light' ? 'dark' : 'light';
233
+ html.setAttribute('data-theme', next);
234
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
235
+ localStorage.setItem('theme', next);
236
+ }
237
+
238
+ const savedTheme = localStorage.getItem('theme') || 'light';
239
+ html.setAttribute('data-theme', savedTheme);
240
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
241
+ </script>
242
+ </body>
243
+ </html>
244
+ """
245
+ return html_content
app/routers/testers/server_status.py CHANGED
@@ -1,5 +1,5 @@
1
  from fastapi import APIRouter, status
2
- from fastapi.responses import JSONResponse
3
  import platform
4
  import psutil
5
 
@@ -10,6 +10,9 @@ router = APIRouter(
10
  responses={404: {"description": "Not found"}},
11
  )
12
 
 
 
 
13
  @router.get(
14
  "/full-info",
15
  summary="Get complete server/PC status and information",
@@ -31,11 +34,14 @@ def get_server_status():
31
  }
32
 
33
  # --- CPU Information ---
 
 
 
34
  cpu_info = {
35
  "physical_cores": psutil.cpu_count(logical=False),
36
  "total_cores": psutil.cpu_count(logical=True),
37
- "cpu_usage_percent": psutil.cpu_percent(interval=1),
38
- "cpu_frequency_mhz": f"{psutil.cpu_freq().current:.2f}",
39
  }
40
 
41
  # --- Memory Information (RAM) ---
@@ -48,18 +54,22 @@ def get_server_status():
48
  }
49
 
50
  # --- Disk Usage Information (Root partition) ---
51
- disk_usage = psutil.disk_usage('/')
52
- disk_info = {
53
- "total_gb": f"{disk_usage.total / (1024**3):.2f}",
54
- "used_gb": f"{disk_usage.used / (1024**3):.2f}",
55
- "free_gb": f"{disk_usage.free / (1024**3):.2f}",
56
- "usage_percent": disk_usage.percent,
57
- }
 
 
 
58
 
59
  # --- Network Information (Simple) ---
 
60
  network_info = {
61
- "bytes_sent_mb": f"{psutil.net_io_counters().bytes_sent / (1024**2):.2f}",
62
- "bytes_recv_mb": f"{psutil.net_io_counters().bytes_recv / (1024**2):.2f}",
63
  }
64
 
65
  response_data = {
@@ -70,4 +80,294 @@ def get_server_status():
70
  "network_io": network_info,
71
  }
72
 
73
- return JSONResponse(content=response_data, status_code=status.HTTP_200_OK)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import APIRouter, status
2
+ from fastapi.responses import JSONResponse, HTMLResponse
3
  import platform
4
  import psutil
5
 
 
10
  responses={404: {"description": "Not found"}},
11
  )
12
 
13
+ # ==========================
14
+ # πŸ“Š API Endpoint
15
+ # ==========================
16
  @router.get(
17
  "/full-info",
18
  summary="Get complete server/PC status and information",
 
34
  }
35
 
36
  # --- CPU Information ---
37
+ # Note: cpu_percent(interval=None) is non-blocking but requires a previous call
38
+ # or it returns 0.0 on first call. For an API, interval=0.1 is a good compromise.
39
+ freq = psutil.cpu_freq()
40
  cpu_info = {
41
  "physical_cores": psutil.cpu_count(logical=False),
42
  "total_cores": psutil.cpu_count(logical=True),
43
+ "cpu_usage_percent": psutil.cpu_percent(interval=0.1),
44
+ "cpu_frequency_mhz": f"{freq.current:.2f}" if freq else "N/A",
45
  }
46
 
47
  # --- Memory Information (RAM) ---
 
54
  }
55
 
56
  # --- Disk Usage Information (Root partition) ---
57
+ try:
58
+ disk_usage = psutil.disk_usage('/')
59
+ disk_info = {
60
+ "total_gb": f"{disk_usage.total / (1024**3):.2f}",
61
+ "used_gb": f"{disk_usage.used / (1024**3):.2f}",
62
+ "free_gb": f"{disk_usage.free / (1024**3):.2f}",
63
+ "usage_percent": disk_usage.percent,
64
+ }
65
+ except Exception:
66
+ disk_info = {"error": "Could not read disk usage"}
67
 
68
  # --- Network Information (Simple) ---
69
+ net_io = psutil.net_io_counters()
70
  network_info = {
71
+ "bytes_sent_mb": f"{net_io.bytes_sent / (1024**2):.2f}",
72
+ "bytes_recv_mb": f"{net_io.bytes_recv / (1024**2):.2f}",
73
  }
74
 
75
  response_data = {
 
80
  "network_io": network_info,
81
  }
82
 
83
+ return JSONResponse(content=response_data, status_code=status.HTTP_200_OK)
84
+
85
+
86
+ # ==========================
87
+ # πŸ–₯️ Dashboard UI Endpoint
88
+ # ==========================
89
+ @router.get("/ui", response_class=HTMLResponse)
90
+ async def server_status_ui():
91
+ html_content = """
92
+ <!DOCTYPE html>
93
+ <html lang="en" data-theme="light">
94
+ <head>
95
+ <meta charset="UTF-8">
96
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
+ <title>Server Monitor | by Sam</title>
98
+ <link rel="preconnect" href="https://fonts.googleapis.com">
99
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
100
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
101
+
102
+ <style>
103
+ :root {
104
+ --primary: #3b82f6;
105
+ --primary-hover: #2563eb;
106
+ --bg-body: #f8fafc;
107
+ --bg-card: #ffffff;
108
+ --text-main: #0f172a;
109
+ --text-sub: #64748b;
110
+ --border: #e2e8f0;
111
+ --shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
112
+ --success: #10b981;
113
+ --warning: #f59e0b;
114
+ --danger: #ef4444;
115
+ }
116
+
117
+ [data-theme="dark"] {
118
+ --primary: #60a5fa;
119
+ --bg-body: #0f172a;
120
+ --bg-card: #1e293b;
121
+ --text-main: #f8fafc;
122
+ --text-sub: #94a3b8;
123
+ --border: #334155;
124
+ --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
125
+ }
126
+
127
+ * { box-sizing: border-box; margin: 0; padding: 0; transition: all 0.3s ease; }
128
+
129
+ body {
130
+ font-family: 'Plus Jakarta Sans', sans-serif;
131
+ background-color: var(--bg-body);
132
+ color: var(--text-main);
133
+ min-height: 100vh;
134
+ display: flex;
135
+ flex-direction: column;
136
+ align-items: center;
137
+ padding: 2rem 1rem;
138
+ }
139
+
140
+ .navbar {
141
+ width: 100%; max-width: 1000px; display: flex;
142
+ justify-content: space-between; align-items: center; margin-bottom: 2rem;
143
+ }
144
+ .brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
145
+ .brand span { color: var(--primary); }
146
+
147
+ .theme-toggle {
148
+ background: var(--bg-card); border: 1px solid var(--border);
149
+ color: var(--text-main); padding: 8px; border-radius: 50%; cursor: pointer;
150
+ }
151
+
152
+ /* Grid Layout */
153
+ .dashboard {
154
+ display: grid;
155
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
156
+ gap: 1.5rem;
157
+ width: 100%;
158
+ max-width: 1000px;
159
+ }
160
+
161
+ .card {
162
+ background: var(--bg-card);
163
+ border-radius: 16px;
164
+ padding: 1.5rem;
165
+ border: 1px solid var(--border);
166
+ box-shadow: var(--shadow);
167
+ }
168
+
169
+ .card-header {
170
+ font-size: 0.9rem; font-weight: 600; color: var(--text-sub);
171
+ text-transform: uppercase; letter-spacing: 0.5px;
172
+ margin-bottom: 1rem; display: flex; justify-content: space-between;
173
+ }
174
+
175
+ .stat-value {
176
+ font-size: 2rem; font-weight: 700; color: var(--text-main);
177
+ font-family: 'JetBrains Mono', monospace;
178
+ }
179
+
180
+ .stat-sub { font-size: 0.9rem; color: var(--text-sub); margin-top: 0.2rem; }
181
+
182
+ /* Progress Bars */
183
+ .progress-container {
184
+ height: 12px; background: var(--border); border-radius: 6px;
185
+ margin-top: 1rem; overflow: hidden;
186
+ }
187
+ .progress-bar {
188
+ height: 100%; width: 0%; background: var(--primary);
189
+ border-radius: 6px; transition: width 0.5s ease-in-out;
190
+ }
191
+
192
+ /* System List */
193
+ .sys-list { list-style: none; }
194
+ .sys-list li {
195
+ display: flex; justify-content: space-between;
196
+ padding: 0.5rem 0; border-bottom: 1px solid var(--border);
197
+ font-size: 0.9rem;
198
+ }
199
+ .sys-list li:last-child { border-bottom: none; }
200
+ .sys-label { color: var(--text-sub); }
201
+ .sys-val { font-weight: 600; }
202
+
203
+ /* Footer */
204
+ .dev-footer {
205
+ margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
206
+ padding: 1rem; border-top: 1px solid var(--border); width: 100%; max-width: 600px;
207
+ }
208
+ .dev-badge {
209
+ display: inline-block; background: var(--bg-card); padding: 5px 15px;
210
+ border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
211
+ }
212
+ .dev-name { color: var(--primary); font-weight: 700; }
213
+
214
+ .refresh-dot {
215
+ width: 10px; height: 10px; background-color: var(--success);
216
+ border-radius: 50%; display: inline-block; animation: pulse 2s infinite;
217
+ }
218
+ @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
219
+ </style>
220
+ </head>
221
+ <body>
222
+
223
+ <nav class="navbar">
224
+ <div class="brand">πŸ“Š Server Monitor <span class="refresh-dot" title="Live Updates"></span></div>
225
+ <button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">πŸŒ™</button>
226
+ </nav>
227
+
228
+ <div class="dashboard">
229
+
230
+ <div class="card">
231
+ <div class="card-header">
232
+ <span>CPU Usage</span>
233
+ <span id="cpuCores">-- Cores</span>
234
+ </div>
235
+ <div class="stat-value" id="cpuPercent">0%</div>
236
+ <div class="progress-container">
237
+ <div class="progress-bar" id="cpuBar"></div>
238
+ </div>
239
+ <div class="stat-sub" id="cpuFreq">Freq: -- MHz</div>
240
+ </div>
241
+
242
+ <div class="card">
243
+ <div class="card-header">Memory (RAM)</div>
244
+ <div class="stat-value" id="memPercent">0%</div>
245
+ <div class="progress-container">
246
+ <div class="progress-bar" id="memBar"></div>
247
+ </div>
248
+ <div class="stat-sub" id="memDetails">Used: -- / -- GB</div>
249
+ </div>
250
+
251
+ <div class="card">
252
+ <div class="card-header">Disk (Root)</div>
253
+ <div class="stat-value" id="diskPercent">0%</div>
254
+ <div class="progress-container">
255
+ <div class="progress-bar" id="diskBar"></div>
256
+ </div>
257
+ <div class="stat-sub" id="diskDetails">Free: -- GB</div>
258
+ </div>
259
+
260
+ <div class="card">
261
+ <div class="card-header">Network I/O</div>
262
+ <div class="sys-list">
263
+ <li>
264
+ <span class="sys-label">⬇️ Received</span>
265
+ <span class="sys-val" id="netRecv">-- MB</span>
266
+ </li>
267
+ <li>
268
+ <span class="sys-label">⬆️ Sent</span>
269
+ <span class="sys-val" id="netSent">-- MB</span>
270
+ </li>
271
+ </div>
272
+ </div>
273
+
274
+ <div class="card" style="grid-column: 1 / -1;">
275
+ <div class="card-header">System Information</div>
276
+ <ul class="sys-list" id="sysList">
277
+ </ul>
278
+ </div>
279
+
280
+ </div>
281
+
282
+ <footer class="dev-footer">
283
+ <div>Designed & Developed by</div>
284
+ <div class="dev-badge">
285
+ <span class="dev-name">Sameer Banchhor</span> | Data Scientist
286
+ </div>
287
+ </footer>
288
+
289
+ <script>
290
+ // Theme Logic
291
+ const html = document.documentElement;
292
+ const themeBtn = document.getElementById('themeBtn');
293
+
294
+ function toggleTheme() {
295
+ const current = html.getAttribute('data-theme');
296
+ const next = current === 'light' ? 'dark' : 'light';
297
+ html.setAttribute('data-theme', next);
298
+ themeBtn.textContent = next === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
299
+ localStorage.setItem('theme', next);
300
+ }
301
+
302
+ const savedTheme = localStorage.getItem('theme') || 'light';
303
+ html.setAttribute('data-theme', savedTheme);
304
+ themeBtn.textContent = savedTheme === 'light' ? 'πŸŒ™' : 'β˜€οΈ';
305
+
306
+ // Data Fetching Logic
307
+ async function fetchData() {
308
+ try {
309
+ const res = await fetch('/server-status/full-info');
310
+ const data = await res.json();
311
+
312
+ updateUI(data);
313
+ } catch (error) {
314
+ console.error("Failed to fetch status:", error);
315
+ }
316
+ }
317
+
318
+ function updateUI(data) {
319
+ // CPU
320
+ const cpuP = data.cpu_status.cpu_usage_percent;
321
+ document.getElementById('cpuPercent').textContent = cpuP + '%';
322
+ document.getElementById('cpuBar').style.width = cpuP + '%';
323
+ setColor('cpuBar', cpuP);
324
+ document.getElementById('cpuCores').textContent = `${data.cpu_status.physical_cores}P / ${data.cpu_status.total_cores}L Cores`;
325
+ document.getElementById('cpuFreq').textContent = `Freq: ${data.cpu_status.cpu_frequency_mhz} MHz`;
326
+
327
+ // RAM
328
+ const memP = data.memory_status.usage_percent;
329
+ document.getElementById('memPercent').textContent = memP + '%';
330
+ document.getElementById('memBar').style.width = memP + '%';
331
+ setColor('memBar', memP);
332
+ document.getElementById('memDetails').textContent = `Used: ${data.memory_status.used_gb} / ${data.memory_status.total_gb} GB`;
333
+
334
+ // Disk
335
+ const diskP = data.disk_status.usage_percent;
336
+ document.getElementById('diskPercent').textContent = diskP + '%';
337
+ document.getElementById('diskBar').style.width = diskP + '%';
338
+ setColor('diskBar', diskP);
339
+ document.getElementById('diskDetails').textContent = `Free: ${data.disk_status.free_gb} GB`;
340
+
341
+ // Network
342
+ document.getElementById('netRecv').textContent = data.network_io.bytes_recv_mb + ' MB';
343
+ document.getElementById('netSent').textContent = data.network_io.bytes_sent_mb + ' MB';
344
+
345
+ // System Info (Only populate once if empty)
346
+ const sysList = document.getElementById('sysList');
347
+ if(sysList.children.length === 0) {
348
+ const info = data.system_information;
349
+ for (const [key, value] of Object.entries(info)) {
350
+ const li = document.createElement('li');
351
+ li.innerHTML = `<span class="sys-label">${key}</span><span class="sys-val">${value}</span>`;
352
+ sysList.appendChild(li);
353
+ }
354
+ }
355
+ }
356
+
357
+ function setColor(id, percent) {
358
+ const el = document.getElementById(id);
359
+ if(percent < 60) el.style.backgroundColor = 'var(--primary)';
360
+ else if(percent < 85) el.style.backgroundColor = 'var(--warning)';
361
+ else el.style.backgroundColor = 'var(--danger)';
362
+ }
363
+
364
+ // Init
365
+ fetchData();
366
+ // Refresh every 2 seconds
367
+ setInterval(fetchData, 2000);
368
+
369
+ </script>
370
+ </body>
371
+ </html>
372
+ """
373
+ return html_content