| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>KeyStone Web Dashboard</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> |
| <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Round" rel="stylesheet"> |
| <style> |
| :root { |
| --bg-color: #0f1115; |
| --glass-bg: rgba(25, 28, 36, 0.6); |
| --glass-border: rgba(255, 255, 255, 0.08); |
| --text-primary: #ffffff; |
| --text-secondary: #94a3b8; |
| --accent: #3b82f6; |
| --accent-hover: #2563eb; |
| --danger: #ef4444; |
| --danger-hover: #dc2626; |
| } |
| |
| * { |
| box-sizing: border-box; |
| margin: 0; |
| padding: 0; |
| } |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: var(--bg-color); |
| color: var(--text-primary); |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| background-image: |
| radial-gradient(circle at 15% 50%, rgba(59, 130, 246, 0.15), transparent 25%), |
| radial-gradient(circle at 85% 30%, rgba(139, 92, 246, 0.15), transparent 25%); |
| background-attachment: fixed; |
| } |
| |
| .glass-panel { |
| background: var(--glass-bg); |
| backdrop-filter: blur(16px); |
| -webkit-backdrop-filter: blur(16px); |
| border: 1px solid var(--glass-border); |
| border-radius: 16px; |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); |
| } |
| |
| |
| #login-container { |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| height: 100vh; |
| } |
| |
| .login-box { |
| padding: 40px; |
| width: 100%; |
| max-width: 400px; |
| text-align: center; |
| } |
| |
| .login-box h1 { |
| font-size: 24px; |
| font-weight: 600; |
| margin-bottom: 8px; |
| background: linear-gradient(to right, #60a5fa, #a78bfa); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .login-box p { |
| color: var(--text-secondary); |
| margin-bottom: 32px; |
| font-size: 14px; |
| } |
| |
| input { |
| width: 100%; |
| padding: 12px 16px; |
| margin-bottom: 16px; |
| background: rgba(0, 0, 0, 0.2); |
| border: 1px solid var(--glass-border); |
| border-radius: 8px; |
| color: white; |
| font-family: 'Inter', sans-serif; |
| font-size: 14px; |
| transition: all 0.2s; |
| } |
| |
| input:focus { |
| outline: none; |
| border-color: var(--accent); |
| background: rgba(0, 0, 0, 0.4); |
| } |
| |
| button { |
| width: 100%; |
| padding: 12px; |
| background: var(--accent); |
| color: white; |
| border: none; |
| border-radius: 8px; |
| font-weight: 600; |
| font-size: 14px; |
| cursor: pointer; |
| transition: background 0.2s, transform 0.1s; |
| font-family: 'Inter', sans-serif; |
| } |
| |
| button:hover { |
| background: var(--accent-hover); |
| } |
| |
| button:active { |
| transform: scale(0.98); |
| } |
| |
| |
| #dashboard-container { |
| display: none; |
| padding: 32px; |
| max-width: 1200px; |
| margin: 0 auto; |
| width: 100%; |
| } |
| |
| .header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 32px; |
| } |
| |
| .header h1 { |
| font-size: 20px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .logout-btn { |
| background: rgba(255, 255, 255, 0.1); |
| width: auto; |
| padding: 8px 16px; |
| } |
| |
| .logout-btn:hover { |
| background: rgba(239, 68, 68, 0.2); |
| color: var(--danger); |
| } |
| |
| .explorer-header { |
| display: flex; |
| align-items: center; |
| padding: 16px 24px; |
| border-bottom: 1px solid var(--glass-border); |
| gap: 12px; |
| } |
| |
| .breadcrumbs { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| flex: 1; |
| overflow-x: auto; |
| white-space: nowrap; |
| } |
| |
| .crumb { |
| color: var(--text-secondary); |
| cursor: pointer; |
| transition: color 0.2s; |
| font-weight: 500; |
| } |
| |
| .crumb:hover { |
| color: var(--accent); |
| } |
| |
| .crumb.active { |
| color: var(--text-primary); |
| cursor: default; |
| } |
| |
| .separator { |
| color: var(--glass-border); |
| } |
| |
| .file-list { |
| padding: 8px; |
| min-height: 400px; |
| } |
| |
| .file-item { |
| display: flex; |
| align-items: center; |
| padding: 12px 16px; |
| border-radius: 8px; |
| cursor: pointer; |
| transition: background 0.2s; |
| gap: 16px; |
| } |
| |
| .file-item:hover { |
| background: rgba(255, 255, 255, 0.05); |
| } |
| |
| .file-icon { |
| color: var(--text-secondary); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| width: 40px; |
| height: 40px; |
| background: rgba(0, 0, 0, 0.2); |
| border-radius: 8px; |
| } |
| |
| .file-item:hover .file-icon { |
| color: var(--accent); |
| } |
| |
| .file-name { |
| flex: 1; |
| font-weight: 500; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| white-space: nowrap; |
| } |
| |
| .file-meta { |
| color: var(--text-secondary); |
| font-size: 13px; |
| width: 120px; |
| text-align: right; |
| } |
| |
| .download-btn { |
| background: transparent; |
| width: 36px; |
| height: 36px; |
| padding: 0; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: var(--text-secondary); |
| } |
| |
| .download-btn:hover { |
| background: rgba(59, 130, 246, 0.2); |
| color: var(--accent); |
| } |
| |
| .empty-state { |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| height: 300px; |
| color: var(--text-secondary); |
| gap: 16px; |
| } |
| |
| #error-msg { |
| color: var(--danger); |
| font-size: 13px; |
| margin-bottom: 16px; |
| display: none; |
| } |
| |
| |
| #loader { |
| position: fixed; |
| top: 0; left: 0; right: 0; bottom: 0; |
| background: rgba(15, 17, 21, 0.8); |
| display: none; |
| justify-content: center; |
| align-items: center; |
| z-index: 100; |
| backdrop-filter: blur(4px); |
| } |
| |
| .spinner { |
| width: 40px; |
| height: 40px; |
| border: 3px solid rgba(255,255,255,0.1); |
| border-radius: 50%; |
| border-top-color: var(--accent); |
| animation: spin 1s ease-in-out infinite; |
| } |
| |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="loader"><div class="spinner"></div></div> |
|
|
| |
| <div id="login-container"> |
| <div class="glass-panel login-box"> |
| <h1>KeyStone</h1> |
| <p>Secure Cloud Dashboard</p> |
| <div id="error-msg">Invalid credentials</div> |
| <form id="login-form"> |
| <input type="text" id="username" placeholder="Username" required autocomplete="username"> |
| <input type="password" id="password" placeholder="Password" required autocomplete="current-password"> |
| <button type="submit">Sign In</button> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div id="dashboard-container"> |
| <div class="header"> |
| <h1><span class="material-icons-round" style="color: var(--accent);">cloud_done</span> KeyStone Dashboard</h1> |
| <button class="logout-btn" onclick="logout()">Logout</button> |
| </div> |
|
|
| <div class="glass-panel"> |
| <div class="explorer-header"> |
| <button class="download-btn" onclick="goUp()" title="Go Up" id="btn-up" style="display: none;"> |
| <span class="material-icons-round">arrow_upward</span> |
| </button> |
| <div class="breadcrumbs" id="breadcrumbs"> |
| |
| </div> |
| <button class="download-btn" onclick="downloadCurrentFolder()" title="Download Folder as ZIP" style="width: auto; padding: 0 16px; border-radius: 8px;"> |
| <span class="material-icons-round">file_download</span> <span style="font-size: 13px; margin-left: 6px;">ZIP</span> |
| </button> |
| </div> |
| |
| <div class="file-list" id="file-list"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const API_BASE = ''; |
| let currentPath = ''; |
| |
| |
| const showLoader = () => document.getElementById('loader').style.display = 'flex'; |
| const hideLoader = () => document.getElementById('loader').style.display = 'none'; |
| |
| |
| const checkAuth = () => { |
| const token = localStorage.getItem('token'); |
| if (token) { |
| document.getElementById('login-container').style.display = 'none'; |
| document.getElementById('dashboard-container').style.display = 'block'; |
| loadPath(''); |
| } else { |
| document.getElementById('login-container').style.display = 'flex'; |
| document.getElementById('dashboard-container').style.display = 'none'; |
| } |
| }; |
| |
| document.getElementById('login-form').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const u = document.getElementById('username').value; |
| const p = document.getElementById('password').value; |
| |
| showLoader(); |
| try { |
| const res = await fetch(`${API_BASE}/auth/login`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ username: u, password: p }) |
| }); |
| if (!res.ok) throw new Error('Invalid credentials'); |
| const data = await res.json(); |
| localStorage.setItem('token', data.token); |
| document.getElementById('error-msg').style.display = 'none'; |
| checkAuth(); |
| } catch (err) { |
| const errEl = document.getElementById('error-msg'); |
| errEl.innerText = err.message; |
| errEl.style.display = 'block'; |
| } finally { |
| hideLoader(); |
| } |
| }); |
| |
| const logout = () => { |
| localStorage.removeItem('token'); |
| checkAuth(); |
| }; |
| |
| |
| const formatSize = (bytes) => { |
| if (bytes === 0) return '--'; |
| const k = 1024; |
| const sizes = ['B', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; |
| }; |
| |
| const formatDate = (ms) => { |
| if (!ms) return '--'; |
| return new Date(ms).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }); |
| }; |
| |
| |
| const loadPath = async (path) => { |
| showLoader(); |
| const token = localStorage.getItem('token'); |
| try { |
| const res = await fetch(`${API_BASE}/api/web/explorer?path=${encodeURIComponent(path)}&token=${token}`); |
| |
| if (res.status === 401 || res.status === 403) { |
| logout(); |
| return; |
| } |
| |
| const data = await res.json(); |
| currentPath = data.path || ''; |
| renderBreadcrumbs(); |
| renderFiles(data.items); |
| } catch (e) { |
| console.error(e); |
| alert("Failed to load directory."); |
| } finally { |
| hideLoader(); |
| } |
| }; |
| |
| const goUp = () => { |
| if (!currentPath) return; |
| const parts = currentPath.split('/'); |
| parts.pop(); |
| loadPath(parts.join('/')); |
| }; |
| |
| const navigateTo = (folder) => { |
| const newPath = currentPath ? `${currentPath}/${folder}` : folder; |
| loadPath(newPath); |
| }; |
| |
| const downloadCurrentFolder = () => { |
| triggerDownload(currentPath); |
| }; |
| |
| const triggerDownload = (itemPath) => { |
| const token = localStorage.getItem('token'); |
| const url = `${API_BASE}/api/web/download?path=${encodeURIComponent(itemPath)}&token=${token}`; |
| |
| |
| |
| const a = document.createElement('a'); |
| a.href = url; |
| |
| const parts = itemPath.split('/'); |
| const fallbackName = parts[parts.length - 1] || 'download'; |
| a.download = fallbackName; |
| |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| }; |
| |
| const renderBreadcrumbs = () => { |
| const container = document.getElementById('breadcrumbs'); |
| const parts = currentPath.split('/').filter(Boolean); |
| |
| document.getElementById('btn-up').style.display = parts.length > 0 ? 'flex' : 'none'; |
| |
| let html = `<div class="crumb ${parts.length === 0 ? 'active' : ''}" onclick="loadPath('')">Home</div>`; |
| |
| let accPath = ""; |
| parts.forEach((p, i) => { |
| accPath += (accPath ? "/" : "") + p; |
| html += `<span class="separator">/</span>`; |
| if (i === parts.length - 1) { |
| html += `<div class="crumb active">${p}</div>`; |
| } else { |
| html += `<div class="crumb" onclick="loadPath('${accPath}')">${p}</div>`; |
| } |
| }); |
| container.innerHTML = html; |
| }; |
| |
| const renderFiles = (items) => { |
| const container = document.getElementById('file-list'); |
| if (items.length === 0) { |
| container.innerHTML = ` |
| <div class="empty-state"> |
| <span class="material-icons-round" style="font-size: 48px; opacity: 0.5;">folder_open</span> |
| <p>This folder is empty</p> |
| </div> |
| `; |
| return; |
| } |
| |
| let html = ''; |
| items.forEach(item => { |
| const icon = item.is_dir ? 'folder' : (item.name.match(/\.(mp4|mov)$/i) ? 'movie' : 'image'); |
| const fullPath = currentPath ? `${currentPath}/${item.name}` : item.name; |
| |
| html += ` |
| <div class="file-item" onclick="${item.is_dir ? `navigateTo('${item.name}')` : `triggerDownload('${fullPath}')`}"> |
| <div class="file-icon"> |
| <span class="material-icons-round">${icon}</span> |
| </div> |
| <div class="file-name">${item.name}</div> |
| <div class="file-meta">${formatDate(item.modified)}</div> |
| <div class="file-meta">${formatSize(item.size)}</div> |
| <button class="download-btn" onclick="event.stopPropagation(); triggerDownload('${fullPath}')" title="Download"> |
| <span class="material-icons-round">download</span> |
| </button> |
| </div> |
| `; |
| }); |
| container.innerHTML = html; |
| }; |
| |
| |
| checkAuth(); |
| </script> |
| </body> |
| </html> |
|
|