A 46MB file now splits into 6 chunks (all 6 streams active) instead
of 2 chunks (4 streams idle). Better saturation of available bandwidth
on high-latency or constrained links.
Large files (>32MB) now use presigned S3 multipart URLs via
/api/desktop/multipart/init, letting the browser PUT 32MB chunks
directly to S3 in 6 parallel streams. No data passes through Node.
Small files still use single presigned PUT. Updated HTTP mode
description to "HTTP multi-part upload".
Reverts accidental landing page overwrite. Files >32MB now use S3
multipart upload with 32MB chunks and 6 parallel streams per file.
Files <=32MB still use fast direct presigned PUT.
Files >32MB now use S3 multipart upload with 32MB chunks and 6 parallel
streams per file. Files <=32MB still use fast direct presigned PUT.
Fixes slow upload speeds for large files.
Files >32MB now use S3 multipart upload with 32MB chunks and 6 parallel
streams per file. Files <=32MB still use fast direct presigned PUT.
Fixes slow upload speeds for large files.
Addresses feedback from Gavin (VPM):
- Sort folders alphabetically in both upload tree and admin tree views
- Auto-select VPM as default folder on login instead of Root
- Fix S3 key construction for nested folders: convert "/" to "--" so FLX
correctly maps subfolders (e.g. Content/TEST - AMPP Demo now produces
Content--TEST - AMPP Demo--file.ext instead of Content/TEST - AMPP Demo--file.ext)
- Clarify HTTP mode note: "Files are processed 6 at a time" instead of
"up to 6 concurrent files" which implied a total file limit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs the first job's keys and data (truncated to 1000 chars) on each
/api/ampp/jobs request to help identify which field contains the asset name.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browser now uploads files directly to RustFS/S3 via presigned PUT URLs.
The Node server only generates signed URLs and tracks quota — file data
never touches the server. 6 concurrent file uploads.
Falls back to server-proxied PutObjectCommand upload if presigned fails.
Server changes:
- /api/presigned now checks folder permissions, quota, and blocked files
- /api/presigned/complete endpoint for post-upload quota tracking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add search/filter input to destination folder browser with 320px
scrollable container
- Add User Audit button showing each user's visible pages, role,
quota, and folder access permissions
- Fix admin Folders tab delete buttons (were broken due to
JSON.stringify quote conflicts in innerHTML onclick handlers)
- Add more AMPP job name field fallbacks and debug logging to
diagnose asset name display issue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The @aws-sdk/lib-storage Upload class internally uses
CreateMultipartUploadCommand for files over the part size threshold,
which returns non-standard XML from RustFS causing UnknownError.
PutObjectCommand does a simple single PUT request that RustFS handles
correctly. Fixed in both /api/upload and /api/link/:id/upload endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The manual CreateMultipartUploadCommand/UploadPartCommand/CompleteMultipartUploadCommand
flow fails with RustFS due to non-standard XML responses (same issue as HeadBucketCommand).
Switch front-end to use /api/upload endpoint which uses @aws-sdk/lib-storage Upload class —
the same method that worked reliably in VPM-Uploader with RustFS.
Uses XMLHttpRequest for upload progress tracking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AMPP API returns keys like 'state:jobState', 'name:text', 'type:jobType',
'creator:id', 'created:dateTime', 'job:id' — not plain 'status', 'name',
etc. Frontend was falling back to 'Job' / 'Unknown' for every entry.
Updated field lookups to read colon-namespaced keys first, with fallbacks
for compatibility. Also added 'aborted' to the failed status detection.
Split files into 32 MB chunks, POST 6 concurrently to /api/upload/chunk,
server proxies each chunk as an S3 multipart part. Up to 4 files upload
in parallel simultaneously. Achieves Aspera-class throughput over plain
HTTP with no UDP port forwarding or custom protocols required.
Same approach used by MASV under the hood.
- server.js: Add /api/upload/initiate, /chunk, /complete, /abort endpoints
- public/index.html: Replace single-PUT uploadHTTP() with parallel chunked version
Root cause: three critical bugs in the UDP upload flow:
1. Main server never registered sessions on the relay — it stored them
in its own memory but never called POST /session on the relay, so
the relay had no idea about any upload sessions.
2. Relay had no HTTP chunk endpoint — the Chrome extension sends chunks
via HTTP POST to /session/:id/chunk/:index, but the relay only had
a binary UDP listener. Added the HTTP fallback endpoint.
3. Relay had no CORS headers — browser requests from chrome-extension://
origins were blocked. Added CORS middleware.
The flow now works:
Browser → POST /api/udp/session (main server)
Main server → POST /session (relay, with s3Config)
Browser → POST /session/:id/chunk/:n (relay, via public URL)
Relay → S3 multipart upload
Browser → POST /api/udp/session/:id/complete (main server)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The relay container is reached internally via docker service name
(http://dragon-wind-relay:3001) but browsers need the public address.
Previously the internal URL was sent to clients causing UDP uploads to fail.
- Add publicRelayUrl field to relay config (server + admin UI)
- /api/udp/session now returns publicRelayUrl to browser clients
- Internal relayUrl still used for server-side health checks
- Falls back to internal URL if public not set
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The extension was saving and restoring UDP mode from storage. If UDP
was previously selected and the relay isn't configured, every upload
immediately fails with "UDP relay not configured".
- Default to HTTP mode on fresh installs (no saved mode)
- Before UDP upload, check /api/health relayConfigured flag and
auto-fall-back to HTTP with a warning if relay isn't set up
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same fix as the Chrome extension — the web GUI's uploadHTTP() was using
item.file.type (browser-determined MIME) instead of presigned.contentType
(server-signed MIME). For broadcast formats like MXF, R3D, BRAW where
the browser returns empty or generic types, this causes an S3 signature
mismatch and the PUT fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The presigned URL is signed with a specific Content-Type (determined by
the server's MIME map). If the browser's file.type doesn't match (common
for broadcast formats like MXF, R3D, BRAW), S3 rejects the PUT with a
signature mismatch. Now the extension uses presigned.contentType from the
server response instead of item.file.type.
Also added console logging for upload requests and detailed error
messages from S3 responses on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- apiFetch() now logs method+URL on start and status code on response
- Added 10s AbortController timeout to prevent infinite hangs
- Added try/catch wrapper in saveSettings() around login() call
- This helps diagnose the "stuck on connecting" issue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Login failures (wrong password) were caught by the 401 handler in
apiFetch() which threw "Session expired" instead of returning the
actual error message. Now /api/login 401 responses pass through
so the real "Invalid credentials" error is shown.
- Settings panel now shows error messages from login() failures
instead of staying stuck on "Saved — connecting…" forever.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add "alarms" permission to manifest — chrome.alarms.create() was
throwing "Cannot read properties of undefined" because the permission
was missing; also removed invalid "sockets" permission (not a valid MV3 perm)
- Remove all inline event handlers from popup.html (onclick, ondragover,
ondragleave, ondrop, onchange) — MV3 CSP blocks inline JS entirely;
all handlers moved to popup.js addEventListener() calls
- Replace inline onclick="removeFile(i)" on dynamically generated remove
buttons with data-idx attribute + delegated click listener on the list
container — same CSP fix for runtime-generated elements
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix duplicate id="ampp-status" collision between the monitor page and
admin AMPP panel — admin panel now uses id="ampp-cfg-status" so the
monitor status messages are no longer silently hijacked
- AMPP monitor now shows a clear "not configured" card with a direct
button to Admin → AMPP instead of a cryptic error when the API key
hasn't been entered yet
- Improve job field mapping to handle AMPP response variations
(jobStatus, displayName, jobType, results array vs items array)
- Add 30-second auto-refresh while on the Monitor tab; timer is cleared
when navigating away to avoid ghost requests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CORS middleware to server.js allowing chrome-extension:// origins
so the popup can make authenticated API requests without browser blocking
- Fix popup.js saveSettings(): require password on save, call login() directly
instead of tryConnect() to avoid password-not-found loop
- Fix init(): open settings panel automatically if no saved token, so users
know they need to enter credentials after first install or session expiry
- Don't persist password to chrome.storage (security), use remove('token')
instead of set({token:null}) to properly clear the old session
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add readonly+onfocus trick to prevent browser autofilling secret key with garbage
- Set region value="us-east-1" so it always has a real default, not just placeholder
- loadS3Config always clears secret field and sets contextual placeholder
- Secret hint now clearly shows saved vs not-saved state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add chrome-extension/ to Dockerfile COPY (was missing, caused 404 on download)
- Remove UDP Relay tab from admin panel (relay is server-side, no user config needed)
- Remove upload mode toggle buttons, replace with clean inline status bar
- Extension panel: drop relay port-forwarding note, simplify to 'extension required' message
- UDP hint shown inline only when extension is detected in browser
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace chip-pill folder selector with clean vertical tree list
- VPM text wordmark replaces vpm-logo.png in login + header (no PNG/invert hack)
- Wild Dragon logo/icon retained only on favicon and splash animation
- Auto-detect prefers-color-scheme on first load (no longer defaults to dark)
- System theme changes update UI if user hasn't manually toggled
- Remove dragon icon from login card and app header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added VPM logo (vpm-logo.png) to login card and header left side
(subtle, opacity 50%, auto-inverted in light mode)
- Footer at bottom of app: "Built by Zac Gaetano & Wild Dragon LLC
· In partnership with Broadcast Management Group"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- setup.sh generates .env with random ports (10000-59999) on first run,
ensuring WEB_PORT, RELAY_TCP_PORT, and RELAY_UDP_PORT are all distinct
- .env.example documents all available env vars with defaults
- docker-compose.yml already reads from .env via ${VAR:-default} syntax
- Run `bash setup.sh` to generate ports, `bash setup.sh --start` to also
bring up the stack in one step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>