Commit Graph

841 Commits

Author SHA1 Message Date
Alex Blank 98b9d612fa fix: container-level capture touch with direct viewport.scrollTop manipulation
- Attach capture-phase touch listeners to .terminal-container (parent of xterm)
- On vertical swipe: e.preventDefault() blocks page scroll, then directly
  adjust .xterm-viewport.scrollTop by the swipe delta
- This bypasses term.scrollLines() API and directly manipulates the DOM
  element that xterm.js watches via its internal scroll handler
- Remove all CSS touch-action overrides — container handles it in JS
2026-05-29 20:23:19 +02:00
Alex Blank 874873541d fix: xterm.js mobile touch scrolling via viewport CSS and stopPropagation
- Add full mobile viewport CSS: overflow-y scroll, -webkit-overflow-scrolling
  touch, overscroll-behavior-y contain, translate3d hardware accel,
  scroll-behavior smooth, touch-action pan-y
- After term.open(), find .xterm-viewport and add passive touch listeners
  that call stopPropagation() (not preventDefault) — this lets the browser
  handle native touch scrolling while preventing xterm.js internal handlers
  from interfering
- Based on xterm.js known issue #5489 and SCROLLING_FIX.md approach
2026-05-29 20:16:12 +02:00
Alex Blank ef9ac76f06 fix: remove all touch interception, let browser scroll xterm viewport natively
- xterm.js has zero touch event handlers (verified: only 1 'touch' ref in
  entire library), so it wasn't intercepting anything
- Our touch-action: none + preventDefault() combo was blocking the browser
  from scrolling the .xterm-viewport natively
- Removed all custom touch event handlers from terminal.tsx
- Removed touch-action: none from .terminal-container
- Added touch-action: pan-y to .xterm-viewport so browser allows vertical pan
- Body scroll lock (terminal-page-open) prevents page from scrolling
2026-05-29 20:08:58 +02:00
Alex Blank ca9db195de fix: document-level capture touch listeners for mobile terminal scroll
- Attach touch listeners to document with capture:true instead of container
- Check if touch target is inside terminal container before handling
- This runs before xterm.js internal handlers, giving us full control
- Add touch-action: none to terminal container to prevent browser gestures
- Lower threshold to 3px, 20px per line for responsive scrolling
2026-05-29 20:03:11 +02:00
Alex Blank c1e16f2163 fix: lock body scroll and re-add programmatic terminal touch scroll
- Add body.terminal-page-open { overflow: hidden } to prevent page scroll
- TerminalPage adds/removes 'terminal-page-open' class on body when mounted
- Re-add capture-phase touch listeners in terminal.tsx with low 3px threshold
- Call e.preventDefault() immediately when vertical gesture is detected,
  before browser compositor commits to page scroll
- Remove CSS touch-action overrides on xterm viewport (now handled in JS)
- Scroll forwarded via term.scrollLines() with 24px per line sensitivity
2026-05-29 19:57:48 +02:00
Alex Blank 61d32fa00f fix: enable native touch scrolling on xterm.js viewport for mobile
- Remove all custom touch event interception code from terminal.tsx
- After term.open(), find the internal .xterm-viewport element and set
  touchAction=pan-y and overscrollBehavior=contain via inline styles
- Add CSS targeting .xterm-viewport on mobile with touch-action: pan-y,
  -webkit-overflow-scrolling: touch, and overflow-y: auto
- Let the browser handle vertical touch panning natively instead of
  trying to intercept and manually forward events
2026-05-29 19:41:23 +02:00
Alex Blank cddb3f8ccf fix: mobile terminal scroll via capture-phase touch listeners
- Attach touch listeners to container wrapper in CAPTURE phase so they run
  before xterm.js internals stop propagation
- Add e.stopPropagation() in touchmove after handling scroll to prevent
  xterm.js from conflicting with our scroll
- Add wheel event fallback for mobile browsers that synthesize wheel from touch
- Remove touch-action: none CSS which was blocking native xterm viewport scroll
2026-05-29 19:35:42 +02:00
Alex Blank 87a938fe58 fix: mobile terminal touch scrolling direction and target
- Attach touch listeners to term.element (xterm root) instead of wrapper
- Fix scroll direction: swipe up now scrolls up (shows older buffer)
- Remove RAF indirection; scroll applied synchronously in touchmove
- Accumulate delta between events for smoother scrolling
- Lower threshold to 6px and px-per-line to 16 for better responsiveness
- Add touch-action: none to terminal container on mobile
2026-05-29 19:29:29 +02:00
Alex Blank 9157694412 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 19:21:35 +02:00
Alex Blank aa34314175 feat: touch swipe scrolling in terminal on mobile
- Intercepts touch events on the terminal container when isMobile=true
- Detects vertical swipe gestures (dominant over horizontal movement)
- Translates swipe distance to xterm.js scrollLines() calls
- Uses requestAnimationFrame for smooth scroll updates
- Threshold of 10px before scroll kicks in; 30px per line
- Touch listeners cleaned up on component unmount
2026-05-29 19:20:52 +02:00
Developer c7fc386d0f Merge branch 'fix/code-server-bind-addr-port' into dev 2026-05-29 16:50:00 +00:00
Developer 6bd814e346 fix(cloudflared): use --bind-addr with port for code-server bind fix
Root cause: _ensure_web_bind_address injected --host 0.0.0.0 for code-server,
which only sets the bind host, not the port. code-server then listens on its
default port (8080) instead of the tool type's default_port (8443). Cloudflared
connects to port 8443 and gets connection refused, resulting in a 502.

Changes:
- _ensure_web_bind_address now accepts default_port and builds
  --bind-addr 0.0.0.0:{port} for code-server
- Same fix for jupyter-notebook with explicit --port flag
- Existing broken --host commands are now detected and replaced
- New migration fixes tool_types templates and instance compose files on disk
- Test fixture updated to use correct --bind-addr 0.0.0.0:8443
2026-05-29 16:49:52 +00:00
alex aa25852091 fix: predictable container names for tunnel connectivity
- Inject explicit container_name into compose files at start/restart time
  via _ensure_container_name_in_compose() to prevent Docker Compose from
  generating UUID-based auto names that break backend network resolution.
- Use instance.name.lower() directly instead of get_container_name() lookups
  which were unreliable with auto-generated names.
- Apply compose sanitization, bind-address fix, and container-name injection
  on restart_instance as well so restarts pick up template fixes.
- Add --force-recreate to docker compose up to ensure container_name changes
  take effect immediately.
- Fix notification lifecycle tests to match current behavior (success severity,
  health_changed event for ownership test).

Quality gates: ruff clean, pytest (7 notification lifecycle tests passed)
2026-05-29 17:51:34 +02:00
Alex Blank c2740cd282 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 17:40:57 +02:00
Developer 23875bb3cc Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 15:40:36 +00:00
Developer ee1eab8408 Merge branch 'feat/agents-english-rule' into dev 2026-05-29 15:40:16 +00:00
Developer 2254ba7496 docs: add english language rule to AGENTS.md
- Require all agent output, comments, commits, docs, and artifacts to be in English unless explicitly requested otherwise
2026-05-29 15:40:11 +00:00
Alex Blank 4866ad08b1 feat: request screen wake lock while terminal page is open
- Uses navigator.wakeLock.request('screen') to keep device awake
- Re-acquires wake lock when tab becomes visible again
- Releases wake lock on component unmount
- Silently ignored on unsupported browsers or if denied
2026-05-29 17:39:57 +02:00
Alex Blank 97ebc19313 fix: restore special keys bar on mobile terminal
- Add SpecialKeysStrip and SpecialKeysPanel to mobile terminal page
- Store sendData and focusInput refs via onTerminalReady callback
- Pass activeModifier/onModifierChange to TerminalComponent on mobile
- Add virtual keyboard padding to prevent keyboard from covering terminal
- Special keys bar sits at bottom of viewport, panel opens as overlay
2026-05-29 17:36:08 +02:00
Alex Blank 946ac6f66a fix: remove mobile terminal pull handle, tap terminal to toggle overlay 2026-05-29 17:29:14 +02:00
Alex Blank 90ddee14c2 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 17:22:07 +02:00
Alex Blank f17f8ae8c8 Merge branch 'fix/mobile-terminal-overlay' into dev 2026-05-29 17:20:22 +02:00
Alex Blank d713bfc5f9 fix: mobile terminal overlay status bar with auto-hide
- Replace inline header+tabs layout with position:absolute overlay
- Overlay contains: back button, session name, status dot, A-/A+ font size, exit
- Session tabs live inside the overlay below the toolbar
- Auto-hides after 3s; clicking terminal content hides it immediately
- Pull handle at top edge appears when overlay is hidden to restore it
- Terminal content always fills full viewport; overlay never resizes container
- Pass showControls=false to TerminalComponent on mobile to avoid double headers
2026-05-29 17:20:15 +02:00
alex 27fe8c24ec merge: keep fixed migration with correct compose_path column 2026-05-29 17:18:17 +02:00
alex eef1e4e8c6 fix(cloudflared): remove command override for LSIO images
Problem: linuxserver/code-server already binds to 0.0.0.0 by default.
Adding any command: override (--bind-addr or --host) breaks the LSIO
s6 init system with 'not found' errors.

Changes:
- _ensure_web_bind_address(): Skip LSIO images entirely (no command
  override needed). If an existing override is found, remove it.
- New migration 2026_05_29_remove_lsio_command_override: Removes
  --bind-addr and --host command overrides from both DB templates
  and existing instance compose files on disk for LSIO images.
- Fixed migration to use correct column name (compose_path) and
  check information_schema for column existence defensively.

Quality gates: ruff clean
2026-05-29 17:17:12 +02:00
alex a7a5905874 fix(cloudflared): remove command override for LSIO images
Problem: linuxserver/code-server already binds to 0.0.0.0 by default.
Adding any command: override (--bind-addr or --host) breaks the LSIO
s6 init system with 'not found' errors.

Changes:
- _ensure_web_bind_address(): Skip LSIO images entirely (no command
  override needed). If an existing override is found, remove it.
- New migration 2026_05_29_remove_lsio_command_override: Removes
  --bind-addr and --host command overrides from both DB templates
  and existing instance compose files on disk for LSIO images.

Quality gates: ruff clean
2026-05-29 17:09:43 +02:00
alex 021537de56 fix(cloudflared): replace broken --bind-addr at runtime + new migration
Problem: The first migration already ran on the user's server with
--bind-addr (broken). Alembic won't re-run the fixed migration.

Changes:
- _ensure_web_bind_address(): Now detects existing --bind-addr commands
  and replaces them with --host 0.0.0.0 instead of skipping
- New migration 2026_05_29_fix_code_server_bind_addr: Finds code-server
  tool types with --bind-addr in compose_template and replaces with
  --host 0.0.0.0

Quality gates: pytest 42 passed (2 pre-existing unrelated failures)
2026-05-29 17:02:26 +02:00
Alex Blank fdfd75790d Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 16:56:14 +02:00
Alex Blank 3d1f8d9cf7 fix: reorder notification DELETE routes so bulk clear matches first
FastAPI matches routes in declaration order. The DELETE /notifications
endpoint (bulk clear) was registered AFTER DELETE /notifications/{id},
so the path parameter route intercepted all requests to the bulk route,
causing a 422 UUID validation error instead of hitting clear_all.

Moved clear_all_notifications above dismiss_notification in the router.
Added regression test to verify route order.

Quality gates: pytest (22 passed)
2026-05-29 16:54:01 +02:00
alex eec37ab710 fix: treat empty config_profile_id as no selection
Frontend was sending empty string for config_profile_id when no profile
was selected, causing 'not compatible' validation error. Backend now
treats any falsy value (None, empty string) as 'no profile selected'.
2026-05-29 16:50:07 +02:00
Alex Blank 5f499ec1b0 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 16:48:24 +02:00
Alex Blank 2b5223097f feat: filter notifications to warnings/errors/ready only and add clear-all button
Notification filtering:
- lifecycle_hooks.py: only instance.error and instance.health_changed
  with status=running generate notifications. All other lifecycle events
  (created, started, stopped, restarted, deleted) are filtered out.
- health_monitor.py: only error and unhealthy states generate notifications.
  Running/recovered state no longer creates info notifications.
- _derive_title now maps instance.health_changed to "Container ready".

Clear-all button:
- Added dismiss_all() to NotificationService
- Added DELETE /notifications endpoint for bulk dismiss
- Frontend: clearAllNotifications API, clearAll in notification context,
  "Clear all" button in notification drawer alongside "Mark all as read"
- Added CSS for .notification-clear-all with danger hover state
- Updated notification-center tests

Quality gates: pytest (21 passed), vitest (11 passed)
2026-05-29 16:42:31 +02:00
alex 1efbc289ba fix(cloudflared): use --host 0.0.0.0 instead of --bind-addr for code-server
The --bind-addr flag caused code-server to fail entirely (app not
responding on any interface). The correct override for the
coder/code-server image is --host 0.0.0.0, which overrides the
entrypoint's --host 127.0.0.1.

Changes:
- Migration: Replace --bind-addr with --host 0.0.0.0, also handle
  existing broken templates by detecting --bind-addr and replacing it
- Runtime safety net: _ensure_web_bind_address uses --host 0.0.0.0
- Test fixture: Updated compose template to match

Quality gates: pytest 42 passed
2026-05-29 16:36:09 +02:00
alex 3c57c8b78b fix(cloudflared): code-server binds to 127.0.0.1 causing tunnel app error 0
Root cause: code-server (and similar web tools) default to binding to
127.0.0.1 (localhost) inside their containers. This makes them unreachable
from the Docker network and from cloudflared, which connects via the
container's Docker network name.

Changes:
- Migration: Update code-server compose_template to include
  --bind-addr 0.0.0.0:8443 command override
- Migration: Update jupyter-notebook compose_template to include
  --ip=0.0.0.0 flag
- Runtime safety net: _ensure_web_bind_address() auto-injects bind
  address for known web tools (code-server, jupyter-notebook) when
  compose doesn't already specify a command
- Diagnostics: _check_app_binding() compares internal vs external
  connectivity to detect 127.0.0.1 binding issues
- Improved readiness check: 30s timeout, checks HTTP status codes,
  logs curl stderr for debugging

Files:
- apps/api/alembic/versions/2026_05_29_fix_web_tool_bind_address.py
- apps/api/src/services/docker.py
- apps/api/src/api/tool_instances.py
- apps/api/tests/integration/test_tool_types_api_extended.py

Quality gates: pytest 42 passed (5 pre-existing unrelated failures)
2026-05-29 16:19:28 +02:00
Alex Blank 9c4500f9cb Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 16:15:37 +02:00
Alex Blank 1e2c5a68cf fix: pass SSH keys and config profile to startInstance in create form
The create-session-form was calling startInstance() without passing the
selected config profile and SSH keys. This caused the backend to receive
ssh_key_ids=[] and clear the keys that were stored during createInstance.
The .ssh directory was never mounted because instance.ssh_key_ids was
wiped during the start call.

Also includes minor formatting cleanup on the data migration.

Quality gates: pytest (18 passed)
2026-05-29 16:14:40 +02:00
alex dc6991e6ef fix(cloudflared): add app binding diagnostics and improve readiness check
- Add _check_app_binding() to detect if app is bound to 127.0.0.1
  instead of 0.0.0.0 (common cause of tunnel 'app error 0')
- Improve curl readiness check: wait up to 30s, check HTTP status codes
  (accept 2xx, 3xx, 401, 403 as 'ready')
- Log curl stderr for connection debugging
- Log binding diagnosis when external connectivity fails

Quality gates: pytest 42 passed
2026-05-29 15:45:56 +02:00
alex cdf233378c revert: cloudflared tunnel localhost fix — wrong diagnosis
The API container and tool instances share the 'backend' Docker network
(connect_container_to_network at tool_instances.py:1576). cloudflared
runs INSIDE the api container, so localhost:host_port is unreachable.

The original container_name:internal_port is correct for networking.
The 'app error 0' is an application-level issue, not networking.

This reverts commit a8fbca9.
2026-05-29 15:42:40 +02:00
Alex Blank 23769e6ad4 fix: remove redundant ssh_keys mount from pi-agent manifest via data migration
The ssh_keys mount was already removed from the Alembic seed migration, but
that migration had already been applied to the DB. This data migration
removes the mount from the actual tool_definition_manifests row so that
instance-level SSH key mounting handles keys exclusively.

Quality gates: pytest (18 passed)
2026-05-29 15:39:30 +02:00
Alex Blank 9f8058223a Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 15:32:38 +02:00
Alex Blank b483a34517 fix: skip read-only mounts in permission fixer and remove redundant ssh_keys manifest mount
- apply_mount_permissions now skips mounts with readonly=true to avoid
  'Read-only file system' warnings on post-start chown/chmod
- Removed the ssh_keys mount from the pi-agent manifest definition;
  instance-level SSH key mounting now handles this exclusively
- Added unit test for read-only mount skipping

