From 565fd1837967a3e1823f41d6cd7a012b016369a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Sedl=C3=A1k?= Date: Fri, 16 Oct 2026 10:20:55 +0200 Subject: [PATCH] Redesign SFTP workspace and fix server monitoring Replace Bootstrap dependencies with custom blue-accented light and dark themes and native dialogs. Share the sidebar, top bar and responsive layout across file browsing, account settings, user management, server statistics and file previews. Add folder search, type filters, upload progress and asset cache versioning. Fix SSH configuration usage, filesystem discovery, missing statistics and RAID health parsing. Adapt PDF sizing and fullscreen controls to the shared workspace. Validation: PHP workspace and server statistics regression tests, JavaScript syntax checks, responsive browser checks and live XAMPP navigation. --- Web/content/adminpanel.php | 90 +++--- Web/content/changepassword.php | 89 ++---- Web/content/css/adminpanel.css | 79 +---- Web/content/css/changepassword.css | 62 +--- Web/content/ftp/css/index.css | 156 +-------- Web/content/ftp/css/open.css | 60 +--- Web/content/ftp/css/pdf.css | 268 +--------------- Web/content/ftp/css/serverstat.css | 142 ++------ Web/content/ftp/css/view.css | 97 +----- Web/content/ftp/index.php | 161 ++++------ Web/content/ftp/js/index.js | 79 ++--- Web/content/ftp/lib/serverstats.php | 108 +++++++ Web/content/ftp/open.php | 79 ++--- Web/content/ftp/pdf.php | 159 +++++---- Web/content/ftp/serverstat.php | 480 ++++++---------------------- Web/content/ftp/shared.php | 83 ++--- Web/content/ftp/view.php | 108 +++---- Web/css/index.css | 58 +--- Web/css/modern.css | 252 ++++----------- Web/css/workspace.css | 62 ++++ Web/includes/workspace.php | 66 ++++ Web/index.php | 70 +--- Web/js/theme.js | 58 ++-- Web/js/ui.js | 10 + tests/serverstats_test.php | 57 ++++ tests/workspace_test.php | 89 ++++++ 26 files changed, 1058 insertions(+), 1964 deletions(-) create mode 100644 Web/content/ftp/lib/serverstats.php create mode 100644 Web/css/workspace.css create mode 100644 Web/includes/workspace.php create mode 100644 Web/js/ui.js create mode 100644 tests/serverstats_test.php create mode 100644 tests/workspace_test.php 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";