dpv007's picture
added1
d3dfeb4
Raw
History Blame Contribute Delete
17.1 kB
<!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 Screen */
#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 */
#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;
}
/* Loading overlay */
#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>
<!-- Login Container -->
<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>
<!-- Dashboard Container -->
<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">
<!-- Rendered via JS -->
</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">
<!-- Rendered via JS -->
</div>
</div>
</div>
<script>
const API_BASE = '';
let currentPath = '';
// UI Helpers
const showLoader = () => document.getElementById('loader').style.display = 'flex';
const hideLoader = () => document.getElementById('loader').style.display = 'none';
// Auth Flow
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();
};
// Formatters
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' });
};
// Explorer Logic
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}`;
// Create an invisible anchor tag to trigger native browser download stream
// This is safer for large files/zip folders than window.open or fetch->blob
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;
};
// Initialize
checkAuth();
</script>
</body>
</html>