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>
- GET/PUT /api/ampp/config — store base URL + API key in db.json (no restart needed)
- POST /api/ampp/test — authenticate against AMPP token endpoint and report result
- Refactored AMPP_BASE/AMPP_API_KEY constants to dynamic getAmppBase()/getAmppApiKey()
functions so live config changes take effect immediately
- Admin → AMPP tab: base URL field, masked API key field, Test Connection + Save buttons
- Cached token invalidated automatically on config save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>