From c93c14ece9dc510507da20eb9f867f74fc8c90f1 Mon Sep 17 00:00:00 2001 From: Lukas Parsons Date: Tue, 7 Jul 2026 12:21:41 -0400 Subject: [PATCH] fix: zoom-aware getBoundingClientRect positioning for all dropdowns, menus, pickers, and overlays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app.js: overflow plus-menu positionMenu() — divide r.left/r.top by zoom cookbookRunning.js: hamburger dropdown — use window.innerWidth - (rect.right/z) cookbookServe.js: 5 fixes — toolbar dropdown, launch-more, split-arrow, GPU popup, saved-menu skills.js: kebab tool menu — divide rect bounds by zoom colorPicker.js: picker position() — divide rect coords by zoom emojiPicker.js: picker open — divide rect coords by zoom theme.js: zone highlights — divide all rect values by zoom --- .env.save | 221 ++++++++++++++++++++++++++++++++++ cuda-keyring_1.1-1_all.deb | Bin 0 -> 4332 bytes docker-compose.gpu-nvidia.yml | 104 ++++++++-------- docker-compose.yml | 9 ++ docs/improvement-tasks.md | 18 ++- routes/cookbook_routes.py | 3 + static/app.js | 7 +- static/js/colorPicker.js | 7 +- static/js/cookbookRunning.js | 4 +- static/js/cookbookServe.js | 20 +-- static/js/emojiPicker.js | 5 +- static/js/skills.js | 7 +- static/js/theme.js | 9 +- 13 files changed, 331 insertions(+), 83 deletions(-) create mode 100755 .env.save create mode 100644 cuda-keyring_1.1-1_all.deb diff --git a/.env.save b/.env.save new file mode 100755 index 00000000..8fb5f244 --- /dev/null +++ b/.env.save @@ -0,0 +1,221 @@ +# Odysseus UI — Environment Configuration +# Copy this file to .env and fill in your values. + +COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml + +# ============================================================ +# LLM Configuration +# ============================================================ + +# Primary LLM host (default: localhost) +LLM_HOST=localhost + +# Additional LLM hosts, comma-separated (for model discovery) +# Use hostnames/IPs only; Odysseus scans common serve ports, including Ollama's 11434. +# LLM_HOSTS=llm-host.local,backup-llm.local + +# Optional Ollama base URL. In Docker, host Ollama is usually reachable here +# when started with OLLAMA_HOST=0.0.0.0:11434. +# OLLAMA_BASE_URL=http://host.docker.internal:11434/v1 + +# Optional LM Studio URL. In Docker, host LM Studio is reachable here +# when LM Studio is set to serve on all interfaces (0.0.0.0). +# LM_STUDIO_URL=http://host.docker.internal:1234 + +# OpenAI API key (only needed if using OpenAI models). +# Do not commit real keys. Keep this commented until needed. +# OPENAI_API_KEY=your_openai_api_key_here + +# Research service LLM endpoint +# RESEARCH_LLM_ENDPOINT=http://localhost:8000/v1/chat/completions + +# Extra CA bundle for LLM providers whose TLS chain isn't in the default +# trust store. Layered ON TOP of the system / certifi bundle — verification +# stays on for every host, the trust set just gets larger. Useful for: +# - GigaChat / Sber (Russian Trusted Root CA): without this the endpoint +# shows offline with CERTIFICATE_VERIFY_FAILED — self-signed certificate +# in certificate chain. +# - On-premise / corporate LLM gateways with an internal CA. +# Point at a PEM file containing the missing root(s). +# LLM_CA_BUNDLE=/etc/odysseus/ca/extra-roots.pem + +# ============================================================ +# Search & Web +# ============================================================ + +# SearXNG instance URL (self-hosted, for web search). +# Docker Compose overrides this to http://searxng:8080 for in-network access. +SEARXNG_INSTANCE=http://localhost:8080 + +# Optional SearXNG cookie/CSRF secret. If blank, Docker generates one on first boot +# and stores it in the searxng-data volume. +# SEARXNG_SECRET= + +# ============================================================ +# Database +# ============================================================ + +# SQLite database path (default: sqlite:///./data/app.db) +# DATABASE_URL=sqlite:///./data/app.db + +# ============================================================ +# Data directory +# ============================================================ +# Move everything that lives under data/ - settings, sessions, database, auth, +# cache, uploads, etc. - to another path: +# ODYSSEUS_DATA_DIR=C:\path\to\dir + +# ============================================================ +# Auth & Security +# ============================================================ + +# Enable authentication (default: true) +# AUTH_ENABLED=true + +# Host bind address and port for the Odysseus web UI in Docker Compose. +# Keep APP_BIND on loopback unless you intentionally want LAN/reverse-proxy access. + APP_BIND=0.0.0.0 +# Change this if another local service already uses 7000 (macOS AirPlay often does). +# APP_PORT=7000 + +# Development-only auth bypass for loopback requests. +# Keep false for Docker, LAN, reverse proxy, and any shared deployment. +# LOCALHOST_BYPASS=false + +# Mark session cookies Secure. Set true when Odysseus is served through HTTPS +# by a trusted reverse proxy or private access gateway. +# SECURE_COOKIES=true + +# Optional: pre-seed the first admin password during setup. +# Do not commit a real password. +# ODYSSEUS_ADMIN_PASSWORD=change_me_before_first_boot + +# CORS allowed origins (default: localhost-only; restrict to your public origin in production) +# ALLOWED_ORIGINS=http://localhost:7000,http://localhost:8000 + +# ============================================================ +# ChromaDB (vector store) +# ============================================================ + +# ChromaDB service host. +# Manual host run: localhost:8100 when using `docker run -p 8100:8000 chromadb/chroma`. +# Docker Compose overrides these to chromadb:8000 for in-network access. +# CHROMADB_HOST=localhost +# CHROMADB_PORT=8100 + +# Docker Compose host-port bind addresses for bundled services. +# Defaults are loopback-only for safety. To expose ntfy only on Tailscale, +# set NTFY_BIND to your host's Tailscale IP and update NTFY_BASE_URL. +# CHROMADB_BIND=127.0.0.1 +# NTFY_BIND=127.0.0.1 +# NTFY_BASE_URL=http://localhost:8091 +# Example: +# NTFY_BIND=100.x.y.z +# NTFY_BASE_URL=http://100.x.y.z:8091 + +# ============================================================ +# RAG / Embeddings +# ============================================================ + +# Embedding API endpoint (OpenAI-compatible /v1/embeddings) +# Default: http://{LLM_HOST}:11434/v1/embeddings (ollama) +# EMBEDDING_URL=http://localhost:11434/v1/embeddings + +# Embedding API key (if there's one) +# EMBEDDING_API_KEY=embedding_api_key_here + +# Embedding model name (must be available at the endpoint above) +# EMBEDDING_MODEL=all-minilm:l6-v2 + +# Local fallback embedding model (used when no HTTP embedding API is available) +# Uses fastembed (ONNX) — downloads model on first run (~50MB) +# FASTEMBED_MODEL=sentence-transformers/all-MiniLM-L6-v2 +# FASTEMBED_CACHE_PATH= # defaults to ~/.cache/fastembed + +# ============================================================ +# Misc +# ============================================================ + +# Cleanup interval in hours (default: 24) +# CLEANUP_INTERVAL_HOURS=24 + +# In-process email pollers (default: on). Set to 0 if you're driving +# polling from cron / systemd via `scripts/odysseus-mail poll-scheduled` +# and `scripts/odysseus-mail poll-summary`, otherwise both schedulers +# race on the same SQLite. +# ODYSSEUS_INPROCESS_POLLERS=1 + +# In-process scheduled-task runner (default: on). Set to 0 to let an +# external driver fire scheduled tasks. Calendar reminders are +# frontend-driven (polling /api/notes from the browser) so no gate is +# needed there. +# ODYSSEUS_INPROCESS_TASKS=1 + +# Host used by the built-in "run_script" scheduled-task action. +# Empty/local/localhost runs scripts on the app host. Set to an SSH host alias +# if you intentionally want scheduled scripts to run remotely. +# ODYSSEUS_SCRIPT_HOST=localhost + +# Chat / agent attachment size cap in bytes (default: 10 MB). +# Raise this for local installs that need larger PDFs or text documents. +# Example: 52428800 = 50 MB. +# ODYSSEUS_CHAT_UPLOAD_MAX_BYTES=10485760 + +# Other per-feature upload size caps in bytes. All are validated and optional; +# defaults shown. An invalid value (non-integer or < 1) fails fast at startup. +# ODYSSEUS_GALLERY_UPLOAD_MAX_BYTES=104857600 # gallery image upload (100 MB) +# ODYSSEUS_GALLERY_TRANSFORM_UPLOAD_MAX_BYTES=26214400 # gallery transform input (25 MB) +# ODYSSEUS_MEMORY_IMPORT_MAX_BYTES=10485760 # memory import file (10 MB) +# ODYSSEUS_PERSONAL_UPLOAD_MAX_BYTES=26214400 # personal document upload (25 MB) +# ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=26214400 # email compose attachment (25 MB) +# ODYSSEUS_STT_MAX_AUDIO_BYTES=26214400 # speech-to-text audio (25 MB) +# ODYSSEUS_ICS_MAX_BYTES=10485760 # calendar .ics import (10 MB) + +# ============================================================ +# Host Docker access (explicit opt-in) +# ============================================================ +# Default Docker Compose does not mount /var/run/docker.sock. Existing +# Ollama, vLLM, and other OpenAI-compatible endpoints remain usable without it. +# +# Enable this only for intentional Cookbook/local Docker-daemon management. +# Raw socket access is high-trust and can grant broad control over the host +# Docker daemon. Set DOCKER_GID to the host docker group's numeric GID. +# Put these values in .env, or export them before running docker compose. +# COMPOSE_FILE=docker-compose.yml:docker/host-docker.yml +# DOCKER_GID=963 +# docker/host-docker.yml sets this inside the container. Keep it paired +# with the socket overlay; setting it alone is not sufficient. +# ODYSSEUS_ENABLE_HOST_DOCKER=true +# +# Host Docker access can be combined with one GPU overlay: +# COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml:docker/host-docker.yml +# COMPOSE_FILE=docker-compose.yml:docker/gpu.amd.yml:docker/host-docker.yml + +# ============================================================ +# GPU support (Docker Compose) +# ============================================================ +# Pass the host GPU into the odysseus container. Default (unset) = CPU. +# COMPOSE_FILE is a native `docker compose` feature: a colon-separated +# list of files merged left-to-right. Pick ONE GPU line below, or leave +# all commented for CPU. +# +# NVIDIA (requires nvidia-container-toolkit + `nvidia-ctk runtime +# configure --runtime=docker` on the host): +# COMPOSE_FILE=docker-compose.yml:docker/gpu.nvidia.yml +# COMPOSE_FILE=docker-compose.yml;docker/gpu.nvidia.yml #(Windows) +# +# AMD ROCm (requires ROCm drivers on the host and the GID of the render group): +# COMPOSE_FILE=docker-compose.yml:docker/gpu.amd.yml + # Find the render GID with: getent group render | cut -d: -f3 +# RENDER_GID=989 +# +# These overlays only expose the GPU devices. The slim Odysseus image +# still needs CUDA/ROCm userspace via Cookbook -> Dependencies (vLLM, +# llama-cpp-python, etc.) before models can actually serve on GPU. + +# ============================================================ +# Storage Paths (Docker Compose) +# ============================================================ + +# APP_DATA_DIR=./data +# APP_LOGS_DIR=./logs diff --git a/cuda-keyring_1.1-1_all.deb b/cuda-keyring_1.1-1_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..d02294184b32b91eb1a7d348406ebaa0f3218bdd GIT binary patch literal 4332 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 literal 0 HcmV?d00001 diff --git a/docker-compose.gpu-nvidia.yml b/docker-compose.gpu-nvidia.yml index 1b551c66..04097f23 100644 --- a/docker-compose.gpu-nvidia.yml +++ b/docker-compose.gpu-nvidia.yml @@ -85,8 +85,8 @@ services: - NVIDIA_VISIBLE_DEVICES=all - NVIDIA_DRIVER_CAPABILITIES=compute,utility depends_on: - searxng: - condition: service_healthy +# searxng: +# condition: service_healthy chromadb: condition: service_started restart: unless-stopped @@ -109,55 +109,55 @@ services: - ANONYMIZED_TELEMETRY=FALSE restart: unless-stopped - searxng: - # Pinned, not :latest — odysseus waits on searxng's healthcheck - # (depends_on: condition: service_healthy), so a broken upstream `latest` - # tag blocks the whole app from starting. 2026.6.2 crashes on boot with - # `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414). - # Bump this deliberately after verifying a newer tag boots clean. - image: docker.io/searxng/searxng:2026.5.31-7159b8aed - entrypoint: - - /bin/sh - - -c - - | - set -eu - if [ ! -s /etc/searxng/settings.yml ] || grep -q 'odysseus-local-searxng-json-2026-05-30\|__SEARXNG_SECRET__' /etc/searxng/settings.yml; then - secret="$${SEARXNG_SECRET:-}" - if [ -z "$$secret" ]; then - secret="$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')" - fi - sed "s|__SEARXNG_SECRET__|$$secret|g" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml - fi - exec /usr/local/searxng/entrypoint.sh - ports: - - "127.0.0.1:8080:8080" - volumes: - - searxng-data:/etc/searxng - - ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z - environment: - - SEARXNG_BASE_URL=http://localhost:8080/ - - SEARXNG_SECRET=${SEARXNG_SECRET:-} - # The official searxng image runs as the non-root `searxng` user, but its - # entrypoint still needs to chown /etc/searxng on first boot, drop privs via - # su-exec, and (with our wrapper above) write settings.yml into the named - # volume. Without these capabilities the wrapper aborts at the redirection - # with EACCES and the container fails its healthcheck with permission - # errors during setup. Mirrors the cap set recommended by the upstream - # searxng-docker compose file. See issue #721. - cap_drop: - - ALL - cap_add: - - CHOWN - - SETGID - - SETUID - - DAC_OVERRIDE - healthcheck: - test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\""] - interval: 5s - timeout: 6s - retries: 20 - start_period: 10s - restart: unless-stopped + # searxng: + # Pinned, not :latest — odysseus waits on searxng's healthcheck + # (depends_on: condition: service_healthy), so a broken upstream `latest` + # tag blocks the whole app from starting. 2026.6.2 crashes on boot with + # `KeyError: 'default_doi_resolver'`, failing the healthcheck (issue #1414). + # Bump this deliberately after verifying a newer tag boots clean. + # image: docker.io/searxng/searxng:2026.5.31-7159b8aed + # entrypoint: + # - /bin/sh + # - -c + # - | + # set -eu + # if [ ! -s /etc/searxng/settings.yml ] || grep -q 'odysseus-local-searxng-json-2026-05-30\\|__SEARXNG_SECRET__' /etc/searxng/settings.yml; then + # secret=\"$${SEARXNG_SECRET:-}\" + # if [ -z \"$$secret\" ]; then + # secret=\"$$(python -c 'import secrets; print(secrets.token_urlsafe(48))')\" + # fi + # sed \"s|__SEARXNG_SECRET__|$$secret|g\" /tmp/searxng-settings.yml.template > /etc/searxng/settings.yml + # fi + # exec /usr/local/searxng/entrypoint.sh + # ports: + # - \"127.0.0.1:8080:8080\" + # volumes: + # - searxng-data:/etc/searxng + # - ./config/searxng/settings.yml:/tmp/searxng-settings.yml.template:ro,z + # environment: + # - SEARXNG_BASE_URL=http://localhost:8080/ + # - SEARXNG_SECRET=${SEARXNG_SECRET:-} + # The official searxng image runs as the non-root `searxng` user, but its + # entrypoint still needs to chown /etc/searxng on first boot, drop privs via + # su-exec, and (with our wrapper above) write settings.yml into the named + # volume. Without these capabilities the wrapper aborts at the redirection + # with EACCES and the container fails its healthcheck with permission + # errors during setup. Mirrors the cap set recommended by the upstream + # searxng-docker compose file. See issue #721. + # cap_drop: + # - ALL + # cap_add: + # - CHOWN + # - SETGID + # - SETUID + # - DAC_OVERRIDE + # healthcheck: + # test: [\"CMD-SHELL\", \"python -c \\\"import urllib.request; urllib.request.urlopen('http://localhost:8080/', timeout=5).read(1)\\\"\"]\" + # interval: 5s + # timeout: 6s + # retries: 20 + # start_period: 10s + # restart: unless-stopped ntfy: image: docker.io/binwiederhier/ntfy @@ -171,6 +171,6 @@ services: restart: unless-stopped volumes: - searxng-data: +# searxng-data: chromadb-data: ntfy-cache: diff --git a/docker-compose.yml b/docker-compose.yml index 80f76698..ec1fb947 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,7 @@ services: # Ollama at http://host.docker.internal:11434. - "host.docker.internal:host-gateway" environment: + - LD_LIBRARY_PATH=/app/.local/bin - LLM_HOST=${LLM_HOST:-localhost} - LLM_HOSTS=${LLM_HOSTS:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-} @@ -76,6 +77,9 @@ services: chromadb: condition: service_started restart: unless-stopped + networks: + - default + - searxng-net chromadb: image: docker.io/chromadb/chroma:latest @@ -152,3 +156,8 @@ volumes: # searxng-data: chromadb-data: ntfy-cache: + +networks: + searxng-net: + external: true + name: searxng_default diff --git a/docs/improvement-tasks.md b/docs/improvement-tasks.md index 02bf96b7..62b356a1 100644 --- a/docs/improvement-tasks.md +++ b/docs/improvement-tasks.md @@ -4,14 +4,17 @@ Last updated: 2026-07-07 ## Priority: HIGH +- [ ] **AUTO_START_LOCAL: persist & auto-restore last running model on restart** + New `AUTO_START_LOCAL` env var (bool, default false). On graceful shutdown/restart, persist the currently running model + all launch parameters (engine, quantization, context length, GPU layers, etc.) to a file in `/app/data/`. On startup, if the file exists and AUTO_START_LOCAL is true, automate the full launch pipeline: resolve the saved preset, spin up the engine, and register the endpoint — skipping the manual cookbook → Launch → pick model → verify params → launch → wait → refresh cycle the user currently endures on every restart. + - [ ] **Floating windows overflow viewport after zoom/font changes — unreachable controls** `:root.ui-scale-125 { zoom: 1.25 }` renders all content 25% larger, but `window.innerHeight` doesn't change with `zoom` (CSS rendering only, not DOM). The `windowResize.js` saved-size restore at line 224 clamps to `window.innerHeight`, which after zoom makes windows 1.25× the visible viewport — pushing headers, close buttons, and top resize handles off-screen. The user literally can't close or move them. Fix: (a) divide restored height by zoom factor in the restore path; (b) add a general safety net — any `.modal-content` on open should clamp to `calc(100dvh / var(--ui-zoom, 1) - 40px)`. Also `windowResize.js` line 134 `if (top + height > vh)` needs zoom-aware `vh`. Current zoom compensation rules (line 181-196) only cover `.modal-content`, `.cal-modal-content`, `.settings-modal-content`, `#theme-popup`, `#cookbook-modal`, and `.pdf-export-overlay` — many windows lack the divisor. -- [ ] **Fix manage_documents tool schema — missing "read" action** - The `manage_documents` function-calling schema in `tool_schemas.py` has `"enum": ["list", "delete", "tidy"]` but the handler in `document_tools.py` supports `"read", "view", "open", "get"`. The model can't read documents through function calling. Fix: add read/view/open/get to enum, add `document_id`/`offset`/`limit` params, update description to mention reading. +- [x] **Fix manage_documents tool schema — missing "read" action** + The `manage_documents` function-calling schema in `tool_schemas.py` had `"enum": ["list", "delete", "tidy"]` but the handler in `document_tools.py` supports `"read", "view", "open", "get"`. The model couldn't read documents through function calling. Fixed: added read/view/open/get to enum, added `document_id`/`offset`/`limit` params with read-specific descriptions, updated description with "DOCUMENT PANEL (not disk)" prefix. Also added disambiguation prefix to `create_document` and `edit_document` schemas. -- [ ] **Add "Send to Chat" button to document editor** - Button left of the Save button in `#doc-actions-footer`. Sends a document reference (title + doc_id + snippet) to the active chat, similar to how Cursor injects file references. Must NOT paste full content — just a reference the agent can resolve. Backend needs an endpoint or the existing `open_panel: documents` UI action may be enough. Requires: `POST /api/document//reference` or use existing `ui_control` with a new action. Frontend: insert button in `document.js` `_buildPaneHTML()` at line ~4598, wire JS handler. +- [x] **Add "Send to Chat" button to document editor** + Button left of the Save button in `#doc-actions-footer`. Sends a document reference (title + doc_id + snippet) to the active chat, similar to how Cursor injects file references. Must NOT paste full content — just a reference the agent can resolve. Done: "Chat" button injects `[Title](#document-) — <120-char snippet>` into the #message textarea. ## Priority: MEDIUM @@ -27,11 +30,14 @@ Last updated: 2026-07-07 - Document references in chat that are clickable and auto-expand - The "Send to Chat" button (HIGH priority) is step 1; step 2 is making the agent proactively aware of library contents -- [ ] **Improve `create_document` schema description** - Current description is verbose and overlaps with `edit_file`/`write_file` disambiguation. Models sometimes use `create_document` for disk files. Add a clear leading signal: "DOCUMENT PANEL (not disk)" prefix like the existing "FILESYSTEM TOOL (not memory)" pattern. +- [x] **Improve `create_document` schema description** + Current description is verbose and overlaps with `edit_file`/`write_file` disambiguation. Models sometimes use `create_document` for disk files. Fixed: added "DOCUMENT PANEL (not disk)" prefix, explicit "For files on disk use write_file" at end. Also applied to `edit_document`. ## Priority: LOW +- [ ] **Cookbook sliders: active highlight misaligned with zoom** + The rounded highlight on CPU/GPU/RAM etc. toggle sliders doesn't scale with `ui-scale-125`. The active indicator (e.g. "GPU") is partially outside the highlight pill. Low priority — cosmetic, obvious which option is selected. + - [ ] **Document library: batch operations** Multi-select, bulk delete, bulk export. Currently only single-document operations. diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py index 7a35de97..0ab1efbd 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -2696,6 +2696,9 @@ def setup_cookbook_routes() -> APIRouter: elif disk_env.get("servers") and not inc_env.get("servers"): inc_env["servers"] = disk_env["servers"] logger.warning("cookbook state POST: incoming env.servers empty; preserved on-disk servers (anti-wipe guard)") + if disk_env.get("hfToken") and not inc_env.get("hfToken"): + inc_env["hfToken"] = disk_env["hfToken"] + logger.warning("cookbook state POST: incoming env.hfToken empty; preserved on-disk hfToken (anti-wipe guard)") disk_tasks = on_disk.get("tasks") or [] if isinstance(on_disk, dict) else [] incoming_tasks = data.get("tasks") if isinstance(data.get("tasks"), list) else [] diff --git a/static/app.js b/static/app.js index 927ee8b2..327925b0 100644 --- a/static/app.js +++ b/static/app.js @@ -1909,19 +1909,20 @@ function initializeEventListeners() { // genuinely taller than the room above the button. function positionMenu() { const r = plusBtn.getBoundingClientRect(); - menu.style.left = r.left + 'px'; + const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; + menu.style.left = (r.left / z) + 'px'; menu.style.right = 'auto'; menu.style.bottom = 'auto'; menu.style.maxHeight = ''; // reset so we can measure the natural height menu.style.overflowY = ''; - const avail = r.top - 16; // room above the chevron + const avail = (r.top / z) - 16; // room above the chevron (viewport-relative) const natural = menu.scrollHeight; const h = Math.min(natural, avail); if (natural > avail) { // only cap + scroll when it doesn't fit menu.style.maxHeight = avail + 'px'; menu.style.overflowY = 'auto'; } - menu.style.top = (r.top - 8 - h) + 'px'; + menu.style.top = ((r.top / z) - 8 - h) + 'px'; } // Tapping the chevron must NOT steal focus from the message box, or the // mobile keyboard collapses. preventDefault on pointerdown keeps the diff --git a/static/js/colorPicker.js b/static/js/colorPicker.js index 3c223080..1f2e4136 100644 --- a/static/js/colorPicker.js +++ b/static/js/colorPicker.js @@ -293,10 +293,11 @@ function commitCurrent() { function position(p, anchor) { const rect = anchor.getBoundingClientRect(); const pRect = p.getBoundingClientRect(); - let left = rect.left; - let top = rect.bottom + 6; + const zc = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; + let left = rect.left / zc; + let top = (rect.bottom / zc) + 6; if (left + pRect.width > window.innerWidth - 8) left = window.innerWidth - pRect.width - 8; - if (top + pRect.height > window.innerHeight - 8) top = rect.top - pRect.height - 6; + if (top + pRect.height > window.innerHeight - 8) top = (rect.top / zc) - pRect.height - 6; if (left < 8) left = 8; if (top < 8) top = 8; p.style.left = left + 'px'; diff --git a/static/js/cookbookRunning.js b/static/js/cookbookRunning.js index 396614a1..9f03fe2e 100644 --- a/static/js/cookbookRunning.js +++ b/static/js/cookbookRunning.js @@ -2530,8 +2530,8 @@ export function _renderRunningTab() { const rect = menuBtn.getBoundingClientRect(); const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; dropdown.style.position = 'fixed'; - dropdown.style.top = rect.bottom + 2 + 'px'; - dropdown.style.right = ((window.innerWidth / z) - rect.right) + 'px'; + dropdown.style.top = (rect.bottom / z) + 2 + 'px'; + dropdown.style.right = (window.innerWidth - (rect.right / z)) + 'px'; document.body.appendChild(dropdown); // Clamp into the *visible* area. On mobile (esp. Firefox) window.innerHeight // includes the strip hidden under the dynamic toolbar, so a menu that "fits" diff --git a/static/js/cookbookServe.js b/static/js/cookbookServe.js index c1204460..7c75e1c3 100644 --- a/static/js/cookbookServe.js +++ b/static/js/cookbookServe.js @@ -1078,7 +1078,7 @@ function _rerenderCachedModels() { dropdown.appendChild(cancelDiv); const rect = btn.getBoundingClientRect(); const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; - dropdown.style.cssText = `position:fixed;z-index:${topPortalZ()};visibility:hidden;top:0;right:${(window.innerWidth / z) - rect.right}px;background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,0.3);font-size:12px;`; + dropdown.style.cssText = `position:fixed;z-index:${topPortalZ()};visibility:hidden;top:0;right:${window.innerWidth - (rect.right / z)}px;background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,0.3);font-size:12px;`; document.body.appendChild(dropdown); // Clamp into the VISIBLE area (visualViewport, not innerHeight — they differ // on mobile under the dynamic toolbar). Flip above the button if there's no @@ -2328,10 +2328,11 @@ function _rerenderCachedModels() { // Clamp into the viewport using the menu's real size (both axes); flip // above the toggle if there isn't room below. Right-align to the anchor. const w = dropdown.offsetWidth, h = dropdown.offsetHeight; - let left = Math.min(rect.right - w, window.innerWidth - w - 8); + const za = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; + let left = Math.min((rect.right / za) - w, window.innerWidth - w - 8); left = Math.max(8, left); - let top = rect.bottom + 6; - if (top + h > window.innerHeight - 8) top = Math.max(8, rect.top - 6 - h); + let top = (rect.bottom / za) + 6; + if (top + h > window.innerHeight - 8) top = Math.max(8, (rect.top / za) - 6 - h); dropdown.style.left = `${left}px`; dropdown.style.top = `${top}px`; dropdown.style.visibility = ''; @@ -2433,8 +2434,9 @@ function _rerenderCachedModels() { menu.appendChild(mk('Clear Server', 'cookbook-dropdown-danger', () => _clearBtn?.click())); menu.appendChild(mk('Cancel', 'dropdown-cancel-mobile', () => {})); const r = _launchMoreBtn.getBoundingClientRect(); + const z = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; menu.style.position = 'fixed'; - menu.style.right = (window.innerWidth - r.right) + 'px'; + menu.style.right = (window.innerWidth - (r.right / z)) + 'px'; document.body.appendChild(menu); { const vv = window.visualViewport; @@ -2479,8 +2481,9 @@ function _rerenderCachedModels() { menu.appendChild(mk('Probe GPUs', '', () => _probeBtn?.click())); menu.appendChild(mk('Cancel', 'dropdown-cancel-mobile', () => {})); const r = _splitArrow.getBoundingClientRect(); + const z2 = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; menu.style.position = 'fixed'; - menu.style.right = (window.innerWidth - r.right) + 'px'; + menu.style.right = (window.innerWidth - (r.right / z2)) + 'px'; document.body.appendChild(menu); // Default open BELOW, but if there's no room (esp. on mobile where // the arrow sits near the bottom of the modal) flip ABOVE so the @@ -2587,12 +2590,13 @@ function _rerenderCachedModels() { // popup stays fully visible — GPU buttons near the right edge // of the modal previously anchored the popup mostly off-screen. const r = anchorBtn.getBoundingClientRect(); + const z3 = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; const vw = window.innerWidth || document.documentElement.clientWidth; const vh = window.innerHeight || document.documentElement.clientHeight; const pw = popup.offsetWidth || 320; const ph = popup.offsetHeight || 200; - let left = r.left; - let top = r.bottom + 4; + let left = r.left / z3; + let top = (r.bottom / z3) + 4; // Push left so the popup doesn't overflow the right edge. if (left + pw > vw - 8) left = Math.max(8, vw - pw - 8); // If there isn't room below, render above the button instead. diff --git a/static/js/emojiPicker.js b/static/js/emojiPicker.js index d932d219..3fc63b3b 100644 --- a/static/js/emojiPicker.js +++ b/static/js/emojiPicker.js @@ -157,9 +157,10 @@ function togglePicker(anchor, target) { document.body.appendChild(_pickerEl); const rect = anchor.getBoundingClientRect(); + const ze = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; _pickerEl.style.position = 'fixed'; - _pickerEl.style.top = (rect.bottom + 4) + 'px'; - _pickerEl.style.left = rect.left + 'px'; + _pickerEl.style.top = ((rect.bottom / ze) + 4) + 'px'; + _pickerEl.style.left = (rect.left / ze) + 'px'; _pickerEl.style.zIndex = String(topPortalZ()); requestAnimationFrame(() => { diff --git a/static/js/skills.js b/static/js/skills.js index 84974d44..be12e81d 100644 --- a/static/js/skills.js +++ b/static/js/skills.js @@ -443,13 +443,14 @@ function _openSkillMenu(btn, card, sk, name, isPublished) { // bring-to-front counter climbs past the static value (#4720). menu.style.zIndex = String(topPortalZ()); const r = btn.getBoundingClientRect(); - menu.style.top = (r.bottom + 4) + 'px'; - menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px'; + const zs = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; + menu.style.top = ((r.bottom / zs) + 4) + 'px'; + menu.style.right = Math.max(6, window.innerWidth - (r.right / zs)) + 'px'; // Keep it on-screen (mobile): flip above the button if it would overflow the // bottom, clamp the left edge, and cap the height as a last resort. const mr = menu.getBoundingClientRect(); if (mr.bottom > window.innerHeight - 6) { - menu.style.top = Math.max(6, r.top - mr.height - 4) + 'px'; + menu.style.top = Math.max(6, (r.top / zs) - mr.height - 4) + 'px'; } if (mr.left < 6) { menu.style.right = Math.max(6, window.innerWidth - 6 - mr.width) + 'px'; diff --git a/static/js/theme.js b/static/js/theme.js index 09b13f2b..c575e89b 100644 --- a/static/js/theme.js +++ b/static/js/theme.js @@ -1397,12 +1397,13 @@ function _showThemeZoneHighlight(selector) { if (el.closest && el.closest('#theme-modal')) return; const r = el.getBoundingClientRect(); if (r.width < 2 || r.height < 2) return; + const zt = parseFloat(getComputedStyle(document.documentElement).zoom) || 1; const overlay = document.createElement('div'); overlay.className = 'theme-zone-highlight'; - overlay.style.top = (r.top - 2) + 'px'; - overlay.style.left = (r.left - 2) + 'px'; - overlay.style.width = (r.width + 4) + 'px'; - overlay.style.height = (r.height + 4) + 'px'; + overlay.style.top = ((r.top / zt) - 2) + 'px'; + overlay.style.left = ((r.left / zt) - 2) + 'px'; + overlay.style.width = ((r.width / zt) + 4) + 'px'; + overlay.style.height = ((r.height / zt) + 4) + 'px'; document.body.appendChild(overlay); }); }