diff --git a/Web/content/adminpanel.php b/Web/content/adminpanel.php index ca4c2ba..8a43c45 100644 --- a/Web/content/adminpanel.php +++ b/Web/content/adminpanel.php @@ -10,6 +10,7 @@ ini_set('display_errors', 1); error_reporting(E_ALL); require_once __DIR__ . '/../config/config.php'; +require_once __DIR__ . '/../includes/workspace.php'; // $username = "UNAME"; // $password = "PSWD"; // $db = "usbraidlogin"; @@ -136,15 +137,15 @@ function CheckPassword($password) { ?> - + + + Admin Panel - - - - - + + + + + - -
- -
+ '../', + 'active' => 'users', + 'section' => 'User management', + 'eyebrow' => 'Workspace administration', + 'title' => 'User management', + 'description' => 'Manage accounts, storage locations and access permissions.', + 'class' => 'users-page', +]); ?> + +
-
-
-

USB RAID Array

- -
-
-
-

User Management

-
+

Add a member

Set up an account and choose its access.

+
- - + +
- - + +
- - + +
@@ -211,9 +206,11 @@ function CheckPassword($password) {
- -

Users List

- + +

Workspace members

Accounts and permissions for your storage.

+ + +
@@ -240,25 +237,10 @@ function CheckPassword($password) { -
Username
-
-
-
- - - - +
+ - + + Change Password - - - - - + + + + + - -
- -
- -
-
-

USB RAID Array

-
- -
-
- -
-
-

Change Password

- + '../', + 'active' => 'account', + 'section' => 'Account settings', + 'eyebrow' => 'Your account', + 'title' => 'Account settings', + 'description' => 'Keep your workspace account secure and up to date.', + 'class' => 'account-page', +]); ?> + + + - - - - -
+ + diff --git a/Web/content/css/adminpanel.css b/Web/content/css/adminpanel.css index de6996f..2165e56 100644 --- a/Web/content/css/adminpanel.css +++ b/Web/content/css/adminpanel.css @@ -1,66 +1,13 @@ -html, body { - height: 100%; - margin: 0; -} - -.custom-container { - display: flex; - flex-direction: column; - min-height: 100vh; - max-width: 60%; - margin: 0 auto; -} -.custom-container.dark-box { - min-height: calc(100vh - 4rem); - width: min(60%, 1200px); -} -section.row { - flex: 1; -} -@media (max-width: 1120px) { - .custom-container { - max-width: 90%; - } - .custom-container.dark-box { - width: 90%; - } -} -@media (max-width: 700px) { - .custom-container.dark-box { - width: calc(100% - 2rem); - min-height: calc(100vh - 2rem); - margin: 1rem auto; - } - body { - min-width: 0; - } -} -.add-user-form { - max-width: 400px; - margin: 0 auto; -} -.form-check { - display: flex; - align-items: center; - width: 100%; - justify-content: flex-start; -} -.form-check-input { - margin-right: 10px; -} -.form-check-label { - margin-left: 0.5rem; -} -.hover-effect { - transition: opacity 0.3s ease; -} - -.hover-effect:hover { - opacity: 0.8; -} -.theme-light .dark-logo { - display: none; -} -.theme-dark .light-logo { - display: none; -} +.users-layout { display:grid; grid-template-columns:300px minmax(0,1fr); align-items:start; gap:22px; } +.users-layout > .alert { grid-column:1/-1; margin:0; } +.add-user-form { margin:0; text-align:left; } +.add-user-form .form-check { width:100%; margin-bottom:12px; font-size:12px; } +.add-user-form .btn { width:100%; } +.users-table { min-width:650px; } +.users-table th { font-size:10px; padding:12px 10px; white-space:nowrap; } +.users-table td { font-size:12px; padding:14px 10px; } +.users-table td:first-child { font-weight:600; } +.users-table td:nth-child(3) { font-family:ui-monospace,monospace; white-space:nowrap; } +.user-create .form-label { font-size:12px; } +@media(max-width:1200px) { .users-layout { grid-template-columns:1fr; } .add-user-form { display:grid; grid-template-columns:1fr 1fr; gap:0 24px; } .add-user-form > .d-flex { grid-column:1/-1; display:grid; grid-template-columns:1fr 1fr; gap:8px; } .add-user-form > .btn { width:auto; justify-self:start; } } +@media(max-width:600px) { .add-user-form { display:block; } .add-user-form > .d-flex { display:flex; align-items:flex-start; gap:0; } } diff --git a/Web/content/css/changepassword.css b/Web/content/css/changepassword.css index 7a03f33..7f4ad73 100644 --- a/Web/content/css/changepassword.css +++ b/Web/content/css/changepassword.css @@ -1,56 +1,6 @@ -.container-fluid { - width: 100%; - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} - -.content-section { - max-width: 800px; - margin: 0 auto; -} - -.change-password-form { - max-width: 100%; - margin: 0 auto; -} - -@media (min-width: 768px) { - .change-password-form { - max-width: 400px; - } -} - -.hover-effect { - transition: opacity 0.3s ease; -} - -.hover-effect:hover { - opacity: 0.8; -} - -.theme-light .dark-logo { - display: none; -} - -.theme-dark .light-logo { - display: none; -} - -.btn-group-responsive { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - justify-content: center; -} - -h1 { - font-size: 1.8rem; -} - -@media (min-width: 576px) { - h1 { - font-size: 2.5rem; - } -} \ No newline at end of file +.account-layout { display:grid; grid-template-columns:minmax(0,560px) minmax(200px,1fr); align-items:start; gap:40px; } +.change-password-form { margin:0; } +.change-password-form .form-label { font-size:12px; } +.account-help { max-width:320px; padding:24px 0; color:var(--muted); font-size:13px; } +.account-help h3 { color:var(--text); margin-top:12px; font-size:18px; } +@media(max-width:1000px) { .account-layout { grid-template-columns:1fr; gap:16px; } .account-help { max-width:100%; padding:12px 4px; } } diff --git a/Web/content/ftp/css/index.css b/Web/content/ftp/css/index.css index 00c079c..90d14bb 100644 --- a/Web/content/ftp/css/index.css +++ b/Web/content/ftp/css/index.css @@ -1,142 +1,16 @@ -html, body { - height: 100%; - margin: 0; - overflow-x: hidden; - overflow-y: auto; -} +.pathbar { display:flex; align-items:center; flex-wrap:wrap; gap:12px; padding:13px 16px; background:var(--surface); border:1px solid var(--line); border-radius:10px; margin-bottom:20px; font-size:12px; overflow-wrap:anywhere; } +.pathbar a { color:var(--text); } .pathbar a:first-child { color:var(--blue); } .path-separator { color:var(--muted); } .path-label { margin-left:auto; color:var(--muted); font:11px ui-monospace,monospace; } +.file-panel { border:1px solid var(--line); border-radius:12px; background:var(--surface); box-shadow:var(--shadow); overflow:hidden; } +.browser-toolbar { padding:20px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--line); } +.search-field { display:flex; align-items:center; gap:10px; margin:0; padding:0 12px; border:1px solid var(--line); border-radius:8px; width:360px; max-width:100%; background:var(--bg); color:var(--muted); font-weight:400; } +.search-field > span { font-size:24px; } .search-field input { width:100%; min-width:0; padding:10px 0; border:0; background:transparent; color:var(--text); outline:none; } .search-field:focus-within { outline:2px solid var(--blue); } .search-field input::placeholder { color:var(--muted); } kbd { font:11px ui-monospace,monospace; padding:0 5px; border:1px solid var(--line); border-radius:3px; } +.browser-toolbar select { width:130px; margin-left:auto; font-size:12px; padding:10px; } +.action-buttons { display:flex; flex-wrap:wrap; gap:8px; padding:18px 20px 0; } .action-buttons .btn { font-size:12px; } .action-buttons .btn-primary { order:-1; } +.dropzone { margin:18px 20px 22px; border:1px dashed var(--line); background:var(--bg); border-radius:9px; display:flex; align-items:center; justify-content:center; gap:14px; padding:20px; text-align:left; } +.dropzone.dragover { border-color:var(--blue); background:var(--blue-soft); } .upload-symbol { display:grid; place-items:center; width:36px; height:36px; border:1px solid var(--line); border-radius:9px; background:var(--surface); font-size:22px; color:var(--blue); } .dropzone strong { font-weight:550; font-size:12px; } .dropzone small { display:block; color:var(--muted); font-size:11px; margin-top:3px; } +.file-table-scroll { overflow-x:auto; } .file-table { min-width:650px; } .file-table th { padding:12px 20px; } .file-table td { padding:14px 20px; font-size:12px; } .file-table th:first-child { width:48%; } .file-table th:nth-child(2) { width:13%; } .file-table th:last-child { width:39%; } .file-table td:first-child { font-weight:550; } .file-table td:first-child a { color:var(--text); } .file-table .btn { font-size:11px; padding:5px 8px; min-height:30px; background:transparent; border-color:transparent; } .file-table .btn:hover { background:var(--surface); border-color:var(--line); } .file-table td:last-child { white-space:nowrap; } .file-table td:nth-child(2) { color:var(--muted); font-family:ui-monospace,monospace; font-size:11px; } +.file-icon { display:inline-grid; place-items:center; width:30px; height:32px; margin-right:10px; color:var(--muted); background:var(--surface-alt); border-radius:7px; vertical-align:middle; font-size:18px; } .folder-icon { color:var(--blue); background:var(--blue-soft); } .file-table .badge { margin-left:5px; } +.browser-status { display:flex; justify-content:space-between; padding:13px 20px; color:var(--muted); font-size:11px; } .empty-state { text-align:center; padding:48px 20px; color:var(--muted); } .empty-state > span { font-size:38px; } .empty-state h3 { color:var(--text); margin:6px 0; } .empty-state p { margin:0; } +.file-content > .alert { margin:16px 20px; } -.custom-container { - display: flex; - flex-direction: column; - min-height: 100vh; - margin: 0; - padding: 0; - max-width: 100%; - overflow: visible; -} - -section.row { - flex: 1; - margin: 0; - width: 100%; - justify-content: center; -} - -article.col-8 { - max-width: 1600px; -} - -@media (max-width: 1800px) { - article.col-8 { - flex: 0 0 auto; - width: 95%; - } -} - -@media (max-width: 768px) { - .custom-container { - height: auto; - min-height: initial; - } - - section.row { - flex: 0 0 auto; - } - - footer { - margin-top: 20px; - position: relative; - } - - body { - height: auto; - min-height: initial; - } - - article { - padding-bottom: 20px; - } -} - -table { - width: 100%; - border-collapse: collapse; - margin: 20px 0; - table-layout: fixed; -} - -th, td { - padding: 10px; - border: 1px solid #ddd; - text-align: left; - word-wrap: break-word; - overflow-wrap: break-word; -} - -.dropzone { - width: 100%; - padding: 20px; - border: 2px dashed #007bff; - border-radius: 10px; - text-align: center; - margin-bottom: 20px; - cursor: pointer; - box-sizing: border-box; -} - -.dropzone.dragover { - background-color: #e0f7fa; -} - -.action-buttons { - margin-bottom: 15px; - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 10px; -} - -.action-buttons button { - margin-right: 0; -} - -body { - min-width: 500px; -} - -.hover-effect { - transition: opacity 0.3s ease; -} - -.hover-effect:hover { - opacity: 0.8; -} - -.theme-light .dark-logo { - display: none; -} - -.theme-dark .light-logo { - display: none; -} - -footer { - margin-top: auto; - padding: 20px; - border-top: 1px solid #ddd; - width: 100%; -} - -/* Give the SFTP file browser its own surface, distinct from the outer page. */ -article.col-8 { - background: linear-gradient(145deg, var(--sftp-bg-start), var(--sftp-bg-end)); - border-color: rgba(77, 145, 255, 0.3) !important; - border-radius: 1rem !important; - box-shadow: 0 12px 32px rgba(3, 8, 18, 0.2); -} - -html[data-bs-theme="light"] article.col-8 { - background: linear-gradient(145deg, #ffffff, #edf3fb); - border-color: rgba(59, 130, 246, 0.4) !important; - box-shadow: 0 18px 48px rgba(31, 58, 92, 0.1); -} +@media(max-width:760px) { .path-label { display:none; } .browser-toolbar { padding:14px; gap:8px; } .search-field { width:auto; flex:1; min-width:0; } .browser-toolbar select { width:100px; } .action-buttons { padding:14px 14px 0; } .dropzone { margin:14px; } } diff --git a/Web/content/ftp/css/open.css b/Web/content/ftp/css/open.css index 87b2911..c8ee4f9 100644 --- a/Web/content/ftp/css/open.css +++ b/Web/content/ftp/css/open.css @@ -1,54 +1,6 @@ -.editor-container { - border: 1px solid #ccc; - border-radius: 4px; - margin-bottom: 20px; - position: relative; -} - -#editor { - width: 100%; - min-height: 400px; - font-family: monospace; - padding: 10px; - white-space: pre; - overflow: auto; - resize: vertical; - tab-size: 4; - -moz-tab-size: 4; - padding-left: 55px; /* Make room for line numbers */ -} - -.line-numbers { - position: absolute; - left: 0; - top: 0; - width: 45px; - text-align: right; - padding: 10px 5px 10px 0; - border-right: 1px solid #ccc; - color: #999; - user-select: none; - font-family: monospace; - overflow: hidden; - z-index: 1; - background-color: #f8f9fa; /* Light mode default */ -} - -[data-bs-theme="dark"] .line-numbers { - background-color: #212529; /* Dark mode background */ - border-right-color: #495057; /* Darker border for dark mode */ - color: #6c757d; /* Lighter text for dark mode */ -} - -.header-container { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; -} - -.readonly-notice { - color: #dc3545; - font-weight: bold; - margin-left: 10px; -} \ No newline at end of file +.editor-container { position:relative; border:1px solid var(--line); border-radius:10px; overflow:hidden; margin-bottom:20px; } +#editor { width:100%; min-height:55vh; padding:16px; font:13px/1.65 ui-monospace,Consolas,monospace; white-space:pre; overflow:auto; resize:vertical; tab-size:4; border:0; border-radius:0; background:var(--bg); } +.editor-container:has(.line-numbers) #editor { padding-left:64px; } +.line-numbers { position:absolute; left:0; top:0; width:48px; text-align:right; padding:16px 10px 16px 0; border-right:1px solid var(--line); color:var(--muted); user-select:none; font:13px/1.65 ui-monospace,Consolas,monospace; overflow:hidden; z-index:1; background:var(--surface-alt); } +.readonly-notice { color:var(--muted); font-size:12px; } +.editor-page .d-flex { flex-wrap:wrap; gap:8px; } diff --git a/Web/content/ftp/css/pdf.css b/Web/content/ftp/css/pdf.css index 5cb6e6b..8051b2d 100644 --- a/Web/content/ftp/css/pdf.css +++ b/Web/content/ftp/css/pdf.css @@ -1,253 +1,15 @@ -body { - min-height: 100vh; - margin: 0; - padding: 0; -} -#pdfContainer { - width: 100%; - min-height: calc(100vh - 80px); - display: flex; - flex-direction: column; - align-items: center; - padding: 20px 0; -} -#loadingMessage { - position: absolute; - top: 0; - left: 0; - z-index: 1000; - background-color: var(--bs-body-bg); -} -[data-bs-theme="dark"] #loadingMessage { - background-color: rgba(33, 37, 41, 0.9); -} - -[data-bs-theme="light"] #loadingMessage { - background-color: rgba(248, 249, 250, 0.9); -} -.page-container { - background-color: white; - margin: 10px; - box-shadow: 0 0 10px rgba(0,0,0,0.3); - border-radius: 4px; - overflow: hidden; -} -.controls { - display: flex; - justify-content: center; - align-items: center; - margin: 0; -} -canvas { - display: block; -} -footer { - margin-top: auto; - padding: 20px; - border-top: 1px solid #ddd; - width: 100%; -} - -.hover-effect { - transition: opacity 0.3s ease; -} - -.hover-effect:hover { - opacity: 0.8; -} - -.theme-light .dark-logo { - display: none; -} - -.theme-dark .light-logo { - display: none; -} -.file-info table { - table-layout: fixed; - word-wrap: break-word; -} -.actionButton { - display: flex; - justify-content: center; - align-items: center; - margin: 0; -} - -.fullscreen-mode { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100vh; - background-color: var(--bs-body-bg); - z-index: 9999; - padding: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.fullscreen-mode header, -.fullscreen-mode footer, -.fullscreen-mode .theme-toggle, -.fullscreen-mode .actionButton { - display: none !important; -} - -.fullscreen-mode #pdfContainer { - flex: 1; - margin: 0; - padding: 0; - height: 100vh; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; -} - -.fullscreen-mode .controls { - position: fixed; - bottom: 10px; - left: 50%; - transform: translateX(-50%); - margin: 0; - padding: 6px 12px; - background-color: rgba(var(--bs-body-bg-rgb), 0.3); - backdrop-filter: blur(5px); - transition: all 0.3s ease; - opacity: 0.3; - z-index: 1000; - border-radius: .375rem; - width: auto; - max-width: 95%; -} - -.fullscreen-mode .controls:hover { - opacity: 1; - background-color: rgba(var(--bs-body-bg-rgb), 0.9); - box-shadow: 0 0 15px rgba(0,0,0,0.2); -} - -.fullscreen-mode[data-bs-theme="light"] .btn-light { - background-color: rgba(233, 236, 239, 0.7); - border-color: rgba(222, 226, 230, 0.7); -} - -.fullscreen-mode[data-bs-theme="dark"] .btn-light { - background-color: rgba(52, 58, 64, 0.7); - border-color: rgba(73, 80, 87, 0.7); -} -.fullscreen-mode .btn-light:hover { - transform: scale(1.05); - transition: transform 0.2s ease; -} - -.fullscreen-mode .btn-light.disabled { - background-color: rgba(var(--bs-body-bg-rgb), 0.5); -} - -.fullscreen-mode .page-container { - display: flex; - justify-content: center; - align-items: center; - margin: 0; - height: 100vh; - width: 100%; -} - -.fullscreen-mode canvas { - max-width: 100%; - max-height: 100vh; - width: 100% !important; - height: auto !important; - margin: 0; - object-fit: contain; -} -.fullscreen-mode .btn-group { - display: flex; - align-items: center; - gap: 4px; -} - -.fullscreen-mode .btn-group .btn { - min-width: 44px; - display: flex; - align-items: center; - justify-content: center; - white-space: nowrap; -} - -.fullscreen-mode .btn-group .btn.disabled { - min-width: 120px; - padding: 6px 12px; -} -/* Mobile-specific adjustments */ -@media (max-width: 768px) { - .fullscreen-mode .controls { - bottom: 5px; - padding: 4px 8px; - width: auto; - max-width: calc(100% - 20px); - } - - .fullscreen-mode .btn-group .btn { - min-width: 36px; - padding: 4px 6px; - } - - .fullscreen-mode .btn-group .btn.disabled { - min-width: 110px; - padding: 4px 8px; - } - - .fullscreen-mode #pageNum, - .fullscreen-mode #pageCount { - font-size: 0.875rem; - margin: 0 2px; - } - - .fullscreen-mode .btn-group { - gap: 3px; - } - - /* Add spacing between page numbers and "of" text */ - .fullscreen-mode .btn-light.disabled span { - margin: 0 2px; - } -} - -/* Theme-specific button styles */ -[data-bs-theme="light"] .btn-light { - background-color: #e9ecef; - border-color: #dee2e6; - color: #212529; -} - -[data-bs-theme="light"] .btn-light:hover { - background-color: #dde2e6; - border-color: #ced4da; - color: #000; -} - -[data-bs-theme="dark"] .btn-light { - background-color: #343a40; - border-color: #495057; - color: #f8f9fa; -} - -[data-bs-theme="dark"] .btn-light:hover { - background-color: #495057; - border-color: #6c757d; - color: #fff; -} - -/* Button group specific styles */ -.btn-group .btn-light { - margin: 0 1px; -} - -.btn-group .btn-light.disabled { - opacity: 0.8; -} \ No newline at end of file +.pdf-workspace { padding:16px; } +#pdfContainer { position:relative; width:100%; min-height:380px; display:flex; flex-direction:column; align-items:center; padding:16px 0; overflow:auto; background:var(--bg); border:1px solid var(--line); border-radius:8px; } +#loadingMessage { position:absolute; inset:0; background:var(--bg); z-index:1; color:var(--muted); } +.page-container { background:white; box-shadow:0 4px 20px #0002; max-width:100%; overflow:hidden; } +.page-container canvas { display:block; } +.controls { display:flex; justify-content:center; margin-top:16px; } +.controls .btn-group { flex-wrap:wrap; justify-content:center; gap:6px; } +.controls .btn { font-size:12px; } +.actionButton { display:flex; justify-content:center; margin-top:12px; } +.actionButton .btn { margin:0; } +.pdf-workspace.fullscreen-mode { position:fixed; inset:0; width:100%; height:100dvh; z-index:9999; background:var(--bg); padding:0; border:0; border-radius:0; display:flex; flex-direction:column; } +.fullscreen-mode #pdfContainer { flex:1; height:100%; min-height:0; padding:0; border:0; border-radius:0; justify-content:center; } +.fullscreen-mode .controls { position:absolute; bottom:12px; left:50%; transform:translateX(-50%); margin:0; padding:8px; border:1px solid var(--line); border-radius:10px; background:var(--surface); max-width:calc(100% - 24px); width:max-content; z-index:2; } +.fullscreen-mode .actionButton { display:none; } +@media(max-width:600px) { .pdf-workspace { padding:10px; } .controls .btn { padding:7px 10px; } } diff --git a/Web/content/ftp/css/serverstat.css b/Web/content/ftp/css/serverstat.css index 836c5f1..27e92ca 100644 --- a/Web/content/ftp/css/serverstat.css +++ b/Web/content/ftp/css/serverstat.css @@ -1,124 +1,18 @@ -/* Layout */ -body { - min-height: 100vh; - margin: 0; - padding: 0; - display: flex; - flex-direction: column; -} - -footer { - margin-top: auto; - padding: 20px; - border-top: 1px solid var(--bs-border-color); - width: 100%; -} - -/* Monitor Cards */ -.monitor-card { - transition: transform 0.2s ease-in-out; - height: 100%; -} - -.monitor-card:hover { - transform: translateY(-5px); - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); -} - -.monitor-card .card-body { - display: flex; - flex-direction: column; - justify-content: center; - padding: 1.5rem; - min-height: 320px; -} - -.monitor-header { - background-color: var(--bs-body-bg); - border-bottom: 1px solid var(--bs-border-color); -} - -.progress { - background-color: var(--bs-tertiary-bg); -} - -/* CPU Gauge */ -.cpu-gauge { - position: relative; - width: 100%; - height: 200px; - display: flex; - align-items: center; - justify-content: center; -} - -.cpu-gauge canvas { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} - -.cpu-gauge .position-relative { - position: absolute !important; - top: 50%; - left: 50%; - transform: translate(-50%, -55%); - z-index: 2; - width: auto; - text-align: center; - line-height: 1.2; -} - -.cpu-gauge .h3 { - font-size: 1.75rem; - line-height: 1; - margin: 0; -} - -.cpu-gauge .small { - opacity: 0.75; - margin-top: 3px; -} - -/* Theme & Logo */ -.hover-effect { - transition: opacity 0.3s ease; -} - -.hover-effect:hover { - opacity: 0.8; -} - -.theme-light .dark-logo { - display: none; -} - -.theme-dark .light-logo { - display: none; -} - -/* Grid Spacing */ -.row.g-4 { - --bs-gutter-y: 2rem; -} - -/* Responsive */ -@media (max-width: 768px) { - .container { - padding: 0.5rem; - } - - .h4 { - font-size: 1.25rem; - } - - .monitor-card .card-body { - padding: 1rem; - min-height: 280px; - } - - .row.g-4 > [class*="col-"] { - margin-bottom: 1rem; - } -} \ No newline at end of file +.stats-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:22px; } +.stats-cell { min-width:0; } +.monitor-card { padding:0; overflow:hidden; box-shadow:var(--shadow); } +.monitor-card .card-header { padding:18px 22px; background:var(--surface); border-bottom:1px solid var(--line); } +.monitor-card h3 { font-size:14px; } +.monitor-card .card-body { padding:24px; min-height:235px; } +.monitor-card .card-body > .text-muted { overflow-wrap:anywhere; } +.monitor-card .row { flex-wrap:nowrap; gap:10px; } +.monitor-card .col-4 { min-width:0; } +.monitor-card .fw-bold { font-size:13px; overflow-wrap:anywhere; margin-top:6px; } +.monitor-card .small { font-size:11px; } +.cpu-gauge { min-height:185px; display:grid; place-items:center; } +.raid-summary { text-align:center; padding:20px 0; } +.raid-summary > .ui-icon { font-size:30px; margin-bottom:12px; } +.monitor-card .progress { margin-top:8px; } +.monitor-card .resource-section + .resource-section { margin-top:24px; } +@media(max-width:1000px) { .stats-grid { grid-template-columns:1fr; } } +@media(max-width:600px) { .monitor-card .card-body { padding:18px; } .monitor-card .row { gap:6px; } .monitor-card .p-2 { padding:10px 6px; } } diff --git a/Web/content/ftp/css/view.css b/Web/content/ftp/css/view.css index 663ec2c..6d7244e 100644 --- a/Web/content/ftp/css/view.css +++ b/Web/content/ftp/css/view.css @@ -1,86 +1,11 @@ -html, body { - height: 100%; - margin: 0; - overflow-x: hidden; -} - -.custom-container { - display: flex; - flex-direction: column; - min-height: 100vh; - margin: 0; - padding: 0; - max-width: 100%; -} - -.media-container { - display: flex; - justify-content: center; - align-items: center; - max-width: 100%; - max-height: 80vh; - margin: 0 auto; - overflow: visible; - flex-direction: column; -} - -.media-container img { - max-width: 100%; - max-height: 70vh; - object-fit: contain; -} - -.media-container video { - max-width: 100%; - max-height: 70vh; -} - -.media-container audio { - width: 100%; - max-width: 600px; - margin: 20px 0; -} - -.controls { - margin: 20px 0; - text-align: center; -} - -footer { - margin-top: auto; - padding: 20px; - border-top: 1px solid #ddd; - width: 100%; -} - -.hover-effect { - transition: opacity 0.3s ease; -} - -.hover-effect:hover { - opacity: 0.8; -} - -.theme-light .dark-logo { - display: none; -} - -.theme-dark .light-logo { - display: none; -} -.file-info table { - table-layout: fixed; - word-wrap: break-word; -} - -@media (max-width: 576px) { - .file-info table { - width: 95% !important; - } - .file-info th { - width: 40%; - } - .file-info td { - width: 60%; - } -} \ No newline at end of file +.media-container { display:flex; align-items:center; justify-content:center; flex-wrap:wrap; gap:12px; padding:24px; background:var(--bg); border:1px solid var(--line); border-radius:10px; min-height:200px; } +.media-container img,.media-container video { display:block; max-width:100%; max-height:65vh; object-fit:contain; flex-basis:100%; } +.media-container audio { display:block; width:100%; max-width:680px; margin:28px auto; flex-basis:100%; } +.media-container .btn { margin:0; } +.file-info { max-width:760px; } +.file-info h4 { font-size:16px; } +.file-info table { width:100%; table-layout:fixed; } +.file-info th { width:160px; } +.file-info td { overflow-wrap:anywhere; font-size:12px; } +.media-page .input-group { flex-wrap:wrap; } .media-page .input-group input { flex:1; min-width:160px; } +@media(max-width:600px) { .media-container { padding:12px; } .file-info th { width:110px; } } diff --git a/Web/content/ftp/index.php b/Web/content/ftp/index.php index 1bb1bf2..7c65be8 100644 --- a/Web/content/ftp/index.php +++ b/Web/content/ftp/index.php @@ -13,19 +13,20 @@ error_reporting(E_ALL); require_once __DIR__ . '/../../config/config.php'; +require_once __DIR__ . '/../../includes/workspace.php'; $sftp = initializeSFTP(); $currentPath = isset($_GET['path']) ? normalizePath($_GET['path']) : $defaultPath; if (strpos($currentPath, $defaultPath) !== 0) { $currentPath = $defaultPath; } - + if (!$sftp->chdir($currentPath)) { die("Failed to navigate to the selected folder: $currentPath"); } $items = $sftp->nlist(); - + $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']; $videoExtensions = ['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv']; $audioExtensions = ['mp3', 'wav', 'm4a', 'flac', 'aac']; @@ -35,51 +36,41 @@ - FTP + Files · USB RAID - - - - - + + + + + - -
- -
-
-
-

USB RAID Array

- -
- -
- -
-
-

Current Path:

-
- -
+ '../../', + 'active' => 'files', + 'section' => 'Files', + 'eyebrow' => 'Your remote storage', + 'title' => 'All files', + 'description' => 'Everything in its place. Browse, manage and transfer your files.', + 'connected' => true, +]); ?> + +
+
+
- - - - + + + + @@ -87,57 +78,57 @@ - + - + - -
+
- Drop files or folders here to upload + Drop files or folders hereOr use the upload buttons to browse your device
- +
- - - + + + @@ -197,15 +188,14 @@ if ($currentPath !== $defaultPath) : ?> - + - - + + @@ -229,7 +219,7 @@ foreach ($files as $file) : ?> - - + -
NameSizeActionsNameSizeActions
.. (Go Back)↑ Parent folder
- / -
+ —
@@ -221,7 +211,7 @@
- +
- +
@@ -301,36 +290,18 @@
+
+ +
Folders first · A–Z
-
+
+ - -
- - + diff --git a/Web/content/ftp/js/index.js b/Web/content/ftp/js/index.js index c366e72..00cb82c 100644 --- a/Web/content/ftp/js/index.js +++ b/Web/content/ftp/js/index.js @@ -158,6 +158,10 @@ function uploadFiles(files) { } }; + uploadStatus.setAttribute('role', 'status'); + xhr.upload.addEventListener('progress', event => { + if (event.lengthComputable) uploadStatus.textContent = 'Uploading ' + filesAdded + ' file(s) · ' + Math.round(event.loaded / event.total * 100) + '%'; + }); xhr.send(formData); } @@ -178,8 +182,7 @@ function createDirectory() { xhr.onload = () => { if (xhr.status === 200) { alert('Directory created successfully!'); - const modal = bootstrap.Modal.getInstance(document.getElementById('createDirModal')); - if (modal) modal.hide(); + document.getElementById('createDirModal').close(); window.location.reload(); } else { alert('Failed to create directory: ' + xhr.responseText || xhr.statusText); @@ -221,8 +224,7 @@ function createFile() { const response = JSON.parse(xhr.responseText); if (response.success) { alert('File created successfully!'); - const modal = bootstrap.Modal.getInstance(document.getElementById('createFileModal')); - if (modal) modal.hide(); + document.getElementById('createFileModal').close(); window.location.reload(); } else { alert('Failed to create file: ' + (response.message || 'Unknown error')); @@ -236,53 +238,36 @@ function createFile() { }; xhr.send(formData); } -function openRenameModal(name, path) { +function openRenameModal(name, path, isDirectory = false) { document.getElementById('originalName').value = name; document.getElementById('fullPath').value = path; - document.getElementById('isDirectory').value = name.endsWith('/') ? '1' : '0'; + document.getElementById('isDirectory').value = isDirectory ? '1' : '0'; document.getElementById('newName').value = name.endsWith('/') ? name.slice(0, -1) : name; - const renameModal = new bootstrap.Modal(document.getElementById('renameModal')); - renameModal.show(); + document.getElementById('renameModal').showModal(); } -document.addEventListener('DOMContentLoaded', function() { - const themeToggle = document.getElementById('themeToggle'); - const html = document.documentElement; - const themeText = document.getElementById('themeText'); - const themeIcon = themeToggle.querySelector('.bi'); - - function setTheme(theme) { - html.setAttribute('data-bs-theme', theme); - document.body.classList.remove('theme-light', 'theme-dark'); - document.body.classList.add('theme-' + theme); - localStorage.setItem('theme', theme); - - if (theme === 'dark') { - themeText.textContent = 'Light Mode'; - themeIcon.className = 'bi bi-sun'; - themeToggle.classList.remove('btn-dark'); - themeToggle.classList.add('btn-light'); - } else { - themeText.textContent = 'Dark Mode'; - themeIcon.className = 'bi bi-moon'; - themeToggle.classList.remove('btn-light'); - themeToggle.classList.add('btn-dark'); - } - } - - const savedTheme = localStorage.getItem('theme'); - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - - if (savedTheme) { - setTheme(savedTheme); - } else { - setTheme(prefersDark ? 'dark' : 'light'); - } - - themeToggle.addEventListener('click', function() { - const currentTheme = html.getAttribute('data-bs-theme'); - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - setTheme(newTheme); + +// Filtering is local to the current directory; no remote request is needed. +const search = document.getElementById('fileSearch'); +const typeFilter = document.getElementById('typeFilter'); +function filterFiles() { + const query = search.value.trim().toLocaleLowerCase(); + const rows = [...document.querySelectorAll('[data-file-name]')]; + let visible = 0; + rows.forEach(row => { + row.hidden = !row.dataset.fileName.toLocaleLowerCase().includes(query) || (typeFilter.value !== 'all' && row.dataset.kind !== typeFilter.value); + if (!row.hidden) visible++; }); -}); \ No newline at end of file + document.getElementById('fileCount').textContent = visible + ' of ' + rows.length + ' items'; + document.getElementById('emptySearch').hidden = visible !== 0; +} +search?.addEventListener('input', filterFiles); +typeFilter?.addEventListener('change', filterFiles); +if (search) filterFiles(); +document.addEventListener('keydown', event => { + if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey && !event.target.closest('input, textarea, select, [contenteditable], dialog')) { + event.preventDefault(); + search?.focus(); + } +}); diff --git a/Web/content/ftp/lib/serverstats.php b/Web/content/ftp/lib/serverstats.php new file mode 100644 index 0000000..3b56449 --- /dev/null +++ b/Web/content/ftp/lib/serverstats.php @@ -0,0 +1,108 @@ +exec('LC_ALL=C df -Pk -- ' . $quotedPath . ' 2>/dev/null')); +} + +function getSystemData($ssh, $storagePath) { + $meminfo = $ssh->exec("LC_ALL=C free -m | awk '/^Mem:/{printf \"%s,%s,%s,\", \$2,\$3,\$7} /^Swap:/{printf \"%s,%s,%s\", \$2,\$3,\$4}'"); + $cpuIdle = $ssh->exec("LC_ALL=C vmstat 1 2 | tail -1 | awk '{print \$15}'"); + + return [ + 'storage' => [ + 'system' => getDiskData($ssh, '/'), + 'data' => getDiskData($ssh, $storagePath), + ], + 'raid' => parseRaidStatus($ssh->exec('cat /proc/mdstat 2>/dev/null')), + 'resources' => parseSystemStats($cpuIdle, $meminfo), + 'lastUpdate' => date('Y-m-d H:i:s'), + ]; +} + +function parseDiskData($output) { + $unavailable = ['available_data' => false, 'size' => 'N/A', 'used' => 'N/A', 'available' => 'N/A', 'usage' => 'N/A', 'filesystem' => '']; + if (!is_string($output)) return $unavailable; + + foreach (preg_split('/\r?\n/', trim($output)) as $line) { + // POSIX df -Pk gives one filesystem per line and sizes in KiB. + if (!preg_match('/^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)%\s+(.+)$/', trim($line), $parts)) continue; + return [ + 'available_data' => true, + 'filesystem' => $parts[1], + 'size' => formatBytes((float) $parts[2] * 1024), + 'used' => formatBytes((float) $parts[3] * 1024), + 'available' => formatBytes((float) $parts[4] * 1024), + 'usage' => $parts[5] . '%', + ]; + } + return $unavailable; +} + +function parseRaidStatus($output) { + $status = ['active' => false, 'status' => 'unavailable', 'type' => 'N/A', 'arrays' => []]; + if (!is_string($output) || trim($output) === '') return $status; + + preg_match_all('/^(md\S+)\s*:\s*(active|inactive)\s+([^\r\n]+)((?:\r?\n[ \t]+[^\r\n]*)*)/m', $output, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $details = $match[3] . $match[4]; + preg_match('/\b(raid\d+|linear|multipath)\b/', $match[3], $level); + $state = $match[2] === 'active' ? 'unknown' : 'inactive'; + if ($state !== 'inactive') { + if (preg_match('/\[([U_]+)\]/', $details, $members)) { + $state = strpos($members[1], '_') !== false ? 'degraded' : 'healthy'; + } elseif (in_array($level[1] ?? '', ['raid0', 'linear'], true)) { + $state = 'active'; // Non-redundant arrays do not expose a [UU] health bitmap. + } + if (preg_match('/\b(recovery|resync|reshape|check|repair)\s*=/', $details)) { + if ($state !== 'degraded') $state = 'syncing'; + } + if (strpos($details, '(F)') !== false) $state = 'degraded'; + } + $status['arrays'][] = ['name' => $match[1], 'type' => $level[1] ?? 'N/A', 'status' => $state]; + } + if (!$status['arrays']) { + $status['status'] = strpos($output, 'Personalities :') !== false ? 'not configured' : 'unavailable'; + return $status; + } + $states = array_column($status['arrays'], 'status'); + $status['active'] = !in_array('inactive', $states, true); + $status['type'] = implode(', ', array_unique(array_column($status['arrays'], 'type'))); + foreach (['inactive', 'degraded', 'unknown', 'syncing', 'active', 'healthy'] as $state) { + if (in_array($state, $states, true)) { $status['status'] = $state; break; } + } + return $status; +} + +function parseSystemStats($cpuOutput, $memOutput) { + $idle = is_string($cpuOutput) ? str_replace(',', '.', trim($cpuOutput)) : ''; + $cpuAvailable = is_numeric($idle) && (float) $idle >= 0 && (float) $idle <= 100; + $cpu = ['available_data' => $cpuAvailable, 'usage' => $cpuAvailable ? number_format(100 - (float) $idle, 1, '.', '') : null]; + $memory = array_fill_keys(['total', 'used', 'available', 'usage', 'swap_total', 'swap_used', 'swap_free', 'swap_usage'], 'N/A'); + $memory['available_data'] = false; + $parts = is_string($memOutput) ? explode(',', trim($memOutput)) : []; + if (count($parts) === 6 && count(array_filter($parts, 'is_numeric')) === 6 && min(array_map('floatval', $parts)) >= 0 && (float) $parts[0] > 0) { + [$total, $used, $available, $swapTotal, $swapUsed, $swapFree] = array_map('floatval', $parts); + $memory = [ + 'available_data' => true, + 'total' => formatBytes($total * 1024 * 1024), + 'used' => formatBytes($used * 1024 * 1024), + 'available' => formatBytes($available * 1024 * 1024), + 'usage' => min(100, round($used / $total * 100, 1)) . '%', + 'swap_total' => formatBytes($swapTotal * 1024 * 1024), + 'swap_used' => formatBytes($swapUsed * 1024 * 1024), + 'swap_free' => formatBytes($swapFree * 1024 * 1024), + 'swap_usage' => ($swapTotal > 0 ? min(100, round($swapUsed / $swapTotal * 100, 1)) : 0) . '%', + ]; + } + return ['cpu' => $cpu, 'memory' => $memory]; +} + +function formatBytes($bytes) { + $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']; + $bytes = max((float) $bytes, 0); + $power = min($bytes > 0 ? (int) floor(log($bytes, 1024)) : 0, count($units) - 1); + return round($bytes / pow(1024, $power), 2) . ' ' . $units[$power]; +} diff --git a/Web/content/ftp/open.php b/Web/content/ftp/open.php index 7ba9bb7..469a5d7 100644 --- a/Web/content/ftp/open.php +++ b/Web/content/ftp/open.php @@ -13,6 +13,7 @@ if(!isset($_SESSION['uname'])){ } require_once __DIR__ . '/../../config/config.php'; +require_once __DIR__ . '/../../includes/workspace.php'; $sftp = initializeSFTP(); $filePath = isset($_GET['file']) ? $_GET['file'] : ''; @@ -37,7 +38,7 @@ if (!empty($filePath) && $sftp->file_exists($filePath) && !$sftp->is_dir($filePa $saveError = "Failed to save changes. Check permissions."; } } - + $content = $sftp->get($filePath); if ($content === false) { $error = "Failed to read file contents."; @@ -54,42 +55,41 @@ $showLineNumbers = in_array(strtolower($extension), ['php', 'js', 'html', 'css', $parentDir = dirname($filePath); ?> - + Edit File - <?= htmlspecialchars($fileName) ?> - - - - - + + + + + - -
- -
-
-
-

Edit File:

- Back to File List -
- + '../../', + 'active' => 'files', + 'section' => 'File editor', + 'eyebrow' => 'File editor', + 'title' => $fileName, + 'description' => 'View and edit text files in your remote workspace.', + 'connected' => true, + 'back' => 'index.php?path=' . urlencode($parentDir), + 'class' => 'editor-page', +]); ?> +
File saved successfully!
- +
- +
@@ -105,7 +105,7 @@ $parentDir = dirname($filePath);
- +
@@ -113,54 +113,55 @@ $parentDir = dirname($filePath); -
- +
+ + + + + + + - -
- -
-
-
-

PDF Viewer

- -
+ '../../', + 'active' => 'files', + 'section' => 'PDF viewer', + 'eyebrow' => 'Document preview', + 'title' => $fileName, + 'description' => 'Browse your document without leaving the workspace.', + 'connected' => true, + 'back' => 'index.php?path=' . urlencode(dirname($filePath)), + 'class' => 'pdf-page', +]); ?> + +
@@ -151,17 +151,17 @@ if (isset($_GET['stream'])) {
- - - -
@@ -171,26 +171,14 @@ if (isset($_GET['stream'])) {
- -
+
+ - + - \ No newline at end of file + diff --git a/Web/content/ftp/serverstat.php b/Web/content/ftp/serverstat.php index 0b27ac7..ca5ee0f 100644 --- a/Web/content/ftp/serverstat.php +++ b/Web/content/ftp/serverstat.php @@ -12,47 +12,20 @@ if (empty($_SESSION['admin']) || $_SESSION['admin'] !== true) { exit(); } -require_once __DIR__ . '/../config/config.php'; - -use phpseclib3\Net\SSH2; - -// $ssh_host = 'localhost'; -// $ssh_username = 'uname'; -// $ssh_password = 'pswd'; +require_once __DIR__ . '/../../config/config.php'; +require_once __DIR__ . '/../../includes/workspace.php'; +// The configuration helper authenticates using SSH_HOST, SSH_USER and SSH_PASS. $ssh = getSSHConnection(); -if (!$ssh->login($ssh_username, $ssh_password)) { - die('SSH login failed'); -} -function getDiskData($ssh, $device) { - return $ssh->exec("df -h $device"); -} +require_once __DIR__ . '/lib/serverstats.php'; -function getSystemData($ssh) { - // Get memory info using awk to clean up the output - $meminfo = $ssh->exec("free -m | awk 'NR==2{printf \"%s,%s,%s,\", $2,$3,$7} NR==3{printf \"%s,%s,%s\", $2,$3,$4}'"); - - // Use vmstat to get CPU idle percentage, more reliable and commonly available - $cpuIdle = $ssh->exec("vmstat 1 2 | tail -1 | awk '{print $15}'"); - - return [ - 'storage' => [ - 'system' => parseDiskData($ssh->exec('df -h /dev/ubuntu-vg/ubuntu-lv')), - 'data' => parseDiskData($ssh->exec('df -h /dev/sdb1')) - ], - 'raid' => parseRaidStatus($ssh->exec('cat /proc/mdstat')), - 'resources' => parseSystemStats($cpuIdle, $meminfo), - 'lastUpdate' => date('Y-m-d H:i:s') - ]; -} - -$systemData = getSystemData($ssh); +$systemData = getSystemData($ssh, $defaultPath); $monitors = [ [ 'id' => 'SystemDisk', - 'title' => 'System Disk (sda)', + 'title' => 'System filesystem', 'icon' => 'hdd', 'color' => 'primary', 'type' => 'storage', @@ -60,7 +33,7 @@ $monitors = [ ], [ 'id' => 'DataDisk', - 'title' => 'Data Disk (sdb)', + 'title' => 'Storage filesystem', 'icon' => 'hdd', 'color' => 'primary', 'type' => 'storage', @@ -91,349 +64,101 @@ $monitors = [ ] ]; -function parseDiskData($output) { - $lines = explode("\n", trim($output)); - if (count($lines) < 2) return null; - - $values = preg_split('/\s+/', trim($lines[1])); - return [ - 'size' => $values[1] ?? 'N/A', - 'used' => $values[2] ?? 'N/A', - 'available' => $values[3] ?? 'N/A', - 'usage' => $values[4] ?? '0%' - ]; -} - -function parseRaidStatus($output) { - if (empty($output)) { - return [ - 'active' => false, - 'status' => 'unknown', - 'type' => 'N/A' - ]; - } - - $status = [ - 'active' => false, - 'status' => 'unknown', - 'type' => 'N/A' - ]; - - $lines = explode("\n", trim($output)); - foreach ($lines as $line) { - if (preg_match('/active\s+(\w+)/', $line, $matches)) { - $status['active'] = true; - $status['type'] = $matches[1] ?? 'N/A'; - } - if (strpos($line, '[UU]') !== false) { - $status['status'] = 'healthy'; - } elseif (strpos($line, '_') !== false) { - $status['status'] = 'degraded'; - } - } - - return $status; -} - -function parseSystemStats($cpuOutput, $memOutput) { - // Parse CPU stats - convert idle percentage to usage percentage - $idlePercent = floatval(trim(str_replace(',', '.', $cpuOutput))); - $cpuUsage = 100 - $idlePercent; // Convert idle to usage percentage - if (is_nan($cpuUsage) || $cpuUsage < 0 || $cpuUsage > 100) { - $cpuUsage = 0; // Default to 0% if parsing fails - } - $cpu = ['usage' => number_format($cpuUsage, 1)]; - - // Parse memory stats - $parts = explode(',', $memOutput); - if (count($parts) >= 6) { - $memTotal = intval($parts[0]); - $memUsed = intval($parts[1]); - $memAvail = intval($parts[2]); - $swapTotal = intval($parts[3]); - $swapUsed = intval($parts[4]); - $swapFree = intval($parts[5]); - - $memory = [ - 'total' => number_format($memTotal / 1024, 2) . 'G', - 'used' => number_format($memUsed / 1024, 2) . 'G', - 'available' => number_format($memAvail / 1024, 2) . 'G', - 'usage' => ($memTotal > 0 ? round(($memUsed / $memTotal) * 100, 1) : 0) . '%', - 'swap_total' => number_format($swapTotal / 1024, 2) . 'G', - 'swap_used' => number_format($swapUsed / 1024, 2) . 'G', - 'swap_free' => number_format($swapFree / 1024, 2) . 'G', - 'swap_usage' => ($swapTotal > 0 ? round(($swapUsed / $swapTotal) * 100, 1) : 0) . '%' - ]; - } else { - $memory = [ - 'total' => '0G', - 'used' => '0G', - 'available' => '0G', - 'usage' => '0%', - 'swap_total' => '0G', - 'swap_used' => '0G', - 'swap_free' => '0G', - 'swap_usage' => '0%' - ]; - } - - return ['cpu' => $cpu, 'memory' => $memory]; -} - -function convertToBytes($size) { - if (preg_match('/^([\d.]+)([KMGT]?)i?B?$/', trim($size), $matches)) { - $value = floatval($matches[1]); - $unit = strtoupper($matches[2]); - - switch ($unit) { - case 'P': $value *= 1024; - case 'T': $value *= 1024; - case 'G': $value *= 1024; - case 'M': $value *= 1024; - case 'K': $value *= 1024; - } - - return $value; - } - return 0; -} - -function formatBytes($bytes, $forceUnit = '') { - $units = ['B', 'K', 'M', 'G', 'T', 'P']; - $bytes = max($bytes, 0); - - if ($forceUnit) { - $unitIndex = array_search($forceUnit, $units); - if ($unitIndex !== false) { - $bytes /= pow(1024, $unitIndex); - return round($bytes, 2) . $forceUnit; - } - } - - $pow = floor(($bytes ? log($bytes) : 0) / log(1024)); - $pow = min($pow, count($units) - 1); - $bytes /= pow(1024, $pow); - - return round($bytes, 2) . $units[$pow]; -} ?> - + Server Status - - - - - + + + + + - -
- -
+ '../../', + 'active' => 'server', + 'section' => 'Server status', + 'eyebrow' => 'Server overview', + 'title' => 'Server status', + 'description' => 'Keep an eye on your storage, RAID health and system resources.', + 'connected' => true, + 'class' => 'server-page', +]); ?> -
-
-

Server Status

- -
-
-
-
-
-
-
-

- - Server Disk Status -

- - - - -
-
- -
-
- -
-
-
-

- - -

-
-
- -
-
- Storage Usage - -
-
-
-
-
-
-
-
-
-
Total
-
-
-
-
-
-
Used
-
-
-
-
-
-
Free
-
-
-
-
- -
-
- -
-

Array

- - - -
- -
- -
- -
-
- Memory Usage - -
-
-
-
-
-
-
-
-
-
Total
-
-
-
-
-
-
Used
-
-
-
-
-
-
Available
-
-
-
-
-
-
- Swap Usage - -
-
-
-
-
-
-
-
-
-
Total
-
-
-
-
-
-
Used
-
-
-
-
-
-
Free
-
-
-
-
- -
-
-
+

Live statistics · refreshes every 5 seconds

+
+ +
+
+

+
+ + +

Storage statistics are unavailable for this path.

+ +

·

+
Storage usage
+
+
+ 'Total', 'used' => 'Used', 'available' => 'Free'] as $key => $label): ?> +
+ +
+ + +
+ +

+ + +

-
+ + +

CPU statistics are unavailable.

+ +
+ + + +

Memory statistics are unavailable.

+ + 'Memory usage', 'swap_usage' => 'Swap usage'] as $usageKey => $label): + $usagePercent = (float) rtrim($memData[$usageKey], '%'); + $fields = $usageKey === 'usage' ? ['total' => 'Total', 'used' => 'Used', 'available' => 'Available'] : ['swap_total' => 'Total', 'swap_used' => 'Used', 'swap_free' => 'Free']; + ?> +
+
+
+
+ $fieldLabel): ?> +
+ +
+
+ + +
-
+ +
+ -
- - - \ No newline at end of file + diff --git a/Web/content/ftp/shared.php b/Web/content/ftp/shared.php index 009733c..9f08bba 100644 --- a/Web/content/ftp/shared.php +++ b/Web/content/ftp/shared.php @@ -5,6 +5,7 @@ ini_set('display_errors', 1); error_reporting(E_ALL); require_once __DIR__ . '/../../config/config.php'; +require_once __DIR__ . '/../../includes/workspace.php'; // getDatabaseConnection relocated to central config @@ -98,11 +99,11 @@ if (isset($_GET['stream'])) { $matches = []; if (preg_match('/bytes=(\d+)-(\d*)/', $rangeHeader, $matches)) { $start = intval($matches[1]); - + if (!empty($matches[2])) { $end = intval($matches[2]); } - + $length = $end - $start + 1; header('HTTP/1.1 206 Partial Content'); @@ -135,13 +136,13 @@ if (isset($_GET['stream'])) { $readSize = min($chunkSize, $bytesRemaining); $chunkData = $sftp->get($filePath, false, $currentPosition, $readSize); - + if ($chunkData !== false) { $bytesSent = strlen($chunkData); echo $chunkData; $bytesRemaining -= $bytesSent; $currentPosition += $bytesSent; - + if (ob_get_level()) { ob_flush(); } @@ -175,25 +176,25 @@ if (isset($_GET['download'])) { if (!$downloadAllowed) { die("Download not permitted for this share."); } - + // Download file code similar to download.php header("Content-Type: application/octet-stream"); header("Content-Disposition: attachment; filename=\"$fileName\""); header("Content-Length: $fileSize"); - + $minChunkSize = 64 * 1024; $maxChunkSize = 2 * 1024 * 1024; $chunkSize = 256 * 1024; - + $currentPosition = 0; $bytesRemaining = $fileSize; - + set_time_limit(0); - + while ($bytesRemaining > 0) { $readSize = min($chunkSize, $bytesRemaining); $chunkData = $sftp->get($filePath, false, $currentPosition, $readSize); - + if ($chunkData !== false) { $bytesRead = strlen($chunkData); echo $chunkData; @@ -218,35 +219,32 @@ function formatBytes($bytes, $precision = 2) { ?> - + Shared Media - <?= htmlspecialchars($fileName) ?> - - - - - + + + + + - -
- -
-
-
-

