feat: settings overhaul, GIF picker, notifications, PTT, scroll fixes

Client:
- Wire up all 4 notification toggles (desktop, taskbar flash, sounds, @everyone)
- Add compact mode CSS with visible layout differences
- Add GIF picker with Tenor API (trending + search)
- Render inline images/GIFs instead of link previews for direct URLs
- Add push-to-talk via Rust GetAsyncKeyState polling (non-consuming)
- Add key capture UI for PTT keybinds (supports mouse buttons)
- Add error/success feedback on account settings (password, username)
- Fix mic stream cleanup on tab switch (VoiceAudioTab factory pattern)
- Fix message timestamps using UTC with proper timezone conversion
- Fix emoji reaction picker (was returning early on empty emoji)
- Fix chat scroll jumpiness with Discord-style overflow-anchor + ResizeObserver
- Pre-measure all message heights on load to prevent first-scroll jump
- Improve scrollbar visibility with semi-transparent white thumb
- Show client version on Logs tab

Server:
- Add admin_allowed_cidrs config to restrict /admin to private networks
- Fix voice config defaults lost when YAML section has omitted fields
- Add negative caching to updater (5min error cache)
This commit is contained in:
jevb
2026-03-19 12:23:24 +01:00
parent b5b8be8370
commit b2038cecdb
24 changed files with 1330 additions and 66 deletions
+52 -5
View File
@@ -45,11 +45,12 @@ type VoiceConfig struct {
// ServerConfig holds HTTP server settings.
type ServerConfig struct {
Port int `koanf:"port"`
Name string `koanf:"name"`
DataDir string `koanf:"data_dir"`
AllowedOrigins []string `koanf:"allowed_origins"`
TrustedProxies []string `koanf:"trusted_proxies"`
Port int `koanf:"port"`
Name string `koanf:"name"`
DataDir string `koanf:"data_dir"`
AllowedOrigins []string `koanf:"allowed_origins"`
TrustedProxies []string `koanf:"trusted_proxies"`
AdminAllowedCIDRs []string `koanf:"admin_allowed_cidrs"`
}
// DatabaseConfig holds database settings.
@@ -81,6 +82,14 @@ func defaults() Config {
DataDir: "data",
AllowedOrigins: []string{"*"},
TrustedProxies: []string{},
AdminAllowedCIDRs: []string{
"127.0.0.0/8", // localhost IPv4
"::1/128", // localhost IPv6
"10.0.0.0/8", // private class A
"172.16.0.0/12", // private class B
"192.168.0.0/16", // private class C
"fc00::/7", // IPv6 unique local
},
},
Database: DatabaseConfig{
Path: "data/chatserver.db",
@@ -117,6 +126,12 @@ server:
data_dir: "data"
# allowed_origins: ["*"] # restrict WebSocket origins, e.g. ["https://example.com"]
# trusted_proxies: [] # CIDRs of trusted reverse proxies, e.g. ["10.0.0.0/8"]
# admin_allowed_cidrs: # CIDRs allowed to access /admin (default: private networks only)
# - "127.0.0.0/8"
# - "::1/128"
# - "10.0.0.0/8"
# - "172.16.0.0/12"
# - "192.168.0.0/16"
database:
path: "data/chatserver.db"
@@ -199,9 +214,41 @@ func Load(cfgPath string) (*Config, error) {
return nil, fmt.Errorf("unmarshalling config: %w", err)
}
// Apply voice defaults for zero-value fields (koanf loses defaults when
// the YAML section is present but fields are commented out / omitted).
applyVoiceDefaults(&cfg.Voice)
return &cfg, nil
}
// applyVoiceDefaults fills in zero-value voice fields with sensible defaults.
// This guards against the koanf merge behaviour where an empty YAML section
// overwrites struct defaults with Go zero values.
func applyVoiceDefaults(v *VoiceConfig) {
def := defaults().Voice
if v.STUNPort == 0 {
v.STUNPort = def.STUNPort
}
if v.TURNPort == 0 {
v.TURNPort = def.TURNPort
}
if v.Quality == "" {
v.Quality = def.Quality
}
if v.MediaPortMin == 0 {
v.MediaPortMin = def.MediaPortMin
}
if v.MediaPortMax == 0 {
v.MediaPortMax = def.MediaPortMax
}
if v.MixingThreshold == 0 {
v.MixingThreshold = def.MixingThreshold
}
if v.TopSpeakers == 0 {
v.TopSpeakers = def.TopSpeakers
}
}
// validateYAML checks that raw bytes are valid YAML.
func validateYAML(raw []byte) error {
var v any