kokokoasd commited on
Commit
afc671b
·
verified ·
1 Parent(s): e9d28b6

Upload 20 files

Browse files
Files changed (3) hide show
  1. static/app.js +9 -2
  2. static/index.html +30 -21
  3. static/style.css +58 -14
static/app.js CHANGED
@@ -17,7 +17,9 @@ function hugpanel() {
17
  currentZone: localStorage.getItem('hugpanel_zone') || null,
18
  activeTab: localStorage.getItem('hugpanel_tab') || 'files',
19
  maxZones: 0,
20
- isDesktop: window.innerWidth >= 1280,
 
 
21
  tabs: [
22
  { id: 'files', label: 'Files', icon: 'folder' },
23
  { id: 'editor', label: 'Editor', icon: 'file-code' },
@@ -81,6 +83,9 @@ function hugpanel() {
81
  await this.loadBackupStatus();
82
  this.adminApiUrl = this.backupStatus.admin_url || '';
83
 
 
 
 
84
  // Try to restore session from stored token
85
  if (this.token && this.adminApiUrl) {
86
  try {
@@ -140,7 +145,7 @@ function hugpanel() {
140
  });
141
 
142
  // Track desktop breakpoint
143
- const mql = window.matchMedia('(min-width: 1280px)');
144
  mql.addEventListener('change', (e) => { this.isDesktop = e.matches; });
145
 
146
  // Persist session state
@@ -206,6 +211,8 @@ function hugpanel() {
206
  if (resp.ok) {
207
  const data = await resp.json();
208
  this.maxZones = data.max_zones || 0;
 
 
209
  }
210
  } catch {}
211
  },
 
17
  currentZone: localStorage.getItem('hugpanel_zone') || null,
18
  activeTab: localStorage.getItem('hugpanel_tab') || 'files',
19
  maxZones: 0,
20
+ motd: '',
21
+ registrationDisabled: false,
22
+ isDesktop: window.innerWidth >= 1024,
23
  tabs: [
24
  { id: 'files', label: 'Files', icon: 'folder' },
25
  { id: 'editor', label: 'Editor', icon: 'file-code' },
 
83
  await this.loadBackupStatus();
84
  this.adminApiUrl = this.backupStatus.admin_url || '';
85
 
86
+ // Load config (MOTD, registration state, zone limit) early
87
+ await this._loadZoneLimit();
88
+
89
  // Try to restore session from stored token
90
  if (this.token && this.adminApiUrl) {
91
  try {
 
145
  });
146
 
147
  // Track desktop breakpoint
148
+ const mql = window.matchMedia('(min-width: 1024px)');
149
  mql.addEventListener('change', (e) => { this.isDesktop = e.matches; });
150
 
151
  // Persist session state
 
211
  if (resp.ok) {
212
  const data = await resp.json();
213
  this.maxZones = data.max_zones || 0;
214
+ this.motd = data.motd || '';
215
+ this.registrationDisabled = !!data.disable_registration;
216
  }
217
  } catch {}
218
  },
static/index.html CHANGED
@@ -56,9 +56,12 @@
56
  <!-- Tab switch -->
57
  <div class="flex bg-gray-800 rounded-lg p-0.5">
58
  <button @click="authMode = 'login'" :class="authMode === 'login' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng nhập</button>
59
- <button @click="authMode = 'register'" :class="authMode === 'register' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng ký</button>
60
  </div>
61
 
 
 
 
62
  <!-- Error -->
63
  <div x-show="authError" class="text-xs text-red-400 bg-red-400/10 rounded-lg px-3 py-2" x-text="authError"></div>
64
 
@@ -99,6 +102,12 @@
99
  <!-- ═══ MAIN PANEL (shown when logged in) ═══ -->
100
  <div x-show="user" x-cloak>
101
 
 
 
 
 
 
 
102
  <!-- ═══ Mobile Top Bar ═══ -->
103
  <header class="lg:hidden fixed top-0 inset-x-0 z-50 bg-gray-900/95 backdrop-blur border-b border-gray-800 px-4 py-3 flex items-center justify-between">
104
  <button @click="sidebarOpen = !sidebarOpen" class="p-1.5 rounded-lg hover:bg-gray-800 transition">
@@ -117,7 +126,7 @@
117
  @click="sidebarOpen = false"
118
  class="lg:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"></div>
119
 
120
- <div class="flex h-full">
121
 
122
  <!-- ═══ Sidebar ═══ -->
123
  <aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
@@ -186,20 +195,20 @@
186
  </aside>
187
 
188
  <!-- ═══ Main Content ═══ -->
189
- <main class="flex-1 flex flex-col min-w-0 pt-14 lg:pt-0">
190
 
191
  <!-- No zone selected -->
192
- <div x-show="!currentZone" class="flex-1 overflow-y-auto p-4 xl:p-8" x-effect="if(!currentZone && backupStatus.configured) loadBackupList()">
193
- <div class="text-center max-w-sm mx-auto mb-6 pt-4 xl:pt-8">
194
- <div class="w-16 h-16 xl:w-20 xl:h-20 mx-auto mb-4 rounded-2xl bg-gray-800 flex items-center justify-center">
195
- <i data-lucide="layout-dashboard" class="w-8 h-8 xl:w-10 xl:h-10 text-gray-600"></i>
196
  </div>
197
- <h2 class="text-lg xl:text-xl font-semibold text-gray-400 mb-2">Chọn hoặc tạo Zone</h2>
198
  <p class="text-sm text-gray-600">Chọn zone từ sidebar hoặc tạo zone mới để bắt đầu.</p>
199
  </div>
200
 
201
  <!-- Cloud Backups (available even without selecting a zone) -->
202
- <div x-show="backupStatus.configured" class="max-w-lg xl:max-w-2xl mx-auto space-y-4">
203
  <!-- Restore All button -->
204
  <button @click="restoreAll()" :disabled="backupStatus.running"
205
  :class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-emerald-500'"
@@ -276,22 +285,22 @@
276
  </template>
277
 
278
  <!-- Zone Actions (right side) -->
279
- <div class="ml-auto flex items-center gap-1 xl:gap-2">
280
- <div class="hidden xl:flex items-center gap-2 mr-3">
281
  <div class="w-2 h-2 rounded-full bg-green-500"></div>
282
  <span x-text="currentZone" class="text-sm text-gray-300 font-medium"></span>
283
  </div>
284
- <span x-text="currentZone" class="text-xs text-gray-500 font-mono mr-2 hidden sm:inline xl:hidden"></span>
285
  <button @click="confirmDeleteZone()" class="p-1.5 rounded-lg text-gray-500 hover:text-red-400 hover:bg-red-400/10 transition flex items-center gap-1.5" title="Xoá zone">
286
  <i data-lucide="trash-2" class="w-4 h-4"></i>
287
- <span class="hidden xl:inline text-xs">Xoá zone</span>
288
  </button>
289
  </div>
290
  </div>
291
  </div>
292
 
293
  <!-- ═══ TAB: Files + Editor (split on desktop) ═══ -->
294
- <div x-show="activeTab === 'files' || activeTab === 'editor'" class="desktop-split flex-1 flex flex-col xl:flex-row min-h-0">
295
 
296
  <!-- Files Panel (always visible on desktop when in files/editor tab) -->
297
  <div x-show="activeTab === 'files' || (isDesktop && activeTab === 'editor')" class="split-panel split-files flex flex-col min-h-0" :class="isDesktop ? '' : 'flex-1'">
@@ -413,10 +422,10 @@
413
  </button>
414
  </div>
415
  </div>
416
- <div class="flex-1 min-h-0">
417
  <textarea x-model="editorContent" @input="editorDirty = true"
418
  @keydown.ctrl.s.prevent="saveFile()"
419
- class="w-full h-full p-4 bg-gray-950 text-gray-200 text-sm font-mono resize-none outline-none leading-relaxed"
420
  spellcheck="false"></textarea>
421
  </div>
422
  </div>
@@ -431,7 +440,7 @@
431
 
432
  <!-- ═══ TAB: Ports ═══ -->
433
  <div x-show="activeTab === 'ports'" class="flex-1 overflow-y-auto">
434
- <div class="p-4 xl:p-6 space-y-4 desktop-content mx-auto w-full">
435
  <!-- Add Port -->
436
  <div class="bg-gray-900 rounded-xl border border-gray-800 p-4">
437
  <h3 class="text-sm font-medium text-gray-300 mb-3">Thêm Port</h3>
@@ -447,7 +456,7 @@
447
  </div>
448
 
449
  <!-- Port List -->
450
- <div class="space-y-2 xl:grid xl:grid-cols-2 xl:gap-3 xl:space-y-0">
451
  <template x-for="port in ports" :key="port.port">
452
  <div class="bg-gray-900 rounded-xl border border-gray-800 p-4 flex items-center gap-3">
453
  <div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center flex-shrink-0">
@@ -471,7 +480,7 @@
471
  </div>
472
  </template>
473
 
474
- <div x-show="ports.length === 0" class="text-center py-8 text-gray-600 text-sm xl:col-span-2">
475
  Chưa có port nào
476
  </div>
477
  </div>
@@ -480,7 +489,7 @@
480
 
481
  <!-- ═══ TAB: Backup ═══ -->
482
  <div x-show="activeTab === 'backup'" x-effect="if(activeTab==='backup' && backupStatus.configured) loadBackupList()" class="flex-1 overflow-y-auto">
483
- <div class="p-4 xl:p-6 space-y-4 desktop-content mx-auto w-full">
484
 
485
  <!-- Not configured -->
486
  <div x-show="!backupStatus.configured" class="bg-gray-900 rounded-xl border border-gray-800 p-6 text-center">
@@ -528,7 +537,7 @@
528
  </div>
529
 
530
  <!-- Action Buttons -->
531
- <div class="grid grid-cols-2 xl:grid-cols-4 gap-2">
532
  <button @click="backupZone(currentZone)" :disabled="backupStatus.running"
533
  :class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-brand-500'"
534
  class="flex items-center justify-center gap-2 px-4 py-3 bg-brand-600 rounded-xl text-sm font-medium transition">
 
56
  <!-- Tab switch -->
57
  <div class="flex bg-gray-800 rounded-lg p-0.5">
58
  <button @click="authMode = 'login'" :class="authMode === 'login' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng nhập</button>
59
+ <button x-show="!registrationDisabled" @click="authMode = 'register'" :class="authMode === 'register' ? 'bg-brand-600 text-white shadow' : 'text-gray-400 hover:text-gray-200'" class="flex-1 py-2 text-sm font-medium rounded-md transition">Đăng ký</button>
60
  </div>
61
 
62
+ <!-- Registration disabled notice -->
63
+ <div x-show="registrationDisabled && authMode === 'register'" x-init="if(registrationDisabled) authMode='login'" class="text-xs text-yellow-400 bg-yellow-400/10 rounded-lg px-3 py-2">Đăng ký đã bị tắt bởi admin</div>
64
+
65
  <!-- Error -->
66
  <div x-show="authError" class="text-xs text-red-400 bg-red-400/10 rounded-lg px-3 py-2" x-text="authError"></div>
67
 
 
102
  <!-- ═══ MAIN PANEL (shown when logged in) ═══ -->
103
  <div x-show="user" x-cloak>
104
 
105
+ <!-- MOTD Banner -->
106
+ <div x-show="motd" class="fixed top-0 inset-x-0 z-[60] bg-brand-600/95 backdrop-blur text-white text-sm px-4 py-2.5 flex items-center justify-between lg:relative lg:z-auto">
107
+ <span x-text="motd" class="flex-1 text-center"></span>
108
+ <button @click="motd=''" class="ml-3 p-1 hover:bg-white/20 rounded transition text-xs">✕</button>
109
+ </div>
110
+
111
  <!-- ═══ Mobile Top Bar ═══ -->
112
  <header class="lg:hidden fixed top-0 inset-x-0 z-50 bg-gray-900/95 backdrop-blur border-b border-gray-800 px-4 py-3 flex items-center justify-between">
113
  <button @click="sidebarOpen = !sidebarOpen" class="p-1.5 rounded-lg hover:bg-gray-800 transition">
 
126
  @click="sidebarOpen = false"
127
  class="lg:hidden fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"></div>
128
 
129
+ <div class="flex h-screen overflow-hidden">
130
 
131
  <!-- ═══ Sidebar ═══ -->
132
  <aside :class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'"
 
195
  </aside>
196
 
197
  <!-- ═══ Main Content ═══ -->
198
+ <main class="flex-1 flex flex-col min-w-0 min-h-0 pt-14 lg:pt-0 h-full">
199
 
200
  <!-- No zone selected -->
201
+ <div x-show="!currentZone" class="flex-1 overflow-y-auto p-4 lg:p-8" x-effect="if(!currentZone && backupStatus.configured) loadBackupList()">
202
+ <div class="text-center max-w-sm mx-auto mb-6 pt-4 lg:pt-8">
203
+ <div class="w-16 h-16 lg:w-20 lg:h-20 mx-auto mb-4 rounded-2xl bg-gray-800 flex items-center justify-center">
204
+ <i data-lucide="layout-dashboard" class="w-8 h-8 lg:w-10 lg:h-10 text-gray-600"></i>
205
  </div>
206
+ <h2 class="text-lg lg:text-xl font-semibold text-gray-400 mb-2">Chọn hoặc tạo Zone</h2>
207
  <p class="text-sm text-gray-600">Chọn zone từ sidebar hoặc tạo zone mới để bắt đầu.</p>
208
  </div>
209
 
210
  <!-- Cloud Backups (available even without selecting a zone) -->
211
+ <div x-show="backupStatus.configured" class="max-w-3xl mx-auto space-y-4">
212
  <!-- Restore All button -->
213
  <button @click="restoreAll()" :disabled="backupStatus.running"
214
  :class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-emerald-500'"
 
285
  </template>
286
 
287
  <!-- Zone Actions (right side) -->
288
+ <div class="ml-auto flex items-center gap-1 lg:gap-2">
289
+ <div class="hidden lg:flex items-center gap-2 mr-3">
290
  <div class="w-2 h-2 rounded-full bg-green-500"></div>
291
  <span x-text="currentZone" class="text-sm text-gray-300 font-medium"></span>
292
  </div>
293
+ <span x-text="currentZone" class="text-xs text-gray-500 font-mono mr-2 hidden sm:inline lg:hidden"></span>
294
  <button @click="confirmDeleteZone()" class="p-1.5 rounded-lg text-gray-500 hover:text-red-400 hover:bg-red-400/10 transition flex items-center gap-1.5" title="Xoá zone">
295
  <i data-lucide="trash-2" class="w-4 h-4"></i>
296
+ <span class="hidden lg:inline text-xs">Xoá zone</span>
297
  </button>
298
  </div>
299
  </div>
300
  </div>
301
 
302
  <!-- ═══ TAB: Files + Editor (split on desktop) ═══ -->
303
+ <div x-show="activeTab === 'files' || activeTab === 'editor'" class="desktop-split flex-1 flex flex-col lg:flex-row min-h-0">
304
 
305
  <!-- Files Panel (always visible on desktop when in files/editor tab) -->
306
  <div x-show="activeTab === 'files' || (isDesktop && activeTab === 'editor')" class="split-panel split-files flex flex-col min-h-0" :class="isDesktop ? '' : 'flex-1'">
 
422
  </button>
423
  </div>
424
  </div>
425
+ <div class="flex-1 min-h-0 overflow-hidden">
426
  <textarea x-model="editorContent" @input="editorDirty = true"
427
  @keydown.ctrl.s.prevent="saveFile()"
428
+ class="w-full h-full p-4 bg-gray-950 text-gray-200 text-sm font-mono resize-none outline-none leading-relaxed block"
429
  spellcheck="false"></textarea>
430
  </div>
431
  </div>
 
440
 
441
  <!-- ═══ TAB: Ports ═══ -->
442
  <div x-show="activeTab === 'ports'" class="flex-1 overflow-y-auto">
443
+ <div class="p-4 lg:p-6 space-y-4 w-full">
444
  <!-- Add Port -->
445
  <div class="bg-gray-900 rounded-xl border border-gray-800 p-4">
446
  <h3 class="text-sm font-medium text-gray-300 mb-3">Thêm Port</h3>
 
456
  </div>
457
 
458
  <!-- Port List -->
459
+ <div class="space-y-2 lg:grid lg:grid-cols-2 2xl:grid-cols-3 lg:gap-3 lg:space-y-0">
460
  <template x-for="port in ports" :key="port.port">
461
  <div class="bg-gray-900 rounded-xl border border-gray-800 p-4 flex items-center gap-3">
462
  <div class="w-10 h-10 rounded-lg bg-green-500/10 flex items-center justify-center flex-shrink-0">
 
480
  </div>
481
  </template>
482
 
483
+ <div x-show="ports.length === 0" class="text-center py-8 text-gray-600 text-sm lg:col-span-2 2xl:col-span-3">
484
  Chưa có port nào
485
  </div>
486
  </div>
 
489
 
490
  <!-- ═══ TAB: Backup ═══ -->
491
  <div x-show="activeTab === 'backup'" x-effect="if(activeTab==='backup' && backupStatus.configured) loadBackupList()" class="flex-1 overflow-y-auto">
492
+ <div class="p-4 lg:p-6 space-y-4 w-full">
493
 
494
  <!-- Not configured -->
495
  <div x-show="!backupStatus.configured" class="bg-gray-900 rounded-xl border border-gray-800 p-6 text-center">
 
537
  </div>
538
 
539
  <!-- Action Buttons -->
540
+ <div class="grid grid-cols-2 lg:grid-cols-4 gap-2">
541
  <button @click="backupZone(currentZone)" :disabled="backupStatus.running"
542
  :class="backupStatus.running ? 'opacity-50 cursor-not-allowed' : 'hover:bg-brand-500'"
543
  class="flex items-center justify-center gap-2 px-4 py-3 bg-brand-600 rounded-xl text-sm font-medium transition">
static/style.css CHANGED
@@ -25,6 +25,13 @@
25
  display: none;
26
  }
27
 
 
 
 
 
 
 
 
28
  /* Terminal container */
29
  #terminal-container {
30
  min-height: 200px;
@@ -51,6 +58,18 @@ textarea {
51
  -moz-tab-size: 4;
52
  }
53
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  /* Smooth transitions */
55
  * {
56
  -webkit-tap-highlight-color: transparent;
@@ -68,6 +87,20 @@ textarea {
68
  }
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  /* Focus ring */
72
  input:focus, textarea:focus, button:focus-visible {
73
  outline: none;
@@ -94,24 +127,41 @@ input:focus, textarea:focus, button:focus-visible {
94
 
95
  /* ═══ Desktop Enhancements ═══ */
96
 
97
- /* Desktop split: files + editor side by side on xl+ */
98
- @media (min-width: 1280px) {
99
  .desktop-split {
100
  flex-direction: row !important;
 
 
101
  }
102
  .desktop-split > .split-files {
103
- width: 380px;
 
 
104
  flex-shrink: 0;
105
  border-right: 1px solid #1f2937;
 
 
 
106
  }
107
  .desktop-split > .split-editor {
108
  flex: 1;
109
  min-width: 0;
 
 
 
110
  }
111
  }
112
 
113
- /* Desktop: better file list items */
114
  @media (min-width: 1280px) {
 
 
 
 
 
 
 
115
  .file-item .file-actions {
116
  opacity: 0;
117
  transition: opacity 0.15s;
@@ -121,14 +171,8 @@ input:focus, textarea:focus, button:focus-visible {
121
  }
122
  }
123
 
124
- /* Desktop: wider content areas for backup/ports */
125
- @media (min-width: 1280px) {
126
- .desktop-content {
127
- max-width: 960px;
128
- }
129
- }
130
- @media (min-width: 1536px) {
131
- .desktop-content {
132
- max-width: 1100px;
133
- }
134
  }
 
25
  display: none;
26
  }
27
 
28
+ /* ═══ Full-height layout ═══ */
29
+ html, body {
30
+ height: 100%;
31
+ margin: 0;
32
+ overflow: hidden;
33
+ }
34
+
35
  /* Terminal container */
36
  #terminal-container {
37
  min-height: 200px;
 
58
  -moz-tab-size: 4;
59
  }
60
 
61
+ /* Editor area fills all available space */
62
+ .split-editor {
63
+ display: flex;
64
+ flex-direction: column;
65
+ min-height: 0;
66
+ flex: 1;
67
+ }
68
+ .split-editor textarea {
69
+ flex: 1;
70
+ min-height: 0;
71
+ }
72
+
73
  /* Smooth transitions */
74
  * {
75
  -webkit-tap-highlight-color: transparent;
 
87
  }
88
  }
89
 
90
+ /* ═══ Mobile: full-height panels ═══ */
91
+ @media (max-width: 1279px) {
92
+ /* Editor tab: fill remaining viewport height */
93
+ .split-editor {
94
+ height: calc(100vh - 7rem);
95
+ min-height: 300px;
96
+ }
97
+ /* Files tab: fill remaining height */
98
+ .split-files {
99
+ height: calc(100vh - 7rem);
100
+ min-height: 300px;
101
+ }
102
+ }
103
+
104
  /* Focus ring */
105
  input:focus, textarea:focus, button:focus-visible {
106
  outline: none;
 
127
 
128
  /* ═══ Desktop Enhancements ═══ */
129
 
130
+ /* Desktop split: files + editor side by side on lg+ (1024px) */
131
+ @media (min-width: 1024px) {
132
  .desktop-split {
133
  flex-direction: row !important;
134
+ flex: 1;
135
+ min-height: 0;
136
  }
137
  .desktop-split > .split-files {
138
+ width: 340px;
139
+ min-width: 280px;
140
+ max-width: 400px;
141
  flex-shrink: 0;
142
  border-right: 1px solid #1f2937;
143
+ display: flex;
144
+ flex-direction: column;
145
+ min-height: 0;
146
  }
147
  .desktop-split > .split-editor {
148
  flex: 1;
149
  min-width: 0;
150
+ display: flex;
151
+ flex-direction: column;
152
+ min-height: 0;
153
  }
154
  }
155
 
156
+ /* xl+ wider file panel */
157
  @media (min-width: 1280px) {
158
+ .desktop-split > .split-files {
159
+ width: 380px;
160
+ }
161
+ }
162
+
163
+ /* Desktop: better file list items */
164
+ @media (min-width: 1024px) {
165
  .file-item .file-actions {
166
  opacity: 0;
167
  transition: opacity 0.15s;
 
171
  }
172
  }
173
 
174
+ /* Desktop: content areas fill width (no max-width constraint) */
175
+ .desktop-content {
176
+ width: 100%;
177
+ max-width: 100%;
 
 
 
 
 
 
178
  }