feat: add web gateway — browser-based chat UI over WebSocket
New platform adapter that serves a full-featured chat interface via HTTP. Enables access from any device on the network (phone, tablet, desktop). Features: - aiohttp server with WebSocket real-time messaging - Token-based authentication - Markdown rendering (marked.js) + code highlighting (highlight.js) - Voice recording via MediaRecorder API + STT transcription - Image, voice, and document display - Typing indicator + message editing (streaming support) - Mobile responsive dark theme - Auto-reconnect on disconnect - Media file cleanup (24h TTL) Config: WEB_UI_ENABLED=true, WEB_UI_PORT=8765, WEB_UI_TOKEN=<token> No new dependencies — uses aiohttp already in [messaging] extra.
This commit is contained in:
parent
e50323f730
commit
a3905ef289
3 changed files with 1219 additions and 0 deletions
|
|
@ -31,6 +31,7 @@ class Platform(Enum):
|
||||||
SIGNAL = "signal"
|
SIGNAL = "signal"
|
||||||
HOMEASSISTANT = "homeassistant"
|
HOMEASSISTANT = "homeassistant"
|
||||||
EMAIL = "email"
|
EMAIL = "email"
|
||||||
|
WEB = "web"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -176,6 +177,9 @@ class GatewayConfig:
|
||||||
# Email uses extra dict for config (address + imap_host + smtp_host)
|
# Email uses extra dict for config (address + imap_host + smtp_host)
|
||||||
elif platform == Platform.EMAIL and config.extra.get("address"):
|
elif platform == Platform.EMAIL and config.extra.get("address"):
|
||||||
connected.append(platform)
|
connected.append(platform)
|
||||||
|
# Web UI uses enabled flag only
|
||||||
|
elif platform == Platform.WEB:
|
||||||
|
connected.append(platform)
|
||||||
return connected
|
return connected
|
||||||
|
|
||||||
def get_home_channel(self, platform: Platform) -> Optional[HomeChannel]:
|
def get_home_channel(self, platform: Platform) -> Optional[HomeChannel]:
|
||||||
|
|
@ -466,6 +470,18 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
|
||||||
name=os.getenv("EMAIL_HOME_ADDRESS_NAME", "Home"),
|
name=os.getenv("EMAIL_HOME_ADDRESS_NAME", "Home"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Web UI
|
||||||
|
web_enabled = os.getenv("WEB_UI_ENABLED", "").lower() in ("true", "1", "yes")
|
||||||
|
if web_enabled:
|
||||||
|
if Platform.WEB not in config.platforms:
|
||||||
|
config.platforms[Platform.WEB] = PlatformConfig()
|
||||||
|
config.platforms[Platform.WEB].enabled = True
|
||||||
|
config.platforms[Platform.WEB].extra.update({
|
||||||
|
"port": int(os.getenv("WEB_UI_PORT", "8765")),
|
||||||
|
"host": os.getenv("WEB_UI_HOST", "0.0.0.0"),
|
||||||
|
"token": os.getenv("WEB_UI_TOKEN", ""),
|
||||||
|
})
|
||||||
|
|
||||||
# Session settings
|
# Session settings
|
||||||
idle_minutes = os.getenv("SESSION_IDLE_MINUTES")
|
idle_minutes = os.getenv("SESSION_IDLE_MINUTES")
|
||||||
if idle_minutes:
|
if idle_minutes:
|
||||||
|
|
|
||||||
1191
gateway/platforms/web.py
Normal file
1191
gateway/platforms/web.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -829,6 +829,13 @@ class GatewayRunner:
|
||||||
return None
|
return None
|
||||||
return EmailAdapter(config)
|
return EmailAdapter(config)
|
||||||
|
|
||||||
|
elif platform == Platform.WEB:
|
||||||
|
from gateway.platforms.web import WebAdapter, check_web_requirements
|
||||||
|
if not check_web_requirements():
|
||||||
|
logger.warning("Web: aiohttp not installed. Run: pip install aiohttp")
|
||||||
|
return None
|
||||||
|
return WebAdapter(config)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _is_user_authorized(self, source: SessionSource) -> bool:
|
def _is_user_authorized(self, source: SessionSource) -> bool:
|
||||||
|
|
@ -848,6 +855,11 @@ class GatewayRunner:
|
||||||
if source.platform == Platform.HOMEASSISTANT:
|
if source.platform == Platform.HOMEASSISTANT:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Web UI users are authenticated via token at the WebSocket level.
|
||||||
|
# No additional allowlist check needed.
|
||||||
|
if source.platform == Platform.WEB:
|
||||||
|
return True
|
||||||
|
|
||||||
user_id = source.user_id
|
user_id = source.user_id
|
||||||
if not user_id:
|
if not user_id:
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue