From 0e56438f1c2a621dc32a416450d300c48f287770 Mon Sep 17 00:00:00 2001 From: daniel wang Date: Wed, 9 Sep 2026 16:59:32 +0000 Subject: [PATCH] fix frontend css issue --- .env.example | 13 +- CHANGELOG.md | 40 +++ Cargo.lock | 1 + Makefile | 5 +- README.md | 11 +- README.zh-TW.md | 11 +- apps/web/src/App.tsx | 133 +++++++-- apps/web/src/animated-icons.tsx | 3 + apps/web/src/api.ts | 9 +- apps/web/src/computer.css | 19 +- apps/web/src/locales/en.ts | 34 ++- apps/web/src/locales/zh-TW.ts | 18 +- apps/web/src/refinements.css | 65 ++++- apps/web/src/responsive.css | 16 +- apps/web/src/run-monitor.tsx | 4 + apps/web/src/types.ts | 6 +- apps/web/src/voice-settings.tsx | 6 +- crates/api/Cargo.toml | 1 + crates/api/src/accounts.rs | 498 ++++++++++++++++++++++++++++++++ crates/api/src/attachments.rs | 5 +- crates/api/src/auth.rs | 472 ++++++++++++++++-------------- crates/api/src/db.rs | 304 ++++++++++++++++++- crates/api/src/main.rs | 30 +- crates/api/src/mcp.rs | 39 ++- crates/api/src/memory.rs | 43 ++- crates/api/src/monitor.rs | 76 ++++- crates/api/src/rooms.rs | 27 +- crates/api/src/routes.rs | 63 ++-- crates/api/src/routing.rs | 14 +- crates/api/src/runs.rs | 12 +- crates/api/src/schedules.rs | 42 ++- crates/api/src/screen_proxy.rs | 31 +- crates/api/src/sessions.rs | 27 +- crates/api/src/skills.rs | 42 +-- crates/api/src/state.rs | 6 +- crates/api/src/vault.rs | 41 +-- crates/api/src/voice.rs | 68 ++--- crates/api/src/voice_call.rs | 11 +- crates/api/src/workspace.rs | 46 ++- crates/contracts/src/model.rs | 10 - crates/contracts/src/voice.rs | 8 - crates/harness/src/resolve.rs | 84 ++++-- crates/harness/src/voice.rs | 11 +- docker-compose.yml | 5 +- docs/development.md | 2 +- docs/operations.md | 16 +- migrations/020_accounts.sql | 29 ++ scripts/capture-hero.mjs | 35 ++- scripts/dev.sh | 3 +- scripts/init-env.py | 5 +- tests/frontend.test.mjs | 18 +- 51 files changed, 1842 insertions(+), 676 deletions(-) create mode 100644 crates/api/src/accounts.rs create mode 100644 migrations/020_accounts.sql diff --git a/.env.example b/.env.example index 9b5fd8c..38f6449 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,16 @@ -XAI_API_KEY= -OPENCODE_GO_API_KEY= -OPENAI_API_KEY= +# 登入改為帳號密碼(在網頁上註冊),不再需要共享 token。 +# API 金鑰也不再讀環境變數:註冊後到「設定 → 模型」貼上,沒有金鑰就不能跑。 # Generate independent values with: openssl rand -hex 32 -LAZYBOY_APP_TOKEN= SANDBOX_SUPERVISOR_TOKEN= -# Keep this key stable when rotating the app login token. +# 這個金鑰要穩定輪替:換掉它會讀不到已加密的密碼庫。 LAZYBOY_VAULT_KEY= POSTGRES_PASSWORD=lazyboy -# 127.0.0.1 = 只有本機;0.0.0.0 = 開放區網(需 LAZYBOY_APP_TOKEN >= 32 字元); +# 127.0.0.1 = 只有本機;0.0.0.0 = 開放區網,讓別人用他自己的帳號登入; # 也可填單一網卡的 IP,把監聽限制在那個介面。 LAZYBOY_BIND_IP=127.0.0.1 +# 只有清單上的網域名稱會被接受(DNS rebinding 防護)。用 IP 或 localhost 連進來 +# 不需要填;用例如 lazyboy.local 這種位名時才加在這裡,逗號分隔。 +LAZYBOY_ALLOWED_HOSTS= SANDBOX_PROVIDER=docker DATABASE_URL=postgres://lazyboy:lazyboy@127.0.0.1:5434/lazyboy DATA_DIR=./data diff --git a/CHANGELOG.md b/CHANGELOG.md index 18fc810..befa2df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,46 @@ All notable changes to LazyBoy are documented here. +## [Unreleased] + +Sign in with your own account. The shared install token is gone, and so are the +model keys in the environment. + +- **Register and log in** with a username and password in the app. Passwords are + stored as PBKDF2-HMAC-SHA256 (120,000 rounds, per-password salt) and sessions + live in the database as token digests in an `HttpOnly`, `SameSite=Strict` + cookie. Repeated failures cool a username down for five minutes. +- **One workspace per account.** Agents, chats, runs, schedules, credential + vaults, and model keys belong to the account that created them; a second + account on the same install sees none of it. Work created before this release + is adopted by the first account that registers. +- **Model keys come from Settings → Models only.** `LAZYBOY_APP_TOKEN`, + `XAI_API_KEY`, `OPENCODE_GO_API_KEY`, and `OPENAI_API_KEY` are no longer read. + A workspace without a key does not quietly run on someone else's: the run + fails immediately and points at the setting to fix. +- **`LAZYBOY_ALLOWED_HOSTS`** lists the domain names an install may be addressed + by (DNS rebinding guard). IPs and `localhost` keep working with no config. +- Removed: the token login screen, `LAZYBOY_APP_TOKEN`, and the rule that a + non-loopback bind required a 32-character token. `SANDBOX_SUPERVISOR_TOKEN` + and `LAZYBOY_VAULT_KEY` are still required in `.env`. + +### 繁體中文 + +改成自己的帳號登入:共享的安裝 token 拿掉了,環境變數裡的模型金鑰也拿掉了。 + +- **在 App 裡註冊與登入**:密碼以 PBKDF2-HMAC-SHA256(120,000 輪、每組密碼各自的 + salt)存放,session 在資料庫只存雜湊,cookie 是 `HttpOnly` + `SameSite=Strict`。 + 同一帳號連續失敗會冷卻五分鐘。 +- **一個帳號一個工作區**:Agent、對話、執行紀錄、排程、憑證庫與模型金鑰都只屬於 + 建立它的帳號,同一套安裝的第二個帳號看不到任何資料;舊資料由第一個註冊的帳號接手。 +- **模型金鑰只吃「設定 → 模型」**:不再讀 `LAZYBOY_APP_TOKEN`、`XAI_API_KEY`、 + `OPENCODE_GO_API_KEY`、`OPENAI_API_KEY`。沒設金鑰的工作區不會偷偷用別人的金鑰, + 任務會立刻失敗並指出該去哪裡補。 +- **`LAZYBOY_ALLOWED_HOSTS`**:要改用網域名稱連進來時列在這裡(DNS rebinding 防護); + 用 IP 或 `localhost` 不用設定。 +- 移除:token 登入畫面、`LAZYBOY_APP_TOKEN`,以及「綁非 loopback 需 32 字元 token」 + 的規則。`.env` 仍需要 `SANDBOX_SUPERVISOR_TOKEN` 與 `LAZYBOY_VAULT_KEY`。 + ## [v0.1.0-alpha] - 2026-09-09 First public alpha. Self-hosted AI agent workspace: each agent gets an isolated Linux desktop, you watch it live, and you can take over at any time. diff --git a/Cargo.lock b/Cargo.lock index 9c3d594..6d1057d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1899,6 +1899,7 @@ dependencies = [ "fastembed", "futures-util", "hex", + "hmac", "http", "lazyboy-contracts", "lazyboy-control", diff --git a/Makefile b/Makefile index e7dc9c2..f743246 100644 --- a/Makefile +++ b/Makefile @@ -60,8 +60,7 @@ help: ## Show this help @echo " make web Build the frontend in $(WEB_DIR) (needs node/npm)" @echo " make clean cargo clean" @echo "" - @echo " After 'make up': open http://127.0.0.1:3101 and sign in with LAZYBOY_APP_TOKEN." - @echo " Set XAI_API_KEY in .env for chat." + @echo " After 'make up': open http://127.0.0.1:3101, register an account, and add a model API key in 設定 → 模型." # --- Environment ----------------------------------------------------------- @@ -79,7 +78,7 @@ up: env ## Build every image and start the whole stack in Docker @$(MAKE) --no-print-directory prepare-screen-network $(COMPOSE) up -d --build @echo "" - @echo "stack launched. open http://127.0.0.1:3101 and sign in with LAZYBOY_APP_TOKEN." + @echo "stack launched. open http://127.0.0.1:3101 and register your account." $(COMPOSE) ps # Compose owns `lazyboy_screen` (internal, labeled). A leftover from host-dev or diff --git a/README.md b/README.md index 1857c64..fa4e646 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,11 @@ A self-hosted AI agent workspace. Assign tasks in text or voice, watch the deskt LazyBoy gives each agent its own Linux desktop in Docker — browser, terminal, and files. You can run several agents, put them in a group, turn a demonstration into a skill, and schedule it to run again. -This is an early `v0.1.0-alpha` release with desktop and phone browser UIs. You bring your own model API key. +This is an early `v0.1.0-alpha` release with desktop and phone browser UIs. Every person registers their own account, and model API keys live in the app's settings rather than in the environment. ## Features +- **One account per person**: register a username and password, and the whole workspace — agents, chats, credentials, model keys — belongs to that account alone. - **A lasting workspace**: each agent has its own chats, run history, and optional long-term memory. - **A real computer**: open pages, use the terminal, organize files, drive the GUI — and watch it live. - **Attachments in chat**: send files or images along with the message; the agent can open them on its own desktop, and inbox copies expire on their own. @@ -36,7 +37,7 @@ This is an early `v0.1.0-alpha` release with desktop and phone browser UIs. You ## Quick start -You need Docker and Compose, Git, Make, Python 3, and an API key for a supported model. On macOS use Docker Desktop or OrbStack; on Linux use Docker Engine. +You need Docker and Compose, Git, Make, and Python 3, plus an API key for a supported model — you paste that key into the app after you register, not into `.env`. On macOS use Docker Desktop or OrbStack; on Linux use Docker Engine. From the repo root: @@ -44,14 +45,14 @@ From the repo root: make env ``` -Edit the generated `.env` and set one of `XAI_API_KEY`, `OPENCODE_GO_API_KEY`, or `OPENAI_API_KEY`. The init tool creates the login and service secrets; running it again keeps existing values. +The generated `.env` holds service secrets only (supervisor, credential vault, database); the init tool fills them in and keeps existing values when run again. Nothing about signing in or model access is configured here. ```bash make up make health ``` -Open **[http://127.0.0.1:3101](http://127.0.0.1:3101)**, sign in with `LAZYBOY_APP_TOKEN` from `.env`, create an agent, and pick a model. +Open **[http://127.0.0.1:3101](http://127.0.0.1:3101)**, register an account with any username and password you like, paste your model API key under **Settings → Models**, then create an agent and pick a model. Without a key nothing runs — the app tells you where to add one. The first run builds the API and Linux desktop images from source and takes a while. The build toolchain lives in the containers, so a full Docker deploy does not need Rust or Node.js on the host. @@ -71,7 +72,7 @@ For resource limits, environment variables, HTTPS, and in-container sudo, see [O ## On a phone -Desktop and phone share the same web UI. The API listens on `127.0.0.1` by default, so set `LAZYBOY_BIND_IP=0.0.0.0` (or one network card's address) in `.env` and recreate the api container before a phone on your network can reach it; off-loopback the login token has to be at least 32 characters. Then open that address in the phone browser — `127.0.0.1` there is the phone itself and will not reach another machine. +Desktop and phone share the same web UI. The API listens on `127.0.0.1` by default, so set `LAZYBOY_BIND_IP=0.0.0.0` (or one network card's address) in `.env` and recreate the api container before a phone on your network can reach it. Then open that address in the phone browser — `127.0.0.1` there is the phone itself and will not reach another machine — and register there too: two people on one install each see only their own agents. Reaching the app by a domain name instead of an IP or `localhost` additionally requires listing it in `LAZYBOY_ALLOWED_HOSTS`. Tap outside the chat sidebar to collapse it. On the remote desktop you can switch between tap-to-click and trackpad, and use the toolbar for keyboard, right-click, or drag. Put the service behind HTTPS before you expose it; see [Operations](./docs/operations.md#安全模型). diff --git a/README.zh-TW.md b/README.zh-TW.md index 49e4e61..bdb0782 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -18,10 +18,11 @@ LazyBoy 讓 Agent 在 Docker 裡使用自己的 Linux 桌面,操作瀏覽器、終端與檔案。你可以建立不同的 Agent、在群組中協作,把示範整理成技能,再安排定時執行。 -目前為早期版本 `v0.1.0-alpha`,提供桌面與手機瀏覽器介面;模型金鑰由你自行設定。 +目前為早期版本 `v0.1.0-alpha`,提供桌面與手機瀏覽器介面;每個人各自註冊帳號,模型金鑰在 App 的設定裡填寫,不放在環境變數。 ## 功能 +- **一人一帳號**:用帳號密碼註冊後,Agent、對話、憑證與模型金鑰都只屬於那個帳號,彼此不會混用。 - **持續的工作空間**:每個 Agent 有自己的對話、工作紀錄與可設定的長期記憶。 - **真的能操作電腦**:開網頁、使用終端、整理檔案、操作圖形介面,過程可即時觀看。 - **附件直接丟進對話**:訊息可帶檔案或圖片,Agent 能在自己的桌面開啟,放在收件匣的複本會自動到期清除。 @@ -36,7 +37,7 @@ LazyBoy 讓 Agent 在 Docker 裡使用自己的 Linux 桌面,操作瀏覽器 ## 快速開始 -需要 Docker 與 Compose、Git、Make、Python 3,以及一組支援的模型 API 金鑰。macOS 可使用 Docker Desktop 或 OrbStack;Linux 可使用 Docker Engine。 +需要 Docker 與 Compose、Git、Make、Python 3,以及一組支援的模型 API 金鑰——金鑰註冊後在 App 裡貼上,不寫進 `.env`。macOS 可使用 Docker Desktop 或 OrbStack;Linux 可使用 Docker Engine。 取得本專案原始碼後,在專案根目錄執行: @@ -44,14 +45,14 @@ LazyBoy 讓 Agent 在 Docker 裡使用自己的 Linux 桌面,操作瀏覽器 make env ``` -編輯產生的 `.env`,填入 `XAI_API_KEY`、`OPENCODE_GO_API_KEY` 或 `OPENAI_API_KEY` 其中之一。初始化工具會產生登入與服務所需的金鑰,重新執行時保留既有設定。 +產生的 `.env` 只有服務內部使用的秘密(supervisor、憑證庫、資料庫);初始化工具會產生這些值,重複執行時保留既有設定。登入與模型金鑰都不需要在這裡設定。 ```bash make up make health ``` -開啟 **[http://127.0.0.1:3101](http://127.0.0.1:3101)**,用 `.env` 裡的 `LAZYBOY_APP_TOKEN` 登入,建立 Agent 並選擇模型。 +開啟 **[http://127.0.0.1:3101](http://127.0.0.1:3101)**,自行註冊一組帳號密碼,到「**設定 → 模型**」貼上模型 API 金鑰,再建立 Agent 並選擇模型。沒有金鑰就不會執行任何工作,App 會告訴你要去哪裡補。 第一次會從原始碼建置 API 與 Linux 桌面映像,所需時間較長。容器內已包含建置環境,完整 Docker 部署不需要在主機安裝 Rust 或 Node.js。 @@ -71,7 +72,7 @@ make down # 停止服務,保留 PostgreSQL 資料 ## 在手機上使用 -手機與桌面使用同一個 Web 介面。API 預設只綁 `127.0.0.1`,請在 `.env` 設定 `LAZYBOY_BIND_IP=0.0.0.0`(或指定網卡位址)並重建 api 容器,區網裡的手機才連得到;綁非 loopback 時登入 token 需至少 32 字元。接著用手機瀏覽器開啟該位址——手機上的 `127.0.0.1` 只代表手機本身,不能拿來連另一台電腦。 +手機與桌面使用同一個 Web 介面。API 預設只綁 `127.0.0.1`,請在 `.env` 設定 `LAZYBOY_BIND_IP=0.0.0.0`(或指定網卡位址)並重建 api 容器,區網裡的手機才連得到。接著用手機瀏覽器開啟該位址——手機上的 `127.0.0.1` 只代表手機本身,不能拿來連另一台電腦——並在手機上也註冊一個帳號:同一套安裝裡,每個人只會看到自己的 Agent。若要用網域名稱(而不是 IP 或 `localhost`)連線,還要把該名稱加進 `LAZYBOY_ALLOWED_HOSTS`。 聊天側欄可點外側空白處收合。操作遠端桌面時,可切換直接點選與觸控板模式,使用工具列叫出鍵盤、按右鍵或拖曳。對外提供服務時請設定 HTTPS,詳見 [部署指南](./docs/operations.md#安全模型)。 diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index d39e3ad..98a7892 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,5 +1,6 @@ import { FormEvent, Fragment, KeyboardEvent as ReactKeyboardEvent, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { BotIcon, Brain, ChevronDown, ChevronsRight, CircleHelp, ClipboardPaste, Computer, Download, Ellipsis, Info, LogOut, Megaphone, Paperclip, Pencil, Pin, Plug, Plus, RefreshCw, Reply, Settings, Smartphone, Sparkle, Square, Upload, Users, X } from "./animated-icons"; +import { createPortal } from "react-dom"; +import { BotIcon, Brain, ChevronDown, ChevronsRight, CircleHelp, ClipboardPaste, Computer, Download, Ellipsis, Info, LogOut, MeetingLayout, Megaphone, Paperclip, Pencil, Pin, Plug, Plus, RefreshCw, Reply, Settings, Smartphone, Sparkle, Square, Upload, Users, X } from "./animated-icons"; import UseAnimations from "./use-animations"; import loading from "react-useanimations/lib/loading"; import arrowUp from "react-useanimations/lib/arrowUp"; @@ -51,7 +52,8 @@ type RightPart="computer"|"memory"|"settings"|"plugins"|"accounts"; type AccountDialog="phone"|"settings"|"model"|"voice"|"about"|"help"|"feedback"|null; function readSessionStore():Record{try{const raw=localStorage.getItem(SESSION_STORE);return raw?JSON.parse(raw) as Record:{}}catch{return {}}} function writeSessionStore(botId:string,sessionId:string){const store=readSessionStore();store[botId]=sessionId;localStorage.setItem(SESSION_STORE,JSON.stringify(store))} -function readPaneStore():{collapsed:boolean;part:RightPart}{try{const raw=localStorage.getItem(PANE_STORE);if(!raw)return{collapsed:false,part:"computer"};const value=JSON.parse(raw) as {collapsed?:boolean;part?:string};return{collapsed:Boolean(value.collapsed),part:value.part==="memory"||value.part==="settings"||value.part==="plugins"||value.part==="accounts"?value.part:"computer"}}catch{return{collapsed:false,part:"computer"}}} +function isPhoneLayout(){return typeof window!=="undefined"&&window.matchMedia("(max-width:700px)").matches} +function readPaneStore():{collapsed:boolean;part:RightPart;meeting:boolean}{try{const raw=localStorage.getItem(PANE_STORE);if(!raw)return{collapsed:false,part:"computer",meeting:false};const value=JSON.parse(raw) as {collapsed?:boolean;part?:string;meeting?:boolean};return{collapsed:Boolean(value.collapsed),part:value.part==="memory"||value.part==="settings"||value.part==="plugins"||value.part==="accounts"?value.part:"computer",meeting:Boolean(value.meeting)&&!isPhoneLayout()}}catch{return{collapsed:false,part:"computer",meeting:false}}} type WorkspacePrefs={name:string;showHidden:boolean}; function isDefaultWorkspaceName(name?:string|null){return !name||name==="Local workspace"||name==="本機工作區"} function readWorkspace():WorkspacePrefs{try{const raw=localStorage.getItem(WORKSPACE_STORE);if(!raw)return{name:t("localWorkspace"),showHidden:false};const value=JSON.parse(raw) as {name?:string;showHidden?:boolean};const name=value.name?.trim();return{name:isDefaultWorkspaceName(name)?t("localWorkspace"):name||t("localWorkspace"),showHidden:Boolean(value.showHidden)}}catch{return{name:t("localWorkspace"),showHidden:false}}} @@ -149,7 +151,8 @@ export function App(){ const [screenUrl,setScreenUrl]=useState(null); const [draft,setDraft]=useState(""); const [query,setQuery]=useState(""); const [createOpen,setCreateOpen]=useState(false); const [createMenuOpen,setCreateMenuOpen]=useState(false); const [groupOpen,setGroupOpen]=useState(false); const [deleteOpen,setDeleteOpen]=useState(false); const [computerOpen,setComputerOpen]=useState(false); const paneStart=readPaneStore(); - const [rightCollapsed,setRightCollapsed]=useState(paneStart.collapsed); const [rightPart,setRightPart]=useState(paneStart.part); + const [rightCollapsed,setRightCollapsed]=useState(paneStart.collapsed||isPhoneLayout()); const [rightPart,setRightPart]=useState(paneStart.part); + const [meetingMode,setMeetingMode]=useState(paneStart.meeting); const [sessionMenuOpen,setSessionMenuOpen]=useState(false); const [clearOpen,setClearOpen]=useState(false); const [sessionToDelete,setSessionToDelete]=useState(null); const [remembered,setRemembered]=useState>({}); const [resumedChips,setResumedChips]=useState>({}); const [retriedRuns,setRetriedRuns]=useState>({}); const [mobileNav,setMobileNav]=useState(false); const [error,setError]=useState(null); const [busy,setBusy]=useState(false); useEffect(()=>{ @@ -327,10 +330,17 @@ export function App(){ useEffect(()=>{setWorkspaceName(name=>isDefaultWorkspaceName(name)?t("localWorkspace"):name)},[locale]); useEffect(()=>{localStorage.setItem(WORKSPACE_STORE,JSON.stringify({name:workspaceName,showHidden}))},[workspaceName,showHidden]); useEffect(()=>{if(!sessionStoreKey||!activeSessionId)return;if(!sessions.some(session=>session.id===activeSessionId))return;if(!activeRoomId&&!sessions.some(session=>session.id===activeSessionId&&session.botId===activeId))return;writeSessionStore(sessionStoreKey,activeSessionId)},[sessionStoreKey,activeId,activeRoomId,activeSessionId,sessions]); - useEffect(()=>{localStorage.setItem(PANE_STORE,JSON.stringify({collapsed:rightCollapsed,part:rightPart}))},[rightCollapsed,rightPart]); + useEffect(()=>{localStorage.setItem(PANE_STORE,JSON.stringify({collapsed:rightCollapsed,part:rightPart,meeting:meetingMode}))},[rightCollapsed,rightPart,meetingMode]); + useEffect(()=>{const mq=window.matchMedia("(max-width:700px)");const stop=()=>{if(!mq.matches)return;setMeetingMode(false);setRightCollapsed(true)};stop();mq.addEventListener("change",stop);return()=>mq.removeEventListener("change",stop)},[]); useEffect(()=>{if(!paneBotId){setSchedules([]);return}api(`/api/bots/${paneBotId}/schedules`).then(setSchedules).catch(()=>setSchedules([]))},[paneBotId]); - useEffect(()=>{if(computer.takeoverRequested){setRightPart("computer");setRightCollapsed(false)}},[computer.takeoverRequested]); - function openPane(part:RightPart){setMobileNav(false);setRightPart(part);setRightCollapsed(false)} + useEffect(()=>{if(!computer.takeoverRequested)return;setRightPart("computer");if(isPhoneLayout())setComputerOpen(true);else setRightCollapsed(false)},[computer.takeoverRequested]); + function openPane(part:RightPart){ + setMobileNav(false); + if(part==="computer"&&isPhoneLayout()){setRightPart("computer");setRightCollapsed(true);setComputerOpen(true);return} + if(meetingMode&&part==="computer"){setRightPart("computer");setRightCollapsed(true);return} + setRightPart(part);setRightCollapsed(false); + } + function toggleMeeting(){if(isPhoneLayout())return;setMeetingMode(on=>{if(!on){setComputerOpen(false);setRightPart("computer");setRightCollapsed(false)}return !on})} async function openLoginScreen(){ const id=paneBot?.id||active?.id;if(!id)return; setRightPart("computer");setRightCollapsed(false);setComputerOpen(true); @@ -511,7 +521,7 @@ export function App(){ function openRoom(room:Room){setCallOpen(false);setMobileNav(false);setSessionMenuOpen(false);if(rightPart==="settings")setRightPart("computer");if(room.id===activeRoomId){if(!activeSessionId){const stored=readSessionStore()[`room:${room.id}`];if(stored)setActiveSessionId(stored);else void loadSessions()}return}setMessages([]);setActiveSessionId(null);setActiveId(null);setBusyMembers([]);setActiveRoomId(room.id)} async function deleteRoom(room:Room){setRoomToDelete(null);await action(async()=>{await api(`/api/rooms/${room.id}`,{method:"DELETE"});if(activeRoomId===room.id){setActiveRoomId(null);setActiveSessionId(null);setMessages([]);setBusyMembers([])}await loadBots()})} function openAccount(dialog:AccountDialog){setAccountOpen(false);setAccountDialog(dialog)} - async function logout(){setAccountOpen(false);await api("/api/session",{method:"DELETE",body:"{}"}).catch(()=>{});setBots([]);setRooms([]);setMcpServers([]);setActiveId(null);setActiveRoomId(null);setAuthRequired(true)} + async function logout(){setAccountOpen(false);await api("/api/auth/session",{method:"DELETE"}).catch(()=>{});setBots([]);setRooms([]);setMcpServers([]);setActiveId(null);setActiveRoomId(null);setAuthRequired(true)} async function changeComputer(operation:"boot"|"restart"|"stop"){ const botId=paneBotId;if(!botId)return; const previous=computer; @@ -582,7 +592,8 @@ export function App(){ const statusMembers=workingMembers; const topTools=