Shared Media

-
- Shared Link -

This link will expire on

-
-
+ '../../', + 'active' => 'shared', + 'section' => 'Shared file', + 'eyebrow' => 'Shared with you', + 'title' => $fileName, + 'description' => 'Preview and access the file shared with you.', + 'guest' => true, + 'class' => 'media-page', +]); ?> -
+ +
Shared link

Available until

+
@@ -293,33 +291,20 @@ function formatBytes($bytes, $precision = 2) {
- -
+ + - + + + + + - -
- -
-
-
-

Media Viewer

- -
+ '../../', + 'active' => 'files', + 'section' => 'Media preview', + 'eyebrow' => 'Media preview', + 'title' => $fileName, + 'description' => 'Preview your media, download a copy or create a shared link.', + 'connected' => true, + 'back' => 'index.php?path=' . urlencode(dirname($filePath)), + 'class' => 'media-page', +]); ?> -
+ +
@@ -305,13 +304,13 @@ if (isset($_GET['stream'])) { Download - + -
- +
@@ -319,7 +318,7 @@ if (isset($_GET['stream'])) {

This link will expire on

@@ -356,33 +355,20 @@ if (isset($_GET['stream'])) {
- -
+ - + - + - - -
- -
- -
-
-

USB RAID Array

-
- -
-
-
-
-
- -
- -
-
- -
- -
-
- -
-
-
-
-
- - -
- + + + +
+ + +
+ diff --git a/Web/js/theme.js b/Web/js/theme.js index 2f64187..0e6161c 100644 --- a/Web/js/theme.js +++ b/Web/js/theme.js @@ -1,40 +1,26 @@ -document.addEventListener('DOMContentLoaded', function() { - const themeToggle = document.getElementById('themeToggle'); - const html = document.documentElement; - const themeText = document.getElementById('themeText'); - const themeIcon = themeToggle.querySelector('.bi'); - +(() => { + const preference = window.matchMedia('(prefers-color-scheme: dark)'); + let saved; + try { saved = localStorage.getItem('theme'); } catch (_) { /* Storage may be disabled. */ } function setTheme(theme) { - html.setAttribute('data-bs-theme', theme); - document.body.classList.remove('theme-light', 'theme-dark'); - document.body.classList.add('theme-' + theme); - localStorage.setItem('theme', theme); - - if (theme === 'dark') { - themeText.textContent = 'Light Mode'; - themeIcon.className = 'bi bi-sun'; - themeToggle.classList.remove('btn-dark'); - themeToggle.classList.add('btn-light'); - } else { - themeText.textContent = 'Dark Mode'; - themeIcon.className = 'bi bi-moon'; - themeToggle.classList.remove('btn-light'); - themeToggle.classList.add('btn-dark'); + document.documentElement.dataset.theme = theme; + const button = document.getElementById('themeToggle'); + if (button) { + button.setAttribute('aria-label', `Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`); + const label = document.getElementById('themeText'); + if (label) label.textContent = theme === 'dark' ? 'Light mode' : 'Dark mode'; + const icon = button.querySelector('i'); + if (icon) { icon.textContent = theme === 'dark' ? '☀' : '◐'; icon.setAttribute('aria-hidden', 'true'); } } } - - const savedTheme = localStorage.getItem('theme'); - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; - - if (savedTheme) { - setTheme(savedTheme); - } else { - setTheme(prefersDark ? 'dark' : 'light'); - } - - themeToggle.addEventListener('click', function() { - const currentTheme = html.getAttribute('data-bs-theme'); - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - setTheme(newTheme); + setTheme(saved === 'dark' || saved === 'light' ? saved : preference.matches ? 'dark' : 'light'); + document.addEventListener('DOMContentLoaded', () => { + setTheme(document.documentElement.dataset.theme); + document.getElementById('themeToggle')?.addEventListener('click', () => { + saved = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; + setTheme(saved); + try { localStorage.setItem('theme', saved); } catch (_) { /* Keep this session usable. */ } + }); }); -}); \ No newline at end of file + preference.addEventListener('change', () => { if (saved !== 'dark' && saved !== 'light') setTheme(preference.matches ? 'dark' : 'light'); }); +})(); diff --git a/Web/js/ui.js b/Web/js/ui.js new file mode 100644 index 0000000..c10c579 --- /dev/null +++ b/Web/js/ui.js @@ -0,0 +1,10 @@ +// Native dialogs supply focus containment, Escape and focus restoration. +document.addEventListener('click', event => { + const trigger = event.target.closest('[data-dialog]'); + if (trigger) document.getElementById(trigger.dataset.dialog)?.showModal(); + if (event.target.closest('[data-dialog-close]')) event.target.closest('dialog')?.close(); + if (event.target instanceof HTMLDialogElement) { + const rect = event.target.getBoundingClientRect(); + if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) event.target.close(); + } +}); diff --git a/tests/serverstats_test.php b/tests/serverstats_test.php new file mode 100644 index 0000000..925c6c5 --- /dev/null +++ b/tests/serverstats_test.php @@ -0,0 +1,57 @@ +\n"; +check(parseRaidStatus($healthy)['status'] === 'healthy', 'Three-disk RAID5 should be healthy'); +check(parseRaidStatus(str_replace('[UUU]', '[U_U]', $healthy))['status'] === 'degraded', 'Missing RAID member should be degraded'); +check(parseRaidStatus($healthy . "md1 : active raid1 sdd[0] sde[1]\n 1000 blocks [2/1] [U_]\n")['status'] === 'degraded', 'One failed array must make overall status degraded'); +check(parseRaidStatus(str_replace('[UUU]', "[UUU]\n resync = 42.0%", $healthy))['status'] === 'syncing', 'RAID resync is visible'); +check(parseRaidStatus("md0 : active raid0 sda[0] sdb[1]\n 1000 blocks super 1.2 512k chunks\n")['status'] === 'active', 'RAID0 does not require a redundancy bitmap'); +check(parseRaidStatus("md0 : inactive sda[0](S)\n 1000 blocks\n")['status'] === 'inactive', 'Inactive array remains inactive'); +check(parseRaidStatus("Personalities : [raid1]\nunused devices: \n")['status'] === 'not configured', 'No configured Linux MD array'); +check(parseRaidStatus(false)['status'] === 'unavailable', 'Unreadable mdstat is unavailable'); +$resources = parseSystemStats('97,5', '1024,256,768,0,0,0'); +check($resources['cpu']['usage'] === '2.5' && $resources['memory']['usage'] === '25%' && $resources['memory']['swap_usage'] === '0%', 'Resource values and zero swap'); +check(!parseSystemStats(false, false)['cpu']['available_data'], 'Missing CPU output must not show 100%'); +check(!parseSystemStats('not a number', '')['memory']['available_data'], 'Missing memory output must be unavailable'); + +// Check remote command construction and path quoting with a harmless stub. +$ssh = new class { + public $commands = []; + function exec($command) { $this->commands[] = $command; return false; } +}; +$defaultPath = "/storage/owner's files"; +$systemData = getSystemData($ssh, $defaultPath); +check(in_array("LC_ALL=C df -Pk -- '/' 2>/dev/null", $ssh->commands, true), 'System filesystem uses root instead of a device name'); +check(in_array("LC_ALL=C df -Pk -- '/storage/owner'\\''s files' 2>/dev/null", $ssh->commands, true), 'Storage path is quoted for remote Linux even on Windows'); + +// Exercise the actual page template with missing output and with valid output. +$source = file_get_contents(__DIR__ . '/../Web/content/ftp/serverstat.php'); +$template = substr($source, strpos($source, '$monitors =')); +foreach ([false, true] as $available) { + if ($available) { + $systemData['storage'] = ['system' => $disk, 'data' => $disk]; + $systemData['resources'] = $resources; + $systemData['raid'] = parseRaidStatus($healthy); + } + ob_start(); + eval($template); + $html = ob_get_clean(); + check(strpos($html, 'Server status') !== false, 'Status page renders'); + check($available ? strpos($html, '1 GiB') !== false : strpos($html, 'Storage statistics are unavailable') !== false, 'Template handles available and missing data'); +} +echo "Server statistics: parser, RAID, command quoting and page rendering checks passed.\n"; diff --git a/tests/workspace_test.php b/tests/workspace_test.php new file mode 100644 index 0000000..7daa995 --- /dev/null +++ b/tests/workspace_test.php @@ -0,0 +1,89 @@ + 'michal', 'admin' => $admin, 'upPer' => true, 'downPer' => true, 'delPer' => true]; + $defaultPath = '/mnt/raid'; $currentPath = '/mnt/raid/Workspace'; + $filePath = $currentPath . '/Example.pdf'; $fileName = basename($filePath); $parentDir = $currentPath; + $fileExtension = 'png'; $fileSize = 245760; $mimeType = 'image/png'; + $isImage = true; $isVideo = false; $isAudio = false; + $editable = true; $showLineNumbers = true; $content = "{\n \"workspace\": \"USB RAID\",\n \"status\": \"connected\"\n}\n"; + $message = ''; $messageType = ''; $message_type = ''; $shareCreated = false; $shareError = ''; + $share = ['expiration' => '2030-01-01']; $downloadAllowed = false; $token = 'test-only'; + $items = ['Documents', 'Photos', 'README.md', 'Example.pdf']; + $imageExtensions = ['png']; $videoExtensions = []; $audioExtensions = []; $editableExtensions = ['md']; $archiveExtensions = []; + $sftp = new class { + function is_dir($name) { return strpos($name, '.') === false; } + function stat($path) { return ['size' => 245760]; } + }; + $result = new class { + private $rows = [ + ['uname' => 'michal', 'admin' => true, 'defPath' => '/mnt/raid', 'delPer' => true, 'downPer' => true, 'upPer' => true], + ['uname' => 'guest', 'admin' => false, 'defPath' => '/mnt/raid/shared', 'delPer' => false, 'downPer' => true, 'upPer' => false], + ]; + function fetch_assoc() { return array_shift($this->rows); } + }; + $systemData = [ + 'storage' => array_fill_keys(['system', 'data'], parseDiskData("Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/md0 1048576 262144 786432 25% /mnt/raid\n")), + 'raid' => parseRaidStatus("md0 : active raid5 sda[0] sdb[1] sdc[2]\n 1000 blocks [3/3] [UUU]\n"), + 'resources' => parseSystemStats('93', '8192,2048,6144,1024,0,1024'), 'lastUpdate' => '2026-10-16 10:00:00', + ]; + $top = in_array($name, ['adminpanel', 'changepassword'], true); + $source = file_get_contents(__DIR__ . '/../Web/content/' . ($top ? '' : 'ftp/') . $name . '.php'); + if ($name === 'serverstat') { + $start = strpos($source, '$monitors ='); + eval(substr($source, $start, strpos($source, '?>', $start) - $start)); + } + $start = strpos($source, ''); + $template = substr($source, $start, strpos($source, '', $start) + 7 - $start); + ob_start(); eval('?>' . $template); return ob_get_clean(); +} + +$preview = in_array('--preview', $argv, true); +$directory = __DIR__ . '/../Web/.layout-preview'; +if ($preview && !is_dir($directory)) mkdir($directory); +$pages = ['index', 'serverstat', 'adminpanel', 'changepassword', 'open', 'view', 'pdf', 'shared']; +foreach ($pages as $name) { + $html = renderWorkspaceFixture($name); + $dom = new DOMDocument(); + @$dom->loadHTML($html); + $xpath = new DOMXPath($dom); + expectWorkspace($xpath->query('//body')->length === 1, "$name has one body"); + expectWorkspace($xpath->query('//main')->length === 1, "$name has one main region"); + expectWorkspace($xpath->query('//aside[contains(@class,"sidebar")]')->length === 1, "$name has shared sidebar"); + expectWorkspace($xpath->query('//*[@id="themeToggle"]')->length === 1, "$name has one theme switch"); + expectWorkspace($xpath->query('//a[@aria-current="page"]')->length === 1, "$name has one active navigation item"); + expectWorkspace($xpath->query('//main//footer')->length === 1, "$name footer is inside the main region"); + if ($name === 'shared') expectWorkspace(strpos($html, 'User management') === false && strpos($html, 'Account settings') === false, 'Public shared preview does not expose account navigation'); + if ($preview) { + $base = $name === 'adminpanel' || $name === 'changepassword' ? '../content/' : '../content/ftp/'; + $html = str_replace('', '', $html); + // Browser previews never request a real remote file or perform mutations. + $image = 'data:image/svg+xml,' . rawurlencode('USB RAID / Workspace preview'); + $html = preg_replace('/src="(?:view|shared)\.php\?[^"\n]+"/', 'src="' . $image . '"', $html); + $html = str_replace("const url = 'pdf.php?file=%2Fmnt%2Fraid%2FWorkspace%2FExample.pdf&stream=1';", "const url = '/Web/.layout-preview/sample.pdf';", $html); + file_put_contents($directory . '/' . $name . '.html', $html); + } +} +$member = renderWorkspaceFixture('index', false); +expectWorkspace(strpos($member, 'User management') === false && strpos($member, '> Server status') === false, 'Member navigation omits admin pages'); +expectWorkspace(strpos($member, 'Account settings') !== false, 'Members can access account settings'); +if ($preview) { + // One-page PDF fixture for viewer navigation and fullscreen checks. + $stream = "BT /F1 24 Tf 60 700 Td (USB RAID workspace preview) Tj ET"; + $objects = ['<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>', '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>', '<< /Length ' . strlen($stream) . ">>\nstream\n" . $stream . "\nendstream"]; + $pdf = "%PDF-1.4\n"; $offsets = [0]; + foreach ($objects as $i => $object) { $offsets[] = strlen($pdf); $pdf .= ($i + 1) . " 0 obj\n$object\nendobj\n"; } + $xref = strlen($pdf); $pdf .= "xref\n0 6\n0000000000 65535 f \n"; + foreach (array_slice($offsets, 1) as $offset) $pdf .= sprintf("%010d 00000 n \n", $offset); + $pdf .= "trailer\n<< /Size 6 /Root 1 0 R >>\nstartxref\n$xref\n%%EOF"; + file_put_contents($directory . '/sample.pdf', $pdf); +} +echo "Workspace: all eight page templates, navigation roles and shared layout checks passed.\n";