mirror of
https://github.com/wahyd4/home-docker.git
synced 2026-08-09 04:15:52 +10:00
fix(demo): 首页路由修复 + admin 浅色 UI 改造 (#4)
- Caddyfile: 添加 handle / 避免首页走 /site-content 重写导致 JSON 404 - service.py: / 路由区分登录态 — 已登录 302 → /admin,未登录显示 404 动画页 - admin-ui.html: 深色→浅色主题,圆角卡片、优化按钮样式、better scrollbar、hover 效果 - 同步源码 Caddyfile 与 ConfigMap Co-authored-by: Junv (via Hermes) <junv@junnv.cc>
This commit is contained in:
@@ -23,6 +23,11 @@ data:
|
||||
}
|
||||
}
|
||||
|
||||
# ── Root path: API handles auth (logged in → /admin, else → error page) ──
|
||||
handle / {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
|
||||
# Management API → Python service
|
||||
handle /api/* {
|
||||
reverse_proxy localhost:3000 {
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
# ── Root path: API handles auth (logged in → /admin, else → error page) ──
|
||||
handle / {
|
||||
reverse_proxy localhost:3000
|
||||
}
|
||||
|
||||
# Management API → Python service
|
||||
handle /api/* {
|
||||
reverse_proxy localhost:3000 {
|
||||
|
||||
@@ -6,75 +6,101 @@
|
||||
<title>Demo Manager — junv.cc</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;min-height:100vh}
|
||||
.header{background:#161b22;border-bottom:1px solid #30363d;padding:16px 24px;display:flex;justify-content:space-between;align-items:center}
|
||||
.header h1{font-size:20px;color:#f0f6fc}
|
||||
.header .user{font-size:13px;color:#8b949e}
|
||||
.container{max-width:1000px;margin:24px auto;padding:0 24px}
|
||||
.section{margin-bottom:32px}
|
||||
.section h2{font-size:18px;color:#f0f6fc;margin-bottom:12px}
|
||||
.btn{padding:8px 16px;border-radius:6px;border:1px solid #30363d;background:#21262d;color:#c9d1d9;cursor:pointer;font-size:13px;transition:all .15s}
|
||||
.btn:hover{background:#30363d}
|
||||
.btn-primary{background:#1f6feb;border-color:#1f6feb;color:#fff}
|
||||
.btn-primary:hover{background:#388bfd}
|
||||
.btn-success{background:#238636;border-color:#238636;color:#fff}
|
||||
.btn-success:hover{background:#2ea043}
|
||||
.btn-warning{background:#9e6a03;border-color:#9e6a03;color:#fff}
|
||||
.btn-warning:hover{background:#bb8009}
|
||||
.btn-danger{background:#da3633;border-color:#da3633;color:#fff}
|
||||
.btn-danger:hover{background:#f85149}
|
||||
.btn-sm{padding:4px 10px;font-size:12px}
|
||||
.site-card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center}
|
||||
.site-card .info{flex:1}
|
||||
.site-card .name{font-size:16px;font-weight:600;color:#58a6ff}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',sans-serif;background:#f4f6f9;color:#1a1a2e;min-height:100vh}
|
||||
.header{background:#fff;border-bottom:1px solid #e2e8f0;padding:14px 28px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:50;box-shadow:0 1px 3px rgba(0,0,0,.04)}
|
||||
.header h1{font-size:20px;font-weight:700;color:#0f172a;letter-spacing:-.3px}
|
||||
.header .user{font-size:13px;color:#64748b;background:#f1f5f9;padding:4px 12px;border-radius:20px}
|
||||
.container{max-width:1040px;margin:28px auto;padding:0 28px}
|
||||
.section{margin-bottom:36px}
|
||||
.section h2{font-size:17px;font-weight:600;color:#0f172a;margin-bottom:14px;letter-spacing:-.2px}
|
||||
/* Buttons */
|
||||
.btn{display:inline-flex;align-items:center;gap:4px;padding:8px 18px;border-radius:8px;border:1px solid #e2e8f0;background:#fff;color:#334155;cursor:pointer;font-size:13px;font-weight:500;transition:all .15s}
|
||||
.btn:hover{background:#f8fafc;border-color:#cbd5e1;box-shadow:0 1px 3px rgba(0,0,0,.06)}
|
||||
.btn-primary{background:#2563eb;border-color:#2563eb;color:#fff}
|
||||
.btn-primary:hover{background:#1d4ed8;border-color:#1d4ed8}
|
||||
.btn-success{background:#16a34a;border-color:#16a34a;color:#fff}
|
||||
.btn-success:hover{background:#15803d;border-color:#15803d}
|
||||
.btn-warning{background:#d97706;border-color:#d97706;color:#fff}
|
||||
.btn-warning:hover{background:#b45309;border-color:#b45309}
|
||||
.btn-danger{background:#dc2626;border-color:#dc2626;color:#fff}
|
||||
.btn-danger:hover{background:#b91c1c;border-color:#b91c1c}
|
||||
.btn-sm{padding:5px 12px;font-size:12px;border-radius:6px}
|
||||
/* Site cards */
|
||||
.site-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:18px 20px;margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;transition:box-shadow .15s}
|
||||
.site-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.06)}
|
||||
.site-card .info{flex:1;min-width:0}
|
||||
.site-card .name{font-size:15px;font-weight:600;color:#2563eb}
|
||||
.site-card .name a{color:inherit;text-decoration:none}
|
||||
.site-card .name a:hover{text-decoration:underline}
|
||||
.site-card .desc{font-size:13px;color:#8b949e;margin-top:4px}
|
||||
.site-card .meta{font-size:11px;color:#6e7681;margin-top:4px}
|
||||
.site-card .actions{display:flex;gap:6px}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
|
||||
.badge-on{background:#1b3a1b;color:#3fb950}
|
||||
.badge-off{background:#3a1b1b;color:#f85149}
|
||||
.badge-draft{background:#3a2e1b;color:#d29922}
|
||||
.badge-published{background:#1b3a1b;color:#3fb950}
|
||||
.form-group{margin-bottom:12px}
|
||||
.form-group label{display:block;font-size:13px;color:#8b949e;margin-bottom:4px}
|
||||
.form-group input{width:100%;padding:8px 12px;border-radius:6px;border:1px solid #30363d;background:#0d1117;color:#c9d1d9;font-size:14px}
|
||||
.form-group input:focus{outline:none;border-color:#1f6feb}
|
||||
.form-row{display:flex;gap:12px;align-items:end}
|
||||
.site-card .desc{font-size:13px;color:#64748b;margin-top:3px}
|
||||
.site-card .meta{font-size:11px;color:#94a3b8;margin-top:3px}
|
||||
.site-card .actions{display:flex;gap:5px;flex-shrink:0;margin-left:16px}
|
||||
.badge{display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;letter-spacing:.02em}
|
||||
.badge-on{background:#dcfce7;color:#166534}
|
||||
.badge-off{background:#fef2f2;color:#991b1b}
|
||||
.badge-draft{background:#fef9c3;color:#854d0e}
|
||||
.badge-published{background:#dcfce7;color:#166534}
|
||||
/* Forms */
|
||||
.form-group{margin-bottom:10px}
|
||||
.form-group label{display:block;font-size:12px;font-weight:600;color:#475569;margin-bottom:4px;text-transform:uppercase;letter-spacing:.04em}
|
||||
.form-group input[type="text"]{width:100%;padding:10px 14px;border-radius:8px;border:1px solid #e2e8f0;background:#fff;color:#0f172a;font-size:14px;transition:border-color .15s,box-shadow .15s}
|
||||
.form-group input[type="text"]:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px rgba(37,99,235,.12)}
|
||||
.form-row{display:flex;gap:14px;align-items:end;flex-wrap:wrap}
|
||||
/* File list */
|
||||
.file-list{list-style:none}
|
||||
.file-list li{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #21262d;font-size:13px}
|
||||
.file-list li{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #f1f5f9;font-size:13px}
|
||||
.file-list li:last-child{border-bottom:none}
|
||||
.upload-zone{border:2px dashed #30363d;border-radius:8px;padding:24px;text-align:center;cursor:pointer;transition:all .15s}
|
||||
.upload-zone:hover{border-color:#58a6ff;background:#161b22}
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:100}
|
||||
.modal{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:24px;max-width:560px;width:90%}
|
||||
.modal h3{color:#f0f6fc;margin-bottom:16px}
|
||||
.toast{position:fixed;bottom:24px;right:24px;padding:12px 20px;border-radius:8px;font-size:14px;z-index:200;animation:slideIn .3s}
|
||||
.toast-success{background:#1b3a1b;border:1px solid #3fb950;color:#3fb950}
|
||||
.toast-error{background:#3a1b1b;border:1px solid #f85149;color:#f85149}
|
||||
@keyframes slideIn{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
.spinner{display:inline-block;width:16px;height:16px;border:2px solid #30363d;border-top-color:#58a6ff;border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle;margin-right:6px}
|
||||
.file-list li a{color:#2563eb;text-decoration:none;font-weight:500}
|
||||
.file-list li a:hover{text-decoration:underline}
|
||||
/* Upload zone */
|
||||
.upload-zone{border:2px dashed #d1d5db;border-radius:10px;padding:28px 20px;text-align:center;cursor:pointer;transition:all .15s;background:#fafafa}
|
||||
.upload-zone:hover{border-color:#2563eb;background:#eff6ff;color:#2563eb}
|
||||
/* Modal */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(15,23,42,.5);display:none;align-items:center;justify-content:center;z-index:100;backdrop-filter:blur(2px)}
|
||||
.modal{background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:28px;max-width:580px;width:92%;box-shadow:0 20px 60px rgba(0,0,0,.12)}
|
||||
.modal h3{color:#0f172a;margin-bottom:18px;font-size:17px;font-weight:600}
|
||||
/* Toast */
|
||||
.toast{position:fixed;bottom:28px;right:28px;padding:14px 22px;border-radius:10px;font-size:14px;font-weight:500;z-index:200;animation:slideIn .3s ease;box-shadow:0 4px 16px rgba(0,0,0,.12)}
|
||||
.toast-success{background:#166534;color:#fff}
|
||||
.toast-error{background:#991b1b;color:#fff}
|
||||
@keyframes slideIn{from{transform:translateY(24px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
/* Spinner */
|
||||
.spinner{display:inline-block;width:16px;height:16px;border:2.5px solid #e2e8f0;border-top-color:#2563eb;border-radius:50%;animation:spin .6s linear infinite;vertical-align:middle;margin-right:6px}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
.help-text{font-size:12px;color:#6e7681;margin-top:4px}
|
||||
.help-text{font-size:12px;color:#94a3b8;margin-top:4px}
|
||||
/* Empty state */
|
||||
.empty-state{padding:48px 24px;text-align:center;color:#94a3b8}
|
||||
.empty-state p{font-size:14px}
|
||||
/* Checkbox inline */
|
||||
.checkbox-row{display:flex;align-items:center;gap:6px}
|
||||
.checkbox-row input[type="checkbox"]{width:auto;accent-color:#2563eb}
|
||||
.checkbox-row label{margin:0;font-size:13px;font-weight:400;text-transform:none;letter-spacing:0;color:#475569}
|
||||
/* Modal close button */
|
||||
.modal-close-btn{text-align:right;margin-bottom:8px}
|
||||
/* Scrollable file area */
|
||||
.file-scroll{max-height:320px;overflow-y:auto}
|
||||
.file-scroll::-webkit-scrollbar{width:6px}
|
||||
.file-scroll::-webkit-scrollbar-track{background:#f1f5f9;border-radius:3px}
|
||||
.file-scroll::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}
|
||||
.file-scroll::-webkit-scrollbar-thumb:hover{background:#94a3b8}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><h1>🧪 Demo Manager</h1><div class="user" id="user-info">Loading...</div></div>
|
||||
<div class="header"><h1>🧪 Demo Manager</h1><span class="user" id="user-info">Loading...</span></div>
|
||||
<div class="container">
|
||||
<div class="section">
|
||||
<h2>➕ New Demo Site <span style="font-size:12px;font-weight:400;color:#6e7681">— drafts are hidden until published</span></h2>
|
||||
<h2>➕ New Demo Site</h2>
|
||||
<form id="create-form" class="form-row">
|
||||
<div class="form-group" style="flex:2"><label>Site Name</label><input type="text" id="site-name" placeholder="my-cool-demo" pattern="[a-z0-9-]+" required></div>
|
||||
<div class="form-group" style="flex:3"><label>Description (optional)</label><input type="text" id="site-desc" placeholder="A quick demo of..."></div>
|
||||
<div class="form-group" style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="site-publish" style="width:auto"><label for="site-publish" style="margin:0;font-size:13px">Publish now</label></div>
|
||||
<button type="submit" class="btn btn-primary" style="height:38px">Create</button>
|
||||
<div class="form-group" style="flex:2;min-width:180px"><label>Site Name</label><input type="text" id="site-name" placeholder="my-cool-demo" pattern="[a-z0-9-]+" required></div>
|
||||
<div class="form-group" style="flex:3;min-width:220px"><label>Description</label><input type="text" id="site-desc" placeholder="A quick demo of..."></div>
|
||||
<div class="checkbox-row" style="padding-bottom:10px"><input type="checkbox" id="site-publish"><label for="site-publish">Publish now</label></div>
|
||||
<button type="submit" class="btn btn-primary" style="height:40px;margin-bottom:10px">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="section"><h2>📂 Sites</h2><div id="sites-list">Loading...</div></div>
|
||||
<div class="section"><h2>📂 Sites</h2><div id="sites-list"><div class="empty-state"><span class="spinner"></span><p style="margin-top:12px">Loading sites...</p></div></div></div>
|
||||
</div>
|
||||
<div id="file-modal" class="modal-overlay"><div class="modal" id="file-modal-content"></div></div>
|
||||
<div id="preview-modal" class="modal-overlay"><div class="modal" id="preview-modal-content" style="max-width:90%;max-height:90vh;overflow:auto"></div></div>
|
||||
<div id="preview-modal" class="modal-overlay"><div class="modal" id="preview-modal-content" style="max-width:92%;max-height:92vh;overflow:auto"></div></div>
|
||||
<div id="toast-container"></div>
|
||||
<script>
|
||||
let currentUser='';
|
||||
@@ -97,7 +123,7 @@ async function uploadFile(siteName,file,subPath){
|
||||
|
||||
function toast(msg,type){
|
||||
const el=document.createElement('div');el.className='toast toast-'+type;el.textContent=msg;
|
||||
document.getElementById('toast-container').appendChild(el);setTimeout(()=>el.remove(),3000);
|
||||
document.getElementById('toast-container').appendChild(el);setTimeout(()=>el.remove(),3500);
|
||||
}
|
||||
|
||||
function formatSize(b){if(b<1024)return b+' B';if(b<1048576)return(b/1024).toFixed(1)+' KB';return(b/1048576).toFixed(1)+' MB';}
|
||||
@@ -107,15 +133,15 @@ async function loadSites(){
|
||||
const container=document.getElementById('sites-list');
|
||||
try{
|
||||
const data=await api('GET',API);currentUser=data.user;
|
||||
document.getElementById('user-info').textContent='👤 '+currentUser+' (Pocket ID)';
|
||||
if(!data.sites.length){container.innerHTML='<p style="color:#8b949e;text-align:center;padding:32px">No sites yet. Create one above!</p>';return;}
|
||||
document.getElementById('user-info').textContent='👤 '+currentUser;
|
||||
if(!data.sites.length){container.innerHTML='<div class="empty-state"><p style="font-size:32px;margin-bottom:12px">📭</p><p>No sites yet. Create one above!</p></div>';return;}
|
||||
container.innerHTML=data.sites.map(s=>{
|
||||
const isPublic=s.public;
|
||||
const isEnabled=s.enabled!==false;
|
||||
let statusBadge='';
|
||||
if(!isEnabled)statusBadge='<span class="badge badge-off">DISABLED</span>';
|
||||
else if(s.published)statusBadge='<span class="badge badge-published">🔓 PUBLISHED</span>';
|
||||
else statusBadge='<span class="badge badge-draft">🔒 DRAFT</span>';
|
||||
if(!isEnabled)statusBadge='<span class="badge badge-off">⏸ Disabled</span>';
|
||||
else if(s.published)statusBadge='<span class="badge badge-published">🔓 Published</span>';
|
||||
else statusBadge='<span class="badge badge-draft">🔒 Draft</span>';
|
||||
|
||||
let actionBtns='';
|
||||
if(!isEnabled){
|
||||
@@ -132,16 +158,17 @@ async function loadSites(){
|
||||
if(!isPublic){actionBtns+='<button class="btn btn-sm" onclick="previewSite(\''+s.name+'\')">👁 Preview</button>';}
|
||||
actionBtns+='<button class="btn btn-sm btn-danger" onclick="deleteSite(\''+s.name+'\')">🗑</button>';
|
||||
|
||||
const nameHtml = isEnabled
|
||||
? '<a href="https://demo.junv.cc/'+s.name+'" target="_blank">'+s.name+'</a>'
|
||||
: s.name;
|
||||
|
||||
return '<div class="site-card"><div class="info"><div class="name">'+
|
||||
(isEnabled
|
||||
? '<a href="https://demo.junv.cc/'+s.name+'" target="_blank">'+s.name+'</a>'
|
||||
: '<a href="#" onclick="previewSite(\''+s.name+'\');return false" style="color:#58a6ff;text-decoration:none;border-bottom:1px dashed #58a6ff" title="Disabled — click to preview">'+s.name+'</a>')+
|
||||
' '+statusBadge+
|
||||
nameHtml+' '+statusBadge+
|
||||
'</div>'+(s.description?'<div class="desc">'+s.description+'</div>':'')+
|
||||
'<div class="meta">'+s.file_count+' files · created '+formatDate(s.created_at)+'</div></div>'+
|
||||
'<div class="actions">'+actionBtns+'</div></div>';
|
||||
}).join('');
|
||||
}catch(e){container.innerHTML='<p style="color:#f85149">'+e.message+'</p>';}
|
||||
}catch(e){container.innerHTML='<div class="empty-state"><p style="color:#dc2626">⚠️ '+e.message+'</p></div>';}
|
||||
}
|
||||
|
||||
async function createSite(e){
|
||||
@@ -183,14 +210,14 @@ async function previewSite(name){
|
||||
const modal=document.getElementById('preview-modal');
|
||||
const content=document.getElementById('preview-modal-content');
|
||||
modal.style.display='flex';
|
||||
content.innerHTML='<h3>👁 Preview: '+name+' <span style="font-size:12px;color:#d29922">(Draft — requires auth)</span></h3><div style="text-align:center;padding:24px"><span class="spinner"></span>Loading preview...</div>';
|
||||
content.innerHTML='<div class="modal-close-btn"><button class="btn btn-sm" onclick="document.getElementById(\'preview-modal\').style.display=\'none\'">✕ Close</button></div><h3>👁 Preview: '+name+'</h3><div style="text-align:center;padding:24px"><span class="spinner"></span>Loading preview...</div>';
|
||||
try{
|
||||
const res=await fetch(API+'/'+name+'/preview');
|
||||
if(!res.ok)throw new Error((await res.json().catch(()=>({detail:'Failed'}))).detail||'Failed');
|
||||
const html=await res.text();
|
||||
content.innerHTML='<h3>👁 Preview: '+name+' <span style="font-size:12px;color:#d29922">(Draft)</span></h3><div style="text-align:right;margin-bottom:8px"><button class="btn btn-sm" onclick="document.getElementById(\'preview-modal\').style.display=\'none\'">Close</button></div><iframe srcdoc="'+html.replace(/"/g,'"')+'" style="width:100%;height:70vh;border:1px solid #30363d;border-radius:8px;background:#fff"></iframe>';
|
||||
content.innerHTML='<div class="modal-close-btn"><button class="btn btn-sm" onclick="document.getElementById(\'preview-modal\').style.display=\'none\'">✕ Close</button></div><h3>👁 Preview: '+name+' <span style="font-size:12px;font-weight:400;color:#d97706">(Draft)</span></h3><iframe srcdoc="'+html.replace(/"/g,'"')+'" style="width:100%;height:70vh;border:1px solid #e2e8f0;border-radius:8px;background:#fff"></iframe>';
|
||||
}catch(e){
|
||||
content.innerHTML='<h3>Preview Error</h3><p style="color:#f85149">'+e.message+'</p><button class="btn btn-sm" onclick="document.getElementById(\'preview-modal\').style.display=\'none\'">Close</button>';
|
||||
content.innerHTML='<div class="modal-close-btn"><button class="btn btn-sm" onclick="document.getElementById(\'preview-modal\').style.display=\'none\'">✕ Close</button></div><h3>Preview Error</h3><p style="color:#dc2626;margin:12px 0">'+e.message+'</p>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,23 +225,23 @@ async function manageSite(name){
|
||||
const modal=document.getElementById('file-modal');
|
||||
const content=document.getElementById('file-modal-content');
|
||||
modal.style.display='flex';
|
||||
content.innerHTML='<h3>📁 '+name+'</h3><div style="margin-bottom:16px"><div class="upload-zone" id="upload-zone-'+name+'"><div id="upload-label-'+name+'">📤 Drop files or click to upload</div><input type="file" id="file-input-'+name+'" style="display:none" multiple><div id="upload-progress-'+name+'" style="display:none;margin-top:8px"></div></div></div><div style="margin:16px 0"><strong style="font-size:13px;color:#8b949e">Files:</strong></div><div id="files-list-'+name+'" style="max-height:300px;overflow-y:auto">Loading...</div><div style="margin-top:16px;text-align:right"><button class="btn btn-sm" onclick="document.getElementById(\'file-modal\').style.display=\'none\'">Close</button></div>';
|
||||
content.innerHTML='<h3>📁 '+name+'</h3><div style="margin-bottom:16px"><div class="upload-zone" id="upload-zone-'+name+'"><div id="upload-label-'+name+'">📤 Drop files here or click to upload</div><input type="file" id="file-input-'+name+'" style="display:none" multiple><div id="upload-progress-'+name+'" style="display:none;margin-top:10px"></div></div></div><strong style="font-size:13px;color:#475569;display:block;margin-bottom:8px">Files:</strong><div class="file-scroll" id="files-list-'+name+'"><div style="padding:16px;text-align:center"><span class="spinner"></span></div></div><div style="margin-top:16px;text-align:right"><button class="btn btn-sm" onclick="document.getElementById(\'file-modal\').style.display=\'none\'">✕ Close</button></div>';
|
||||
|
||||
try{
|
||||
const data=await api('GET',API+'/'+name);
|
||||
const fd=document.getElementById('files-list-'+name);
|
||||
if(!data.files.length){fd.innerHTML='<p style="color:#8b949e">No files. Upload one above.</p>';}
|
||||
if(!data.files.length){fd.innerHTML='<p style="color:#94a3b8;text-align:center;padding:16px">No files. Upload one above.</p>';}
|
||||
else{
|
||||
fd.innerHTML='<ul class="file-list">'+data.files.map(f=>'<li><span><a href="'+API+'/'+name+'/preview?file='+encodeURIComponent(f.path)+'" target="_blank" style="color:#58a6ff">'+f.path+'</a></span><span style="color:#8b949e">'+formatSize(f.size)+' <button class="btn btn-sm btn-danger" style="margin-left:8px" onclick="deleteFile(\''+name+'\',\''+f.path+'\')">×</button></span></li>').join('')+'</ul>';
|
||||
fd.innerHTML='<ul class="file-list">'+data.files.map(f=>'<li><span><a href="'+API+'/'+name+'/preview?file='+encodeURIComponent(f.path)+'" target="_blank">'+f.path+'</a></span><span style="color:#94a3b8">'+formatSize(f.size)+' <button class="btn btn-sm btn-danger" style="margin-left:8px" onclick="deleteFile(\''+name+'\',\''+f.path+'\')">×</button></span></li>').join('')+'</ul>';
|
||||
}
|
||||
}catch(e){document.getElementById('files-list-'+name).innerHTML='<p style="color:#f85149">'+e.message+'</p>';}
|
||||
}catch(e){document.getElementById('files-list-'+name).innerHTML='<p style="color:#dc2626;text-align:center;padding:16px">'+e.message+'</p>';}
|
||||
|
||||
const zone=document.getElementById('upload-zone-'+name);
|
||||
const input=document.getElementById('file-input-'+name);
|
||||
zone.onclick=()=>input.click();
|
||||
zone.ondragover=(e)=>{e.preventDefault();zone.style.borderColor='#58a6ff';};
|
||||
zone.ondragleave=()=>{zone.style.borderColor='';};
|
||||
zone.ondrop=async(e)=>{e.preventDefault();zone.style.borderColor='';await handleUpload(name,e.dataTransfer.files);};
|
||||
zone.ondragover=(e)=>{e.preventDefault();zone.style.borderColor='#2563eb';zone.style.background='#eff6ff';};
|
||||
zone.ondragleave=()=>{zone.style.borderColor='';zone.style.background='';};
|
||||
zone.ondrop=async(e)=>{e.preventDefault();zone.style.borderColor='';zone.style.background='';await handleUpload(name,e.dataTransfer.files);};
|
||||
input.onchange=async()=>{await handleUpload(name,input.files);input.value='';};
|
||||
}
|
||||
|
||||
@@ -222,13 +249,15 @@ async function handleUpload(siteName,files){
|
||||
const pd=document.getElementById('upload-progress-'+siteName);
|
||||
const label=document.getElementById('upload-label-'+siteName);
|
||||
pd.style.display='block';
|
||||
let successCount=0, failCount=0;
|
||||
for(const file of files){
|
||||
pd.innerHTML='<span class="spinner"></span> Uploading '+file.name+'...';
|
||||
try{await uploadFile(siteName,file,'');pd.innerHTML+=' ✅ '+file.name+'<br>';}
|
||||
catch(e){pd.innerHTML+=' ❌ '+file.name+': '+e.message+'<br>';}
|
||||
try{await uploadFile(siteName,file,'');successCount++;pd.innerHTML+=' ✅ '+file.name+'<br>';}
|
||||
catch(e){failCount++;pd.innerHTML+=' ❌ '+file.name+': '+e.message+'<br>';}
|
||||
}
|
||||
label.textContent='📤 Drop more files or click to upload';
|
||||
toast('Upload complete!','success');
|
||||
if(failCount===0)toast(successCount+' file(s) uploaded','success');
|
||||
else toast(successCount+' uploaded, '+failCount+' failed','error');
|
||||
manageSite(siteName);
|
||||
}
|
||||
|
||||
|
||||
@@ -352,7 +352,10 @@ def admin_ui(request: Request, path: str = ""):
|
||||
|
||||
@app.get("/")
|
||||
def index(request: Request):
|
||||
return HTMLResponse("""<!DOCTYPE html>
|
||||
<html><head><meta http-equiv="refresh" content="0;url=/admin"></head>
|
||||
<body><p>Redirecting to <a href="/admin">Admin</a>...</p></body>
|
||||
</html>""")
|
||||
user = check_auth(request)
|
||||
if user:
|
||||
return RedirectResponse(url="/admin", status_code=302)
|
||||
resp = HTMLResponse(AUTH_REQUIRED_HTML, status_code=404)
|
||||
resp.headers["Content-Disposition"] = "inline"
|
||||
resp.headers["Cache-Control"] = "no-store"
|
||||
return resp
|
||||
|
||||
Reference in New Issue
Block a user