fix: S3 generic compat, nav tab reliability, single logo
S3: - Endpoint is now optional (leave blank = AWS S3, custom = MinIO/R2/Backblaze/etc.) - forcePathStyle only applied when a custom endpoint is set (harmless on AWS) - initS3() no longer requires endpoint to be present - Updated form hint to explain AWS vs generic S3 usage Nav tabs: - Switched admin tab active-state matching from fragile array-index to data-tab attribute - Added user-select:none to prevent text selection on click for both nav and admin tabs - Admin tabs now flex-wrap for narrow viewports Logo: - Removed wilddragon-logo.png wordmark from splash, login, and header - Single dragon-icon.png used throughout — no CSS invert hack needed - Cleaner header: icon + "Dragon Wind" badge only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
895145e1ed
commit
e2c6db7113
2 changed files with 35 additions and 35 deletions
|
|
@ -114,13 +114,8 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
66%{transform:translateY(-4px) rotate(-.3deg)}
|
||||
}
|
||||
|
||||
/* Wild Dragon wordmark */
|
||||
.splash-wordmark{
|
||||
height:56px;max-width:360px;object-fit:contain;
|
||||
/* Invert black logo to white, keep blue flame color */
|
||||
filter:invert(1) drop-shadow(0 2px 12px rgba(0,0,0,.5));
|
||||
animation:wordmarkIn .8s .25s cubic-bezier(.22,1,.36,1) both;
|
||||
}
|
||||
/* Wild Dragon wordmark — not used, dragon icon only */
|
||||
.splash-wordmark{ display:none; }
|
||||
@keyframes wordmarkIn{
|
||||
from{opacity:0;transform:translateY(10px) scale(.97)}
|
||||
to{opacity:1;transform:translateY(0) scale(1)}
|
||||
|
|
@ -169,9 +164,7 @@ body::before{content:'';position:fixed;inset:0;background:radial-gradient(ellips
|
|||
/* Dragon icon in login */
|
||||
.login-icon-wrap{display:flex;flex-direction:column;align-items:center;gap:.5rem;margin-bottom:1.5rem}
|
||||
.login-dragon-icon{width:72px;height:72px;object-fit:contain;filter:drop-shadow(0 4px 16px rgba(30,75,216,.4));animation:dragonFloat 4s ease-in-out infinite}
|
||||
.login-wordmark{height:32px;max-width:220px;object-fit:contain}
|
||||
/* Invert black logo in dark mode only */
|
||||
body:not([data-theme="light"]) .login-wordmark{filter:invert(1)}
|
||||
.login-wordmark{display:none}
|
||||
|
||||
.login-product{font-size:.65rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.18em;margin-top:.25rem}
|
||||
.login-field{width:100%;padding:.75rem 1rem;font-family:'Outfit',sans-serif;font-size:.9rem;background:var(--bg-secondary);border:1px solid var(--border);border-radius:10px;color:var(--text-primary);outline:none;transition:all .2s;margin-bottom:.75rem}
|
||||
|
|
@ -192,8 +185,7 @@ body:not([data-theme="light"]) .login-wordmark{filter:invert(1)}
|
|||
.header{display:flex;align-items:center;justify-content:space-between;padding:.8rem 2rem;border-bottom:1px solid var(--border);background:rgba(6,8,14,.9);backdrop-filter:blur(20px);position:sticky;top:0;z-index:100}
|
||||
.header-left{display:flex;align-items:center;gap:.85rem;min-width:0;flex:1}
|
||||
.header-dragon{width:40px;height:40px;object-fit:contain;filter:drop-shadow(0 2px 8px rgba(30,75,216,.5));flex-shrink:0}
|
||||
.header-wordmark{height:28px;max-width:180px;object-fit:contain;flex-shrink:0}
|
||||
body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
|
||||
.header-wordmark{display:none}
|
||||
[data-theme="light"] .header{background:rgba(240,242,247,.9)}
|
||||
[data-theme="light"] .header-dragon{filter:none}
|
||||
.header-product-tag{font-family:'JetBrains Mono',monospace;font-size:.58rem;color:var(--dragon-bright);text-transform:uppercase;letter-spacing:.15em;background:var(--dragon-glow);border:1px solid rgba(224,92,26,.25);border-radius:4px;padding:.15rem .45rem;flex-shrink:0}
|
||||
|
|
@ -206,7 +198,7 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
|
|||
|
||||
/* NAV TABS */
|
||||
.nav-tabs{display:flex;padding:0 2rem;border-bottom:1px solid var(--border);background:var(--bg-secondary)}
|
||||
.nav-tab{padding:.65rem 1.1rem;font-size:.78rem;font-weight:600;color:var(--text-dim);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;white-space:nowrap}
|
||||
.nav-tab{padding:.65rem 1.1rem;font-size:.78rem;font-weight:600;color:var(--text-dim);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;white-space:nowrap;user-select:none;-webkit-user-select:none}
|
||||
.nav-tab:hover{color:var(--text-secondary)}
|
||||
.nav-tab.active{color:var(--blue-bright);border-bottom-color:var(--blue-bright)}
|
||||
.nav-tab.admin-tab.active{color:var(--warning);border-bottom-color:var(--warning)}
|
||||
|
|
@ -314,8 +306,8 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
|
|||
.refresh-btn:hover{border-color:var(--border-bright);color:var(--text-primary)}
|
||||
|
||||
/* ADMIN */
|
||||
.admin-tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:1.5rem}
|
||||
.admin-tab{padding:.5rem 1rem;font-size:.76rem;font-weight:600;color:var(--text-dim);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}
|
||||
.admin-tabs{display:flex;border-bottom:1px solid var(--border);margin-bottom:1.5rem;flex-wrap:wrap}
|
||||
.admin-tab{padding:.5rem 1rem;font-size:.76rem;font-weight:600;color:var(--text-dim);cursor:pointer;border-bottom:2px solid transparent;transition:all .15s;user-select:none;-webkit-user-select:none}
|
||||
.admin-tab:hover{color:var(--text-secondary)}
|
||||
.admin-tab.active{color:var(--blue-bright);border-bottom-color:var(--blue-bright)}
|
||||
.admin-panel{display:none}
|
||||
|
|
@ -526,18 +518,22 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
|
|||
<div class="page" id="page-admin">
|
||||
<div style="margin-top:1.5rem"></div>
|
||||
<div class="admin-tabs">
|
||||
<div class="admin-tab active" onclick="switchAdminTab('s3')">S3 Storage</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('relay')">UDP Relay</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('ampp')">AMPP</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('extension')">🧩 Extension</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('users')">Users</div>
|
||||
<div class="admin-tab" onclick="switchAdminTab('folders')">Folders</div>
|
||||
<div class="admin-tab active" data-tab="s3" onclick="switchAdminTab('s3')">S3 Storage</div>
|
||||
<div class="admin-tab" data-tab="relay" onclick="switchAdminTab('relay')">UDP Relay</div>
|
||||
<div class="admin-tab" data-tab="ampp" onclick="switchAdminTab('ampp')">AMPP</div>
|
||||
<div class="admin-tab" data-tab="extension" onclick="switchAdminTab('extension')">🧩 Extension</div>
|
||||
<div class="admin-tab" data-tab="users" onclick="switchAdminTab('users')">Users</div>
|
||||
<div class="admin-tab" data-tab="folders" onclick="switchAdminTab('folders')">Folders</div>
|
||||
</div>
|
||||
|
||||
<!-- S3 -->
|
||||
<div class="admin-panel active" id="admin-s3">
|
||||
<div class="section-title">S3 Storage Configuration</div>
|
||||
<div class="form-group"><label class="form-label">Endpoint URL</label><input class="form-input" id="s3-endpoint" type="url" placeholder="https://s3.example.com"/></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Endpoint URL <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--text-dim)">(optional — leave blank for AWS S3)</span></label>
|
||||
<input class="form-input" id="s3-endpoint" type="url" placeholder="https://s3.example.com — or blank for AWS"/>
|
||||
<div class="form-hint">For MinIO, Backblaze, Cloudflare R2, Wasabi, etc. enter the full endpoint URL. Leave blank to use standard AWS S3.</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label class="form-label">Region</label><input class="form-input" id="s3-region" type="text" placeholder="us-east-1"/></div>
|
||||
<div class="form-group"><label class="form-label">Bucket Name</label><input class="form-input" id="s3-bucket" type="text" placeholder="mybucket"/></div>
|
||||
|
|
@ -814,8 +810,8 @@ function switchPage(name) {
|
|||
}
|
||||
|
||||
function switchAdminTab(name) {
|
||||
document.querySelectorAll('.admin-tab').forEach((t,i) => {
|
||||
t.classList.toggle('active', ['s3','relay','ampp','extension','users','folders'][i] === name);
|
||||
document.querySelectorAll('.admin-tab').forEach(t => {
|
||||
t.classList.toggle('active', t.dataset.tab === name);
|
||||
});
|
||||
document.querySelectorAll('.admin-panel').forEach(p => p.classList.toggle('active', p.id === `admin-${name}`));
|
||||
if (name === 'users') loadUsers();
|
||||
|
|
|
|||
26
server.js
26
server.js
|
|
@ -86,32 +86,35 @@ function setTree(t) { db.folderTree = t; saveData(db); }
|
|||
let s3Client = null;
|
||||
|
||||
function buildS3Client(cfg) {
|
||||
const endpoint = cfg.endpoint || "";
|
||||
const isHttpsEndpoint = endpoint.startsWith("https");
|
||||
const endpoint = (cfg.endpoint || "").trim();
|
||||
const hasCustomEndpoint = endpoint.length > 0;
|
||||
const isHttps = !hasCustomEndpoint || endpoint.startsWith("https");
|
||||
const agentOpts = { keepAlive: true, maxSockets: 25 };
|
||||
const agent = isHttpsEndpoint
|
||||
const agent = isHttps
|
||||
? new https.Agent({ ...agentOpts, rejectUnauthorized: false })
|
||||
: new http.Agent(agentOpts);
|
||||
|
||||
return new S3Client({
|
||||
endpoint: endpoint || undefined,
|
||||
// No endpoint → AWS SDK uses the official AWS S3 endpoint for the region
|
||||
...(hasCustomEndpoint ? { endpoint } : {}),
|
||||
region: cfg.region || "us-east-1",
|
||||
credentials: {
|
||||
accessKeyId: cfg.accessKeyId || "",
|
||||
secretAccessKey: cfg.secretAccessKey || "",
|
||||
},
|
||||
forcePathStyle: true,
|
||||
// forcePathStyle needed for MinIO/Ceph/generic S3; harmless for AWS
|
||||
forcePathStyle: hasCustomEndpoint,
|
||||
requestHandler: new NodeHttpHandler({
|
||||
connectionTimeout: 15000,
|
||||
socketTimeout: 300000,
|
||||
...(isHttpsEndpoint ? { httpsAgent: agent } : { httpAgent: agent }),
|
||||
...(isHttps ? { httpsAgent: agent } : { httpAgent: agent }),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function initS3() {
|
||||
const cfg = db.s3Config || {};
|
||||
if (cfg.endpoint && cfg.accessKeyId && cfg.secretAccessKey && cfg.bucket) {
|
||||
if (cfg.accessKeyId && cfg.secretAccessKey && cfg.bucket) {
|
||||
s3Client = buildS3Client(cfg);
|
||||
console.log(`[S3] Client initialized → ${cfg.endpoint} / ${cfg.bucket}`);
|
||||
} else {
|
||||
|
|
@ -340,8 +343,9 @@ app.get("/api/s3/config", requireAdmin, (req, res) => {
|
|||
|
||||
app.put("/api/s3/config", requireAdmin, (req, res) => {
|
||||
const { endpoint, region, bucket, accessKeyId, secretAccessKey } = req.body;
|
||||
if (!endpoint || !region || !bucket || !accessKeyId)
|
||||
return res.status(400).json({ success: false, error: "endpoint, region, bucket, and accessKeyId are required" });
|
||||
// endpoint is optional for AWS S3 (leave blank to use AWS default)
|
||||
if (!region || !bucket || !accessKeyId)
|
||||
return res.status(400).json({ success: false, error: "region, bucket, and accessKeyId are required" });
|
||||
if (!db.s3Config) db.s3Config = {};
|
||||
db.s3Config.endpoint = endpoint.trim();
|
||||
db.s3Config.region = region.trim();
|
||||
|
|
@ -363,8 +367,8 @@ app.post("/api/s3/test", requireAdmin, async (req, res) => {
|
|||
accessKeyId: accessKeyId || db.s3Config?.accessKeyId || "",
|
||||
secretAccessKey: secretAccessKey || db.s3Config?.secretAccessKey || "",
|
||||
};
|
||||
if (!testCfg.endpoint || !testCfg.bucket || !testCfg.accessKeyId || !testCfg.secretAccessKey)
|
||||
return res.status(400).json({ success: false, error: "All S3 fields required to test connection" });
|
||||
if (!testCfg.bucket || !testCfg.accessKeyId || !testCfg.secretAccessKey)
|
||||
return res.status(400).json({ success: false, error: "Bucket, Access Key ID, and Secret Key are required to test" });
|
||||
|
||||
const testClient = buildS3Client(testCfg);
|
||||
const testKey = `_dragonwind_test_${Date.now()}.txt`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue