- Atomic save: write to .tmp file then rename (prevents corruption
if container is killed mid-write)
- Backup: .bak copy created on successful load, used as fallback
if main file is corrupted
- Startup log shows loaded keys (verifies favorites survived)
Ensures radio_favorites and radio_volumes survive container
updates, crashes, and forced restarts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. FAB button (favorites star) now hidden when any panel is open.
Previously the FAB (z-index:20) covered the panel close button
(z-index:15), making it impossible to close the station sidebar.
2. Added margin-left:auto to radio-topbar-right so theme dots
always stay right-aligned. Previously they sat left when no
station was playing (missing flex:1 now-playing spacer).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Auto-rotation now stops completely when a station list or
favorites sidebar is visible. Resumes only when all panels
are closed. Interaction-based pause (drag/scroll) also
respects open panels - won't resume after 5s timeout if
a sidebar is still showing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Restore d.size-dependent point radius so larger cities (more
stations) show bigger dots like Radio Garden. Formula:
radius = clamp(0.12, 0.45, 0.06 + size * 0.005)
Zoom scaling also respects per-station size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace sprite-based markers (objectsData + SpriteMaterial with
soft 64x64 gradient texture → blurry at zoom) with optimized
point meshes:
- pointResolution(24): smooth 24-sided circles (no hexagons)
- pointAltitude(0.001): nearly flat on surface (no cylinder effect)
- sqrt-based zoom scaling: dots shrink when zooming in, grow when
zooming out → visually consistent at all zoom levels
- Removed three.js Sprite/SpriteMaterial/CanvasTexture imports
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Radio: Move controls from bottom bar to topbar above globe
(guild/channel selectors, now playing, volume, connection, stop)
- Radio: Theme selector moved inline into topbar-right
- Radio: Globe now fills remaining space below topbar
- Soundboard: Fix connection modal not opening when voiceStats is null
(modal now renders with '---' values when no voice data available)
- Both: Modal condition changed from `showConnModal && voiceStats` to
just `showConnModal`, handling null voiceStats gracefully
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of destroying and recreating the voice connection on every
station change, now checks if the bot is already in the target channel.
If same channel: only stops ffmpeg/player, spawns new stream, reuses
the existing connection (no reconnect flicker).
If different channel: full disconnect + reconnect as before.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Globe was black because Radio Garden CDN (rg-tiles.b-cdn.net) returns
403 without Referer: radio.garden header. Added server-side tile proxy
/api/radio/tile/:z/:x/:y with in-memory cache (max 500 tiles).
- Added radio_voicestats SSE broadcast (every 5s) with voice ping,
gateway ping, status, channel name, and connected-since timestamp.
- Added clickable "Verbunden" connection indicator in RadioTab bottom
bar with live ping display and connection details modal (matching
soundboard's existing modal pattern).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each plugin now uses its own @discordjs/voice group:
- Radio: group='radio'
- Soundboard: group='soundboard'
This prevents joinVoiceChannel from one bot overwriting the
other bot's connection. Both bots can now play simultaneously
in the same voice channel. Removed claimVoice system (not needed
with separate bots).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each plugin gets its own Discord client and token:
- DISCORD_TOKEN_JUKEBOX (fallback: DISCORD_TOKEN) → Soundboard
- DISCORD_TOKEN_RADIO → Radio
discord.ts: factory createClient() instead of singleton
plugin.ts: per-plugin context storage via registerPlugin(p, ctx)
index.ts: creates/logins/shutdowns multiple bots independently
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace static earth texture with tile-based rendering from CDN.
Tiles load progressively (zoom 1→2→3 base), then dynamically
load zoom 4-6 as user zooms in. Mercator→equirectangular
reprojection via strip-based canvas compositing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Plugins now claim voice per guild via claimVoice(). When soundboard
plays a sound, radio's cleanup runs automatically (kills ffmpeg,
broadcasts SSE stop event). Fixes stale "now playing" UI on tab switch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Search API: read title/subtitle/url from _source.page (nested)
- Channel click: extract correct ID from URL (last segment)
- Replace earth texture with higher-res 4096x2048 original
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NASA's image server blocks cross-origin requests. Downloaded the
3600x1800 Black Marble texture and serve it from web/public/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Set renderer pixel ratio to window.devicePixelRatio for crisp
rendering on Retina/HiDPI displays (was defaulting to 1)
- Upgraded Earth texture from low-res three-globe example (~2K)
to NASA Black Marble 3600x1800 for sharp detail when zooming
- Added pointResolution(6) for cleaner point geometry
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pointRadius was recalculated on every controls change event (including
rotation frames), causing 50k+ points to re-render each frame.
Now only triggers when altitude changes >5%, keeping rotation smooth.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SSE handler now distinguishes per-guild events (single NowPlaying)
from snapshots (Record<string, NowPlaying>) to prevent state corruption
- Theme selector moved left (right: 72px) to not overlap favorites button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Points now shrink proportionally when zooming in, preventing
overlap in dense areas and making individual stations clickable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 5 themes: Sunset (default), Midnight, Forest, Ocean, Cherry
- Theme selector dots in top-right corner, persisted to localStorage
- Globe accent colors (points, atmosphere) update with theme
- Globe pauses auto-rotation for 5s on any interaction (click, drag, scroll)
- All radio CSS vars scoped to .radio-container[data-theme]
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Server: inlineVolume on AudioResource, POST /api/radio/volume endpoint
- Volume persisted per guild, broadcast via SSE to all clients
- Frontend: volume slider in bottom bar with debounced API calls
- Volume icon changes based on level (muted/low/normal)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Jukebox has @snazzah/davey (Discord Audio Video Encryption) and ws
(WebSocket) as dependencies which were missing from the Gaming Hub.
Without davey, voice connections get stuck at 'signalling' because
Discord's voice servers require DAVE negotiation in @discordjs/voice 0.19.
Also removed unused prism-media (covered by @discordjs/opus).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Listen for state transitions and errors immediately after joinVoiceChannel,
not just after ensureConnectionReady succeeds. This will show if the
connection transitions at all internally or stays stuck at signalling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previous wrapper intercepted wrong methods (library→adapter vs adapter→library).
Now correctly wraps:
- sendPayload (adapter→gateway): logs op code and return value
- onVoiceServerUpdate/onVoiceStateUpdate (gateway→library): logs incoming events
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs sendPayload calls (op code, result), onVoiceServerUpdate
and onVoiceStateUpdate to identify why connection stays at signalling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Log voice state transitions (Signalling → Connecting → Ready etc.)
- Log play requests with sound name, guild, channel, file path
- Log connection creation, rejoin attempts, and failures
- Log AudioPlayer state changes and errors
- All prefixed with [Soundboard] for easy filtering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rotation continued while browsing stations, making selection impossible.
Now stops immediately when clicking any radio point.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- await sodium.ready + nacl preload (same as original jukebox)
- Add generateDependencyReport() for debugging
- Add type declarations for libsodium-wrappers and tweetnacl
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Only request Guilds + GuildVoiceStates. GuildMembers, GuildPresences
and MessageContent are privileged and require manual portal activation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ADMIN_PWD and ALLOWED_GUILD_IDS env vars to config
- Extend PluginContext with adminPwd and allowedGuildIds
- Add adminAuth and guildFilter middleware for plugins
- Add /api/admin/login endpoint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change SPA fallback from app.get('*') to app.get('/{*splat}')
as Express 5 uses path-to-regexp v8+ which requires named splat.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Radio Garden API Client (30K+ Orte, Sender-Suche, Stream-URL Auflösung)
- Discord Voice Streaming via ffmpeg (PCM Pipeline)
- Interactive 3D Globe (globe.gl) mit allen Radiosender-Standorten
- Sender-Panel mit Play/Stop/Favoriten
- Live-Suche nach Sendern und Städten
- Now-Playing Bar mit Equalizer-Animation
- Guild/Voice-Channel Auswahl
- SSE Broadcasting für Live-Updates
- Favoriten-System mit Persistenz
- Responsive Design (Mobile/Tablet/Desktop)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>