mbkbucket 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -2
- package/README.md +59 -14
- package/index.d.ts +67 -3
- package/index.js +4 -79
- package/lib/bucket.js +658 -405
- package/lib/config/index.js +134 -1
- package/lib/s3.js +297 -54
- package/package.json +2 -1
- package/public/bucketadmin.js +139 -33
- package/views/bucketadmincontent.handlebars +3 -2
- package/views/mbkbucket_info.handlebars +226 -0
- package/views/info.handlebars +0 -1056
package/public/bucketadmin.js
CHANGED
|
@@ -347,6 +347,9 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
347
347
|
// 1. Show files where FILENAME matches the search term
|
|
348
348
|
const searchLower = currentSearch.toLowerCase();
|
|
349
349
|
displayFiles = files.filter(f => {
|
|
350
|
+
// Skip folder marker objects (keys ending with /)
|
|
351
|
+
if (f.Key.endsWith('/')) return false;
|
|
352
|
+
|
|
350
353
|
const fileName = f.Key.split('/').pop();
|
|
351
354
|
return fileName.toLowerCase().includes(searchLower);
|
|
352
355
|
});
|
|
@@ -355,13 +358,17 @@ function renderFilesTable(files, folders = [], prefix = '') {
|
|
|
355
358
|
displayFolders = extractMatchingFolders(files, currentSearch);
|
|
356
359
|
} else {
|
|
357
360
|
// Normal Flat Mode: Show all files, no folders
|
|
358
|
-
|
|
361
|
+
// Filter out folder marker objects (keys ending with /)
|
|
362
|
+
displayFiles = files.filter(f => !f.Key.endsWith('/'));
|
|
359
363
|
displayFolders = [];
|
|
360
364
|
}
|
|
361
365
|
} else {
|
|
362
366
|
// Normal Folder View logic
|
|
363
367
|
// Filter files to show only those at current level (not in subfolders)
|
|
364
368
|
displayFiles = files.filter(file => {
|
|
369
|
+
// Skip folder marker objects (keys ending with /)
|
|
370
|
+
if (file.Key.endsWith('/')) return false;
|
|
371
|
+
|
|
365
372
|
const relativePath = prefix ? file.Key.substring(prefix.length + (prefix.endsWith('/') ? 0 : 1)) : file.Key;
|
|
366
373
|
// File is at current level if it has no '/' in relative path
|
|
367
374
|
return !relativePath.includes('/');
|
|
@@ -823,7 +830,7 @@ function createId() { return Date.now().toString(36) + '-' + Math.random().toStr
|
|
|
823
830
|
|
|
824
831
|
function addFilesToQueue(fileList) {
|
|
825
832
|
for (const f of Array.from(fileList)) {
|
|
826
|
-
uploadQueue.push({ id: createId(), file: f, status: 'queued', progress: 0 });
|
|
833
|
+
uploadQueue.push({ id: createId(), file: f, status: 'queued', progress: 0, uploadedBytes: 0, uploadRate: 0 });
|
|
827
834
|
}
|
|
828
835
|
renderUploadQueue();
|
|
829
836
|
}
|
|
@@ -839,7 +846,27 @@ function renderUploadQueue() {
|
|
|
839
846
|
return;
|
|
840
847
|
}
|
|
841
848
|
|
|
842
|
-
|
|
849
|
+
// Local size formatter so it's always in scope regardless of IIFE load order
|
|
850
|
+
function fmtSize(bytes) {
|
|
851
|
+
if (!bytes || bytes === 0) return '0 B';
|
|
852
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
853
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
854
|
+
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
container.innerHTML = uploadQueue.map(item => {
|
|
858
|
+
let statusText;
|
|
859
|
+
if (item.status === 'uploading') {
|
|
860
|
+
const pct = Math.round(item.progress || 0);
|
|
861
|
+
statusText = fmtSize(item.uploadedBytes || 0) + ' / ' + fmtSize(item.file.size) + ' (' + pct + '%)';
|
|
862
|
+
if (item.uploadRate > 0) statusText += ' \u00b7 ' + fmtSize(item.uploadRate) + '/s';
|
|
863
|
+
} else {
|
|
864
|
+
statusText = item.status;
|
|
865
|
+
}
|
|
866
|
+
const actionBtn = item.status === 'uploading'
|
|
867
|
+
? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>`
|
|
868
|
+
: `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`;
|
|
869
|
+
return `
|
|
843
870
|
<div class="upload-item" data-id="${item.id}">
|
|
844
871
|
<div class="meta">
|
|
845
872
|
<div style="display:flex; align-items:center; gap:0.75rem; min-width:0; width:100%; margin-bottom:5px;">
|
|
@@ -853,14 +880,12 @@ function renderUploadQueue() {
|
|
|
853
880
|
<div class="progress-bar" style="width: ${Math.round(item.progress)}%; height: 8px;"></div>
|
|
854
881
|
</div>
|
|
855
882
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem;">
|
|
856
|
-
<small class="text-muted status">${
|
|
857
|
-
<div class="controls">
|
|
858
|
-
${item.status === 'uploading' ? `<button class="btn btn-sm btn-outline-secondary cancel-upload" data-id="${item.id}"><i class="fas fa-ban"></i></button>` : `<button class="btn btn-sm btn-danger remove-queued" data-id="${item.id}"><i class="fas fa-trash"></i></button>`}
|
|
859
|
-
</div>
|
|
883
|
+
<small class="text-muted status">${statusText}</small>
|
|
884
|
+
<div class="controls">${actionBtn}</div>
|
|
860
885
|
</div>
|
|
861
886
|
</div>
|
|
862
|
-
</div
|
|
863
|
-
|
|
887
|
+
</div>`;
|
|
888
|
+
}).join('');
|
|
864
889
|
|
|
865
890
|
// Wire up controls
|
|
866
891
|
document.querySelectorAll('.remove-queued').forEach(btn => btn.addEventListener('click', function (e) {
|
|
@@ -870,15 +895,27 @@ function renderUploadQueue() {
|
|
|
870
895
|
renderUploadQueue();
|
|
871
896
|
}));
|
|
872
897
|
|
|
873
|
-
document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', function (e) {
|
|
898
|
+
document.querySelectorAll('.cancel-upload').forEach(btn => btn.addEventListener('click', async function (e) {
|
|
874
899
|
const id = this.dataset.id;
|
|
875
900
|
const handle = xhrMap.get(id);
|
|
876
|
-
if (handle && handle.abort) {
|
|
877
|
-
handle.abort();
|
|
878
|
-
} else if (handle && handle.xhr) {
|
|
879
|
-
handle.xhr.abort();
|
|
880
|
-
}
|
|
881
901
|
const entry = uploadQueue.find(i => i.id === id);
|
|
902
|
+
|
|
903
|
+
// Abort in-flight fetch
|
|
904
|
+
if (handle && handle.controller) handle.controller.abort();
|
|
905
|
+
// Abort legacy XHR (small file path)
|
|
906
|
+
if (handle && handle.xhr) handle.xhr.abort();
|
|
907
|
+
|
|
908
|
+
// Tell S3 to discard the incomplete multipart upload
|
|
909
|
+
if (entry && entry.s3UploadId && entry.s3Key) {
|
|
910
|
+
try {
|
|
911
|
+
await fetch('/mbkbucket/upload-abort', {
|
|
912
|
+
method: 'POST',
|
|
913
|
+
headers: { 'Content-Type': 'application/json' },
|
|
914
|
+
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
915
|
+
});
|
|
916
|
+
} catch (_) { /* best-effort */ }
|
|
917
|
+
}
|
|
918
|
+
|
|
882
919
|
if (entry) { entry.status = 'cancelled'; entry.progress = 0; }
|
|
883
920
|
xhrMap.delete(id);
|
|
884
921
|
renderUploadQueue();
|
|
@@ -913,41 +950,90 @@ async function startUploadEntry(entry) {
|
|
|
913
950
|
const file = entry.file;
|
|
914
951
|
try {
|
|
915
952
|
if (file.size > CHUNK_SIZE) {
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
953
|
+
// ---------------------------------------------------------------
|
|
954
|
+
// S3 Multipart Upload
|
|
955
|
+
// Step 1 — Initiate: get uploadId + key from the server
|
|
956
|
+
// ---------------------------------------------------------------
|
|
957
|
+
const initResp = await fetch('/mbkbucket/upload-init', {
|
|
958
|
+
method: 'POST',
|
|
959
|
+
headers: { 'Content-Type': 'application/json' },
|
|
960
|
+
body: JSON.stringify({
|
|
961
|
+
fileName: file.name,
|
|
962
|
+
prefix: currentPrefix,
|
|
963
|
+
contentType: file.type || 'application/octet-stream'
|
|
964
|
+
})
|
|
965
|
+
});
|
|
966
|
+
const initJson = await initResp.json();
|
|
967
|
+
if (!initResp.ok || !initJson.success) throw new Error(initJson.error || 'Failed to initiate upload');
|
|
968
|
+
|
|
969
|
+
const { uploadId, key } = initJson;
|
|
970
|
+
entry.s3UploadId = uploadId; // stored so cancel handler can abort
|
|
971
|
+
entry.s3Key = key;
|
|
972
|
+
|
|
973
|
+
// ---------------------------------------------------------------
|
|
974
|
+
// Step 2 — Upload parts (1-based partNumber, min 5 MB except last)
|
|
975
|
+
// ---------------------------------------------------------------
|
|
919
976
|
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
|
|
977
|
+
const parts = []; // [{ partNumber, ETag }]
|
|
920
978
|
let uploadedBytes = 0;
|
|
979
|
+
|
|
921
980
|
for (let i = 0; i < totalChunks; i++) {
|
|
922
|
-
|
|
981
|
+
const _chunkStart = Date.now();
|
|
982
|
+
if (entry.status === 'cancelled') {
|
|
983
|
+
// Clean up on S3
|
|
984
|
+
await fetch('/mbkbucket/upload-abort', {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: { 'Content-Type': 'application/json' },
|
|
987
|
+
body: JSON.stringify({ uploadId, key })
|
|
988
|
+
}).catch(() => {});
|
|
989
|
+
throw new Error('Upload cancelled');
|
|
990
|
+
}
|
|
991
|
+
|
|
923
992
|
const start = i * CHUNK_SIZE;
|
|
924
|
-
const
|
|
925
|
-
const
|
|
993
|
+
const blob = file.slice(start, Math.min(file.size, start + CHUNK_SIZE));
|
|
994
|
+
const partNumber = i + 1; // S3 parts are 1-based
|
|
926
995
|
|
|
927
996
|
const fd = new FormData();
|
|
928
997
|
fd.append('chunk', blob, file.name);
|
|
929
998
|
fd.append('uploadId', uploadId);
|
|
930
|
-
fd.append('
|
|
931
|
-
fd.append('
|
|
932
|
-
fd.append('totalChunks', String(totalChunks));
|
|
999
|
+
fd.append('key', key);
|
|
1000
|
+
fd.append('partNumber', String(partNumber));
|
|
933
1001
|
|
|
934
1002
|
const controller = new AbortController();
|
|
935
|
-
xhrMap.set(entry.id, { controller });
|
|
1003
|
+
xhrMap.set(entry.id, { controller, s3UploadId: uploadId, s3Key: key });
|
|
1004
|
+
|
|
1005
|
+
const chunkResp = await fetch('/mbkbucket/upload-chunk', {
|
|
1006
|
+
method: 'POST',
|
|
1007
|
+
body: fd,
|
|
1008
|
+
signal: controller.signal
|
|
1009
|
+
});
|
|
1010
|
+
const chunkJson = await chunkResp.json();
|
|
1011
|
+
if (!chunkResp.ok || !chunkJson.success) throw new Error(chunkJson.error || `Part ${partNumber} upload failed`);
|
|
936
1012
|
|
|
937
|
-
|
|
938
|
-
if (!resp.ok) throw new Error('Chunk upload failed');
|
|
1013
|
+
parts.push({ partNumber: chunkJson.partNumber, ETag: chunkJson.ETag });
|
|
939
1014
|
uploadedBytes += blob.size;
|
|
940
|
-
|
|
1015
|
+
const _chunkDuration = (Date.now() - _chunkStart) / 1000;
|
|
1016
|
+
entry.uploadRate = _chunkDuration > 0 ? blob.size / _chunkDuration : 0;
|
|
1017
|
+
entry.uploadedBytes = uploadedBytes;
|
|
1018
|
+
entry.progress = (uploadedBytes / file.size) * 95; // reserve last 5% for complete call
|
|
941
1019
|
renderUploadQueue();
|
|
942
1020
|
}
|
|
943
1021
|
|
|
944
|
-
//
|
|
945
|
-
|
|
1022
|
+
// ---------------------------------------------------------------
|
|
1023
|
+
// Step 3 — Complete: assemble all parts on S3
|
|
1024
|
+
// ---------------------------------------------------------------
|
|
1025
|
+
const completeResp = await fetch('/mbkbucket/upload-complete', {
|
|
1026
|
+
method: 'POST',
|
|
1027
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1028
|
+
body: JSON.stringify({ uploadId, key, parts })
|
|
1029
|
+
});
|
|
946
1030
|
const completeJson = await completeResp.json();
|
|
947
|
-
if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || '
|
|
1031
|
+
if (!completeResp.ok || !completeJson.success) throw new Error(completeJson.error || 'Failed to complete upload');
|
|
948
1032
|
|
|
949
|
-
entry.status = 'done'; entry.progress = 100;
|
|
950
|
-
|
|
1033
|
+
entry.status = 'done'; entry.progress = 100;
|
|
1034
|
+
xhrMap.delete(entry.id);
|
|
1035
|
+
renderUploadQueue();
|
|
1036
|
+
showAlert(`${file.name} uploaded successfully`, 'success');
|
|
951
1037
|
loadFiles(currentPage, currentPrefix);
|
|
952
1038
|
} else {
|
|
953
1039
|
// small file XHR
|
|
@@ -960,9 +1046,20 @@ async function startUploadEntry(entry) {
|
|
|
960
1046
|
|
|
961
1047
|
xhr.upload.addEventListener('progress', (e) => {
|
|
962
1048
|
if (!e.lengthComputable) return;
|
|
1049
|
+
const _now = Date.now();
|
|
1050
|
+
const _dt = (_now - (entry._rateLastTime || _now)) / 1000;
|
|
1051
|
+
const _db = e.loaded - (entry._rateLastBytes || 0);
|
|
1052
|
+
if (_dt > 0.2) {
|
|
1053
|
+
entry.uploadRate = _db / _dt;
|
|
1054
|
+
entry._rateLastTime = _now;
|
|
1055
|
+
entry._rateLastBytes = e.loaded;
|
|
1056
|
+
}
|
|
1057
|
+
entry.uploadedBytes = e.loaded;
|
|
963
1058
|
entry.progress = (e.loaded / e.total) * 100;
|
|
964
1059
|
renderUploadQueue();
|
|
965
1060
|
});
|
|
1061
|
+
entry._rateLastTime = Date.now();
|
|
1062
|
+
entry._rateLastBytes = 0;
|
|
966
1063
|
|
|
967
1064
|
xhr.addEventListener('load', () => {
|
|
968
1065
|
try {
|
|
@@ -990,7 +1087,16 @@ async function startUploadEntry(entry) {
|
|
|
990
1087
|
}
|
|
991
1088
|
} catch (err) {
|
|
992
1089
|
console.error('Upload failed for', entry.file.name, err);
|
|
1090
|
+
// If a multipart upload was initiated but didn't finish (not a user cancel), abort it on S3
|
|
1091
|
+
if (entry.s3UploadId && entry.s3Key && entry.status !== 'cancelled') {
|
|
1092
|
+
fetch('/mbkbucket/upload-abort', {
|
|
1093
|
+
method: 'POST',
|
|
1094
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1095
|
+
body: JSON.stringify({ uploadId: entry.s3UploadId, key: entry.s3Key })
|
|
1096
|
+
}).catch(() => {});
|
|
1097
|
+
}
|
|
993
1098
|
entry.status = entry.status === 'cancelled' ? 'cancelled' : 'error';
|
|
1099
|
+
xhrMap.delete(entry.id);
|
|
994
1100
|
renderUploadQueue();
|
|
995
1101
|
showAlert('Upload failed: ' + (err.message || 'unknown'), 'error');
|
|
996
1102
|
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
<p id="filterInfo">Loading file information...</p>
|
|
14
14
|
</div>
|
|
15
15
|
<div style="margin-left: auto;">
|
|
16
|
-
<
|
|
17
|
-
|
|
16
|
+
<span style="background:rgba(255,255,255,0.2); padding:0.35rem 0.75rem; border-radius:0.5rem; font-size:0.85rem; font-weight:600; color:white; letter-spacing:0.02em;">
|
|
17
|
+
<i class="fas fa-database" style="margin-right:0.4rem;"></i>{{bucketvar}}
|
|
18
|
+
</span>
|
|
18
19
|
</div>
|
|
19
20
|
</div>
|
|
20
21
|
</div>
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<meta name="description"
|
|
8
|
+
content="Log in to portal.mbktech.org to access your resources and manage projects securely.">
|
|
9
|
+
<meta name="keywords" content="MBK Tech, Web-Portal, Web, Portal, Admin-Panel, Admin, login">
|
|
10
|
+
<meta property="og:title" content="Version and Configuration Information | MBK Tech" />
|
|
11
|
+
<meta property="og:image" content="https://mbktech.org/Assets/Images/Icon/logo.png" />
|
|
12
|
+
<meta property="og:url" content="/mbkbucket/info">
|
|
13
|
+
<title>Version and Configuration Information | MBK Tech Portal</title>
|
|
14
|
+
<link rel="icon" type="image/svg+xml" href="/icon.svg">
|
|
15
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
16
|
+
{{> sharedStyles}}
|
|
17
|
+
<style>
|
|
18
|
+
.info-box {
|
|
19
|
+
background: rgba(10, 20, 20, 0.95);
|
|
20
|
+
backdrop-filter: blur(10px);
|
|
21
|
+
border-radius: var(--border-radius);
|
|
22
|
+
padding: 2.5rem;
|
|
23
|
+
width: 100%;
|
|
24
|
+
max-width: 900px;
|
|
25
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
26
|
+
border: 1px solid rgba(0, 184, 148, 0.2);
|
|
27
|
+
position: relative;
|
|
28
|
+
z-index: 2;
|
|
29
|
+
transition: var(--transition);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.info-box:hover {
|
|
33
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
|
34
|
+
border-color: rgba(0, 184, 148, 0.3);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.info-title {
|
|
38
|
+
text-align: center;
|
|
39
|
+
margin-bottom: 2rem;
|
|
40
|
+
font-size: 2rem;
|
|
41
|
+
position: relative;
|
|
42
|
+
color: var(--light);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.info-title::after {
|
|
46
|
+
content: '';
|
|
47
|
+
position: absolute;
|
|
48
|
+
bottom: -10px;
|
|
49
|
+
left: 50%;
|
|
50
|
+
transform: translateX(-50%);
|
|
51
|
+
width: 100px;
|
|
52
|
+
height: 4px;
|
|
53
|
+
background-color: var(--accent);
|
|
54
|
+
border-radius: 2px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.info-section {
|
|
58
|
+
margin-bottom: 2rem;
|
|
59
|
+
padding: 1.5rem;
|
|
60
|
+
border: 1px solid rgba(0, 184, 148, 0.2);
|
|
61
|
+
border-radius: var(--border-radius);
|
|
62
|
+
background-color: rgba(0, 0, 0, 0.2);
|
|
63
|
+
transition: var(--transition);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.info-section:hover {
|
|
67
|
+
border-color: rgba(0, 184, 148, 0.4);
|
|
68
|
+
background-color: rgba(0, 0, 0, 0.3);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.info-section h2 {
|
|
72
|
+
color: var(--accent);
|
|
73
|
+
border-bottom: 2px solid rgba(0, 184, 148, 0.3);
|
|
74
|
+
padding-bottom: 10px;
|
|
75
|
+
margin-top: 0;
|
|
76
|
+
margin-bottom: 1.5rem;
|
|
77
|
+
font-size: 1.4rem;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 10px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.info-section h2 i {
|
|
84
|
+
font-size: 1.2rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.info-row {
|
|
88
|
+
display: flex;
|
|
89
|
+
margin-bottom: 1rem;
|
|
90
|
+
padding-bottom: 1rem;
|
|
91
|
+
border-bottom: 1px solid rgba(0, 184, 148, 0.1);
|
|
92
|
+
align-items: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.info-row:last-child {
|
|
96
|
+
border-bottom: none;
|
|
97
|
+
margin-bottom: 0;
|
|
98
|
+
padding-bottom: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.info-label {
|
|
102
|
+
font-weight: 600;
|
|
103
|
+
color: var(--text-light);
|
|
104
|
+
min-width: 240px;
|
|
105
|
+
font-size: 1rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.info-value {
|
|
109
|
+
flex: 1;
|
|
110
|
+
word-break: break-word;
|
|
111
|
+
color: var(--text);
|
|
112
|
+
font-size: 1rem;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.version-status {
|
|
116
|
+
display: inline-flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
gap: 6px;
|
|
119
|
+
padding: 4px 12px;
|
|
120
|
+
border-radius: 20px;
|
|
121
|
+
font-size: 0.85rem;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
margin-left: 10px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.version-up-to-date {
|
|
127
|
+
background: rgba(67, 233, 123, 0.2);
|
|
128
|
+
color: var(--success);
|
|
129
|
+
border: 1px solid var(--success);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.version-outdated {
|
|
133
|
+
background: rgba(255, 118, 117, 0.2);
|
|
134
|
+
color: var(--danger);
|
|
135
|
+
border: 1px solid var(--danger);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.version-fetch-error {
|
|
139
|
+
background: rgba(255, 209, 102, 0.2);
|
|
140
|
+
color: var(--warning);
|
|
141
|
+
border: 1px solid var(--warning);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@media (max-width: 768px) {
|
|
145
|
+
.info-box {
|
|
146
|
+
padding: 2rem;
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.info-title {
|
|
151
|
+
font-size: 1.8rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.info-row {
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
align-items: flex-start;
|
|
157
|
+
gap: 0.5rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.info-label {
|
|
161
|
+
min-width: unset;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.info-section h2 {
|
|
165
|
+
font-size: 1.2rem;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@media (max-width: 576px) {
|
|
170
|
+
.info-box {
|
|
171
|
+
padding: 1.5rem;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.info-section {
|
|
175
|
+
padding: 1rem;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
|
|
181
|
+
<body>
|
|
182
|
+
<header>
|
|
183
|
+
<div class="header-container">
|
|
184
|
+
<a class="logo">
|
|
185
|
+
<img src="/icon.svg" alt="Logo" class="logo-image" />
|
|
186
|
+
<span class="logo-text">BK <span>Tech</span></span>
|
|
187
|
+
<span class="logo-comp">{{#if APP_NAME}}{{APP_NAME}}{{else}}mbkbucket{{/if}}
|
|
188
|
+
</a>
|
|
189
|
+
|
|
190
|
+
{{> profilemenu}}
|
|
191
|
+
</div>
|
|
192
|
+
</header>
|
|
193
|
+
<section class="login-container">
|
|
194
|
+
<i class="fas fa-info-circle ai-element"></i>
|
|
195
|
+
<i class="fas fa-cogs ai-element"></i>
|
|
196
|
+
<i class="fas fa-server ai-element"></i>
|
|
197
|
+
<i class="fas fa-code-branch ai-element"></i>
|
|
198
|
+
|
|
199
|
+
<div class="info-box">
|
|
200
|
+
<h1 class="info-title">System Information</h1>
|
|
201
|
+
|
|
202
|
+
<div class="info-section">
|
|
203
|
+
<h2><i class="fas fa-code-branch"></i> Version Information</h2>
|
|
204
|
+
<div class="info-row">
|
|
205
|
+
<div class="info-label">Current Version:</div>
|
|
206
|
+
<div class="info-value" id="CurrentVersion">{{CurrentVersion}}</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div class="info-row">
|
|
210
|
+
<div class="info-label">Latest Version:</div>
|
|
211
|
+
<div class="info-value">
|
|
212
|
+
{{latestVersion}}
|
|
213
|
+
{{#if (eq latestVersion CurrentVersion)}}
|
|
214
|
+
<span class="version-status version-up-to-date">
|
|
215
|
+
<i class="fas fa-check-circle"></i> Up to date
|
|
216
|
+
</span>
|
|
217
|
+
{{/if}}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</section>
|
|
223
|
+
|
|
224
|
+
</body>
|
|
225
|
+
|
|
226
|
+
</html>
|