diff --git a/package.json b/package.json
index 55e6c92..6ccf587 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@aws-sdk/lib-storage": "^3.650.0",
"@aws-sdk/s3-request-presigner": "^3.650.0",
"@smithy/node-http-handler": "^3.2.0",
+ "archiver": "^7.0.1",
"express": "^4.21.0",
"multer": "^1.4.5-lts.1"
},
diff --git a/public/index.html b/public/index.html
index ef597bf..8ecbb0f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -320,6 +320,15 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
.admin-tab.active{color:var(--blue-bright);border-bottom-color:var(--blue-bright)}
.admin-panel{display:none}
.admin-panel.active{display:block}
+/* EXTENSION INSTALL GUIDE */
+.ext-steps{display:flex;flex-direction:column;gap:.88rem;margin-bottom:1.5rem}
+.ext-step{display:flex;gap:1rem;align-items:flex-start}
+.ext-step-num{flex-shrink:0;width:28px;height:28px;border-radius:50%;background:var(--blue);color:#fff;font-size:.78rem;font-weight:700;display:flex;align-items:center;justify-content:center;margin-top:.1rem}
+.ext-step-body{flex:1}
+.ext-step-body strong{display:block;font-size:.88rem;color:var(--text-primary);margin-bottom:.25rem}
+.ext-step-body p{font-size:.82rem;color:var(--text-secondary);margin:0;line-height:1.55}
+.ext-step-body code{background:var(--bg-secondary);border:1px solid var(--border);border-radius:4px;padding:.1rem .4rem;font-family:'Courier New',monospace;font-size:.8rem;color:var(--blue-bright)}
+.ext-note{background:var(--bg-secondary);border:1px solid var(--border);border-left:3px solid var(--dragon);border-radius:8px;padding:1rem 1.1rem;font-size:.82rem;color:var(--text-secondary);line-height:1.6}
/* FORMS */
.form-group{margin-bottom:1rem}
@@ -520,6 +529,7 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
S3 Storage
UDP Relay
AMPP
+ 🧩 Extension
Users
Folders
@@ -578,6 +588,58 @@ body:not([data-theme="light"]) .header-wordmark{filter:invert(1)}
+
+
+
Chrome Extension — UDP Upload
+
The Dragon Wind Chrome extension enables UDP-accelerated uploads directly from your browser. Download the extension package below, then follow the steps to install it.
+
+
⬇️ Download Extension (.zip)
+
+
Installation Steps
+
+
+
1
+
+
Unzip the downloaded file
+
Extract dragon-wind-extension.zip to a permanent folder on your computer — Chrome needs this folder to stay in place.
+
+
+
+
2
+
+
Open Chrome Extensions
+
In Chrome, navigate to chrome://extensions or open the menu → More Tools → Extensions.
+
+
+
+
3
+
+
Enable Developer Mode
+
Toggle the Developer mode switch in the top-right corner of the Extensions page.
+
+
+
+
4
+
+
Load the extension
+
Click Load unpacked , then navigate to and select the dragon-wind-extension folder you unzipped in step 1.
+
+
+
+
5
+
+
Configure the extension
+
Click the 🐉 Dragon Wind icon in Chrome's toolbar. Enter this server's URL and log in with your credentials. UDP uploads will now be available from any page.
+
+
+
+
+
+ 📡 UDP Port Forwarding
+ For uploads from outside your local network, make sure your UDP relay port is forwarded in your router. You can find your assigned UDP port in the .env file (RELAY_UDP_PORT) or check the UDP Relay tab above.
+
+
+
User Management
@@ -753,7 +815,7 @@ function switchPage(name) {
function switchAdminTab(name) {
document.querySelectorAll('.admin-tab').forEach((t,i) => {
- t.classList.toggle('active', ['s3','relay','ampp','users','folders'][i] === name);
+ t.classList.toggle('active', ['s3','relay','ampp','extension','users','folders'][i] === name);
});
document.querySelectorAll('.admin-panel').forEach(p => p.classList.toggle('active', p.id === `admin-${name}`));
if (name === 'users') loadUsers();
@@ -761,6 +823,25 @@ function switchAdminTab(name) {
if (name === 'ampp') loadAmppConfig();
}
+async function downloadExtension() {
+ const btn = event.target;
+ btn.disabled = true; btn.textContent = '⏳ Preparing download…';
+ try {
+ const res = await fetch('/api/extension/download', { headers: { 'Authorization': 'Bearer ' + authToken } });
+ if (!res.ok) throw new Error(`Server error ${res.status}`);
+ const blob = await res.blob();
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url; a.download = 'dragon-wind-extension.zip'; a.click();
+ URL.revokeObjectURL(url);
+ btn.textContent = '✅ Downloaded!';
+ setTimeout(() => { btn.disabled = false; btn.textContent = '⬇️ Download Extension (.zip)'; }, 3000);
+ } catch(e) {
+ btn.disabled = false; btn.textContent = '⬇️ Download Extension (.zip)';
+ showToast('Download failed: ' + e.message, 'error');
+ }
+}
+
// ============================================================
// UPLOAD MODE
// ============================================================
diff --git a/server.js b/server.js
index 406dbc9..c95057e 100644
--- a/server.js
+++ b/server.js
@@ -10,6 +10,7 @@ const { NodeHttpHandler } = require("@smithy/node-http-handler");
const fs = require("fs");
const https = require("https");
const http = require("http");
+const archiver = require("archiver");
require("events").defaultMaxListeners = 50;
@@ -640,6 +641,21 @@ app.get("/api/ampp/jobs/:jobId", requireAuth, async (req, res) => {
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
});
+// ==================== CHROME EXTENSION DOWNLOAD ====================
+app.get("/api/extension/download", requireAdmin, (req, res) => {
+ const extDir = path.join(__dirname, "chrome-extension");
+ if (!fs.existsSync(extDir)) {
+ return res.status(404).json({ success: false, error: "chrome-extension directory not found" });
+ }
+ res.setHeader("Content-Type", "application/zip");
+ res.setHeader("Content-Disposition", "attachment; filename=\"dragon-wind-extension.zip\"");
+ const archive = archiver("zip", { zlib: { level: 6 } });
+ archive.on("error", (err) => { console.error("[Extension ZIP]", err.message); res.end(); });
+ archive.pipe(res);
+ archive.directory(extDir, "dragon-wind-extension");
+ archive.finalize();
+});
+
// ==================== HEALTH ====================
app.get("/api/health", (req, res) => {
res.json({