From e2c6db7113f7dfbff0cfd7fe3fa97f1a3fc499dc Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Sun, 5 Apr 2026 20:42:21 -0400 Subject: [PATCH] fix: S3 generic compat, nav tab reliability, single logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- public/index.html | 44 ++++++++++++++++++++------------------------ server.js | 26 +++++++++++++++----------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/public/index.html b/public/index.html index 8ecbb0f..d868cb7 100644 --- a/public/index.html +++ b/public/index.html @@ -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)}
-
S3 Storage
-
UDP Relay
-
AMPP
-
🧩 Extension
-
Users
-
Folders
+
S3 Storage
+
UDP Relay
+
AMPP
+
🧩 Extension
+
Users
+
Folders
S3 Storage Configuration
-
+
+ + +
For MinIO, Backblaze, Cloudflare R2, Wasabi, etc. enter the full endpoint URL. Leave blank to use standard AWS S3.
+
@@ -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(); diff --git a/server.js b/server.js index c95057e..713dd1d 100644 --- a/server.js +++ b/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`;