Quality gates: pytest (15 passed)
2026-05-29 15:32:00 +02:00
alex a8fbca9ef5 fix: cloudflare tunnel connects to localhost:host_port instead of container_name:container_port
Root cause: start_cloudflared_tunnel was trying to connect to
http://{container_name}:{container_port}, but:
1. The host OS cannot resolve Docker container names
2. cloudflared runs on the host, so it needs the host-mapped port

Changes:
- start_cloudflared_tunnel: changed signature to accept host_port only
- Connects cloudflared to localhost:{host_port} via Docker port mapping
- Connectivity check uses localhost:{host_port}
- recreate_tunnel updated to match new signature
- Callers in tool_instances.py pass instance.port (host port)

Quality gates: pytest 42 passed
2026-05-29 15:19:32 +02:00
Alex Blank de8c47c81c fix: deep-merge manifest with base to resolve container user UID/GID
- start_instance now deep-merges manifest with base definition before extracting user.uid/user.gid
- The user config is typically defined in the base image (ubuntu-24.04-dev), not the extending manifest
- Add debug logging to verify resolved uid/gid/home_dir
- Add logging to prepare_ssh_key_files for chown success/failure visibility
- Log current process uid when chown fails to diagnose permission issues

Quality gates: pytest 239 passed (6 pre-existing failures), tsc --noEmit clean
2026-05-29 14:49:24 +02:00
Alex Blank b11089896a fix: prepare SSH keys with container UID/GID on host before mounting
- Extend prepare_ssh_key_files() with optional uid/gid parameters
- Call os.chown on created files when uid/gid are provided
- Gracefully handle PermissionError if API process is not root
- In start_instance, extract container user UID/GID from manifest
- Pass container UID/GID when preparing instance-level SSH key mounts
- Legacy clone-mode SSH keys continue to use root (0,0)
- Add unit tests for prepare_ssh_key_files ownership logic
- Keep apply_ssh_permissions() as fallback for cases where host chown fails

Quality gates: pytest 239 passed (6 pre-existing failures), tsc --noEmit clean
2026-05-29 14:38:15 +02:00
Alex Blank 16549709e2 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 14:25:22 +02:00
Alex Blank 68977b73be fix: add detailed SSH permission fix logging for debugging
- Replace apply_ssh_permissions internals with _exec_and_log for full visibility
- Log every docker exec command, stdout, and stderr at DEBUG level
- After chown/chmod, run ls -la and stat to verify final state
- Log verified state at INFO level so users can see exactly what happened
- Update tests to mock subprocess.run instead of _run_in_container

Quality gates: pytest 236 passed (6 pre-existing), tsc --noEmit clean
2026-05-29 14:24:37 +02:00
alex 3da2bc93cb fix: skip intermediate 'starting' notifications, only notify on failed/successful attempts
- lifecycle_hooks.publish_lifecycle_event now skips notification creation
  when event_type='instance.started' and status='starting'
- Users only see notifications for terminal states:
  - Failed: instance.error
  - Successful: instance.health_changed with status='running'
- Updated integration tests to verify new behavior:
  - test_lifecycle_started_intermediate_skips_notification
  - test_lifecycle_running_creates_notification

Quality gates: pytest 42 passed, ruff clean
2026-05-29 14:05:56 +02:00
Alex Blank d9632a3412 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 14:05:08 +02:00
Alex Blank 03d22c4d06 fix: set SSH key ownership to container user with mode 600
- Mount SSH keys as bind (not ro) so docker exec --user root can chown
- Add apply_ssh_permissions() to permission_fixer.py
- Call apply_ssh_permissions() after container start for all instance types
- Derive container user from home_dir (/root → root, /home/user → user)
- Tests: apply_ssh_permissions unit tests + start_instance integration tests

Quality gates: pytest 236 passed (6 pre-existing failures), tsc --noEmit clean
2026-05-29 14:04:52 +02:00
alex 19242b4152 feat: notification center toast coordination (PR-4)
- EventToastBridge checks notification_toast_level and notification_mute_categories
- toast-rules.ts: event-to-category/severity mapping functions
- Settings page: notification preferences section (toast level dropdown, mute checkboxes)
- Settings API types extended with notification preference fields
- 17 frontend tests (toast-rules + bridge)
- Preference hierarchy: mute categories → toast level → show/hide

Quality gates: vitest 17 passed, tsc clean, eslint clean
2026-05-29 13:52:49 +02:00