Commit Graph

594 Commits

Author SHA1 Message Date
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
Alex Blank ceaed9af66 Merge remote dev branch 2026-05-29 13:34:08 +02:00
Alex Blank e9364fa70f feat: instance-level SSH key selection for container mounting
- Revert mistaken ssh_key_id from ConfigProfile (model, API, resolver, frontend)
- Add ssh_key_ids JSON column to tool_instances via migration
- Update create_instance to accept and store ssh_key_ids
- Update start_instance to mount selected SSH keys to {home_dir}/.ssh
- Update list_instances to return ssh_key_ids
- Frontend CreateSessionForm: multi-select SSH key checkboxes
- Frontend instance-list: SSH key selector for start/restart actions
- Maintain separate SSH key dirs per key to avoid conflicts

Quality gates: pytest (231 passed, 6 pre-existing), tsc --noEmit clean
2026-05-29 13:30:53 +02:00
alex 2bec205a30 feat: notification center frontend core (PR-3)
- Bell icon in icon registry (Phosphor Bell)
- NotificationProvider context with polling (15s unread / 30s list)
- useNotifications() hook with optimistic updates
- NotificationCenter component: bell + badge + dropdown panel
- NotificationItem component: severity icon, title, relative time, actions
- AppShell integration: mount in header-actions, hidden on mobile
- CSS styles: dropdown, items, unread/read states, empty state
- formatRelativeTime utility (custom, no new deps)
- 25 frontend tests (9 hook + 6 item + 10 center)

Quality gates: vitest 25 passed, tsc clean, eslint clean
2026-05-29 13:17:22 +02:00
Alex Blank cbd3436ff7 Merge remote dev branch 2026-05-29 12:55:03 +02:00
Alex Blank 57ff236f2d feat: add ssh_key_id to config profiles for container key mounting
- Add ssh_key_id column to ConfigProfile model and migration
- Update config profile API to accept/return ssh_key_id
- Include ssh_key_id in ResolvedProfile and resolver logic
- Mount selected SSH key into container home dir at start_instance
- Frontend config profile form with SSH key selector dropdown
- Git mount URL validation defaults to profile's SSH key

Quality gates: pytest (231 passed, 6 pre-existing), tsc --noEmit clean
2026-05-29 12:53:51 +02:00
alex 6085859874 feat: notification center backend integration (PR-2)
- Wire lifecycle_hooks.py to NotificationService after event bus publish
- Wire health_monitor.py to NotificationService after state changes
- Category/severity mapping: instance.* → info, error → error, unhealthy → warning
- Extend UserConfig API with notification_mute_categories and notification_toast_level
- 6 integration tests for event-to-notification flow
- All producer calls wrapped in try/except — failures logged, pipeline continues

Quality gates: pytest 41 passed (monitoring + lifecycle), ruff clean
2026-05-29 12:40:15 +02:00
Alex Blank d413fb84a5 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 12:16:40 +02:00
Alex Blank c22b047b8c merge: git mount URL validation with branch detection 2026-05-29 12:15:41 +02:00
Alex Blank 090edf7ef6 feat: git mount URL validation with branch detection
- Add POST /config-profiles/validate-git-url endpoint:
  - Parses URL using existing parse_git_url utility
  - Suggests corrected URL for browser URLs
  - Runs git ls-remote --heads to verify reachability
  - Lists available branches from remote
  - Supports SSH key for private repos
  - Returns structured response: valid, suggested_url, branches,
    default_branch, error, error_code

- Update frontend GitMountEditor:
  - Add Check button next to URL field with loading state
  - Show validation result: valid (green), suggestion (yellow),
    invalid (red)
  - Suggestion includes Use this button to apply corrected URL
  - Branch field becomes dropdown when URL is validated,
    populated with remote branches
  - Mappings section disabled until URL is validated
  - Shows hint: Validate the URL first

- Quality gates: pytest (218 passed, 6 pre-existing),
  tsc --noEmit (clean)
2026-05-29 12:15:30 +02:00
alex cbaebcf649 feat: notification center backend core (PR-1)
- Add notifications table with Alembic migration
- Notification model with user-scoped indexing and partial index on unread
- NotificationService singleton with create/list/count/mark-read/dismiss
- FastAPI router: GET /notifications, GET /unread, PATCH /{id}/read,
  POST /mark-all-read, DELETE /{id}
- Mute categories filtering from UserConfig
- 13 unit tests for NotificationService
- 10 integration tests for API endpoints
- Updated test_models.py with new table registration

Quality gates: pytest 23 new passed, ruff clean
2026-05-29 12:09:14 +02:00
Alex Blank 4a0d38384f merge: file-level mount overlays for ResolvedMount 2026-05-29 11:59:01 +02:00
Alex Blank ea006b68c2 fix: mount config profile files individually instead of replacing directories
The previous sorting fix exposed a deeper bug: ResolvedMount always
mounted its staging directory as a single bind mount. When a config
profile mount targeted /workspace/x/y and contained a single file
z.json, the staging directory (containing only z.json) replaced the
ENTIRE /workspace/x/y directory, hiding all sibling files from git
repo mounts.

- Change apply_resolved_profile to mount each file individually:
  - source: staging_dir/relative_path
  - target: expanded_target/relative_path
  - Sibling files from other mounts are preserved.
  - Empty mounts produce no volume entries.

- Keep volume sorting (parent paths before child paths) which is
  still necessary for directory mounts and ensures parent dirs exist
  before file mounts inside them.

- Add 4 unit tests for file-level mount behavior.

Quality gates: pytest (218 passed, 6 pre-existing), tsc --noEmit (clean)
2026-05-29 11:58:50 +02:00
Alex Blank 202533fbb1 merge: sort mounts by specificity 2026-05-29 11:35:36 +02:00
Alex Blank 0952aa8217 fix: sort mount volumes by specificity to prevent parent mounts hiding children
When git repo mounts and regular file mounts have overlapping target
paths, broader parent mounts hide deeper child mounts because Docker
Compose applies volumes in array order.

- Add sort_volumes_by_specificity() to docker.py:
  - Sorts by target path depth (parent paths first, child paths last)
  - Logs warnings for duplicate targets
  - Handles :bind and :ro suffixes correctly

- Integrate into manifest flow (compile_compose):
  - Sorts manifest mounts + EXTRA_VOLUMES before writing compose

- Integrate into legacy flow (_modify_compose_file):
  - Sorts after appending extra_volumes to existing template volumes

- Add 6 unit tests covering parent/child ordering, stable sort,
  type suffixes, empty list, single volume, and duplicate warnings.

Quality gates: pytest (214 passed, 6 pre-existing), tsc --noEmit (clean)
2026-05-29 11:35:27 +02:00
Alex Blank 787e8844bc merge: exit fullscreen on click outside, free Escape key 2026-05-29 11:11:11 +02:00
Alex Blank fe98f966d6 fix: allow Escape in terminal, exit fullscreen on click outside
- Remove global Escape key listener that intercepted Escape before
  xterm.js could receive it, breaking vim/tmux/etc.
- Add click-outside-to-exit for fullscreen: clicking on the padding
  area around .terminal-page-content or .terminal-fullscreen-header
  exits fullscreen. Clicks inside content or header are ignored.
- Add 8px padding/gap to .terminal-page.fullscreen to create a
  clickable border area around the terminal.
- Keep Exit button and Alt+Shift+F as explicit exit methods.

Quality gates: tsc --noEmit (clean), pytest (208 passed, 6 pre-existing)
2026-05-29 11:11:03 +02:00
Alex Blank 79ad3b0715 merge: fix terminal fullscreen cumulative shrinkage 2026-05-29 10:53:18 +02:00
Alex Blank f728011b2a fix: prevent cumulative terminal shrinkage in fullscreen mode
When switching terminal sessions in fullscreen mode, the viewport
shrank cumulatively because .terminal-wrapper uses
grid-template-rows: auto 1fr. With showControls=false, the single
child (.terminal-container) landed in the auto track instead of 1fr,
creating a feedback loop with xterm fit().

- Add .terminal-wrapper.no-controls with grid-template-rows: 1fr
  so the container fills the wrapper when the header is hidden.
- Apply no-controls class in TerminalComponent when showControls=false.
- Replace setTimeout(50) with double requestAnimationFrame in
  TerminalPage for more reliable fit() timing after tab switches.

Quality gates: tsc --noEmit (clean), pytest (208 passed, 6 pre-existing)
2026-05-29 10:53:02 +02:00
Alex Blank 569876538a Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-29 10:35:47 +02:00
Alex Blank d2b1c132d1 Merge fix/terminal-fullscreen-unified-header into dev 2026-05-29 10:34:44 +02:00
Alex Blank 8926152fca fix: unified fullscreen terminal header
- Add showControls prop to TerminalComponent to optionally hide internal header
- Add reset() method to TerminalRef for external reset control
- TerminalPage now renders a unified fullscreen header bar combining:
  - Session tabs (TerminalSessionTabs)
  - Terminal controls (status dot, A-, A+, Reset, Exit Fullscreen)
- Unified header is always visible in fullscreen (no hover-to-reveal)
- TerminalComponent internal header hidden when in fullscreen mode
- Remove old CSS that hid session tabs with opacity:0 until hover

Quality gates: pytest 188 passed, frontend typecheck clean

Fixes: terminal-fullscreen-unified-header
2026-05-29 10:34:31 +02:00
alex 2682e0268c feat: container monitoring integration + polish (PR-3)
- Integration tests: SSE auth, connection limits, lifecycle hooks, event persistence (6 tests)
- Instance events history API: GET /instances/{id}/events
- Documentation updates: terminal.md, backend.md, frontend.md
- Performance: SSE max 5 connections, health monitor write-on-change

Quality gates: pytest 21 monitoring passed, 172 unit passed (4 pre-existing), vitest 14 passed, tsc clean, eslint clean, ruff clean
2026-05-29 10:25:00 +02:00
alex f13a63dc2f feat: container monitoring frontend UI (PR-2)
- Custom ToastContext + ToastProvider + ToastContainer (~170 lines, no deps)
- useEvents() SSE hook with exponential backoff reconnect
- EventProvider context for app-wide SSE stream sharing
- Event-to-toast bridge with severity mapping and deduplication
- Real-time status badge updates replacing 30s polling
- EventSource auth probe (401/429 detection via fetch)
- 14 frontend tests (useEvents + toast-rules)

Quality gates: vitest 14 passed, tsc clean, eslint clean
2026-05-29 10:25:00 +02:00
alex 4a7f24348c feat: container monitoring backend core (PR-1)
- Add instance_events and health_checks tables with Alembic migration
- InstanceEventBus: typed pub/sub singleton with wildcard support
- HealthMonitor: async background loop polling containers every 15s
- SSE endpoint GET /events/stream with auth and connection limits
- Lifecycle hooks in tool_instances.py (create/start/stop/restart/delete)
- Structured JSON logging with correlation IDs
- 15 new unit tests (EventBus, HealthMonitor, MonitoringModels)

Quality gates: pytest 15 new passed, ruff clean
2026-05-29 10:25:00 +02:00
Alex Blank 0fdbef578f Merge feat/home-path-expansion into dev 2026-05-29 00:01:13 +02:00
Alex Blank 29a12bb102 feat: expand ~ and $HOME in mount target paths
- Add expand_container_path() helper that resolves ~/ and $HOME/ prefixes
- Add get_manifest_home_dir() to compute /home/{user.name} or /root from manifest
- Set ENV HOME=... and ENV USER=... in generated Dockerfile for runtime compatibility
- Pass home_dir through instance creation and startup pipeline
- Expand mount targets in apply_resolved_profile() for regular profile mounts
- Expand mapping targets in _resolve_git_mount_mappings() for git mounts
- Expand working_directory and volume targets in _modify_compose_file()
- Update _prepare_manifest_instance to return home_dir alongside image tag
- Fetch tool_type early in start_instance to determine home_dir before profile application

Quality gates: pytest 188 passed, frontend typecheck clean

Addresses: home-path-expansion
2026-05-29 00:01:04 +02:00
Alex Blank 270764ff0f Merge feat/config-profile-multi-repo-mounts into dev 2026-05-28 23:35:29 +02:00
Alex Blank 0e6521e433 feat: config profile multi-repo mounts
- Add mappings array support to git_mount entries
- Clone repository once per git_mount entry, mount multiple subdirectories
- Normalize legacy source_path+target_path to mappings on read
- Update _merge_git_mounts to dedup by (remote_url, branch) and concatenate mappings
- Add _normalize_git_mount, _clone_git_repo, _resolve_git_mount_mappings helpers
- Update GitMountItem Pydantic model with GitMountMapping and model_validator
- Update frontend GitMountEditor component with mappings UI
- Auto-convert legacy git mount entries to mappings format on load
- Add 15 backend unit tests for normalization, resolution, and glob expansion
- Update existing config profile resolver tests for new merge behavior

Quality gates: pytest 167 passed, frontend typecheck clean

Addresses: config-profile-multi-repo-mounts
2026-05-28 23:35:22 +02:00
Alex Blank e20d94d6ba chore: remove unnecessary debug logging
Frontend:
- Remove 4 console.log statements from terminal.tsx that flooded the
  browser console with WebSocket traffic (open, received X bytes, sending Y,
  xterm focused)

Backend:
- Downgrade Dockerfile/entrypoint compilation logs from INFO to DEBUG in
  _prepare_manifest_instance
- Remove hex-dump diagnostic logging from docker_build.py (was for
  troubleshooting the backslash continuation bug, now fixed)
- Downgrade Dockerfile write log from INFO to DEBUG
2026-05-28 23:05:48 +02:00
Alex Blank f4802ece4d fix: build manifest image during create_instance instead of start_instance
The manifest-based flow was building the Docker image inside start_instance,
which made the start HTTP request take 3-5 minutes (downloading ubuntu:24.04,
apt-get update, installing packages, Node.js, npm packages). The frontend
showed a spinner forever because the HTTP request was still pending.

Move the image build to create_instance (same pattern as dockerfile types):
1. create_instance now compiles Dockerfile + entrypoint and builds the image
2. start_instance sees the image already exists and skips the build
3. Start is fast — just docker compose up + health checks

This matches the UX expectation: creation has a spinner (can be slow),
start should be quick.
2026-05-28 22:51:15 +02:00
Alex Blank 9800e37cd6 fix: use single backslash for Dockerfile line continuations
The compile_dockerfile function used \\\\ in Python string literals,
which produces \ (two backslashes) in the Dockerfile output. Docker's
legacy builder requires a single backslash \ for line continuation.

This caused 'unknown instruction: curl' because Docker saw the first as the continuation and the second \ as a literal character before the
newline, breaking the RUN command parsing.

Fix: change all \\ to \ in Python string literals within
compile_dockerfile, producing the correct single-backslash continuation.

Verified with hex dump from container logs:
- Before: line ended with 5c5c (two backslashes)
- After: line ends with 5c (one backslash)
2026-05-28 22:35:47 +02:00
Alex Blank 84f30b07c4 fix: normalise CRLF to LF in Docker build files
Docker's legacy builder treats \r as a literal character after a backslash
continuation, breaking RUN multi-line commands and producing
'unknown instruction: curl' errors.

Add defensive CRLF→LF normalisation for both Dockerfile and build context
files before writing. Also log hex representation of first 8 lines so we
can verify exactly what bytes Docker receives.
2026-05-28 22:22:05 +02:00
Alex Blank fba5e7c7be fix: add Dockerfile logging and force unix line endings for Docker builds
The container build fails with 'unknown instruction: curl' on line 6, which
suggests the Dockerfile continuation characters or line endings may be
malformed. Add defensive logging to diagnose:

- Force newline='\n' in all write_text calls in build_image for consistent
  Unix line endings regardless of platform
- Log compiled Dockerfile and entrypoint content at INFO/DEBUG level
- Log Dockerfile byte count when written

This will let us see exactly what Docker is receiving in the next build attempt.
2026-05-28 22:11:32 +02:00
Alex Blank 1e7bd0a540 fix: handle manifest definition type in create_instance
create_instance had an if/else where the else branch handled both compose
and manifest types. For manifest types, compose_template is NULL (migrated
tools no longer store raw compose strings), so render_compose_template(None,...)
crashed with 'NoneType' object has no attribute 'replace'.

Add an explicit elif tool_type.definition_type == 'manifest' branch that:
1. Looks up the ToolDefinitionManifest from tool_type.manifest_id
2. Resolves base definition if referenced
3. Computes deterministic image tag
4. Generates compose via compile_compose

Legacy compose types continue to use render_compose_template in the else branch.
2026-05-28 21:53:17 +02:00
Alex Blank 0a0af4e02a fix: stop manifest editor base image selection loop
The ManifestEditor had a feedback loop:
1. State change → buildManifest changes → onChange notifies parent
2. Parent updates manifestData → new manifest prop
3. Loading effect sets all state from manifest (arrays get new refs even if same content)
4. New array refs → buildManifest changes → onChange fires again → loop

Fix: track the last-sent manifest via a ref and only call onChange when the
serialized built manifest actually differs. This breaks the cycle because after
the loading effect syncs state, the rebuilt manifest is identical in content
so we skip the parent notification.
2026-05-28 21:44:49 +02:00
Alex Blank 3aa56dcfc3 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-28 21:36:04 +02:00
Alex Blank 7e3c701ea6 fix: support manifest-type tool definitions in Tool Workshop
Backend:
- Allow 'manifest' in tool_types definition_type validators
- Add manifest_id to ToolTypeCreate, ToolTypeUpdate, ToolTypeResponse
- Skip compose/dockerfile template validation when definition_type is manifest
- Require manifest_id when definition_type is manifest
- Clear legacy templates when switching to manifest type

Frontend:
- Load manifest data via getToolDefinition when selecting a manifest-type tool
- Create/update manifest definition via tool-definitions API when saving
- Pass manifest_id to tool-types create/update API
- Fix unused EmptyState import after configs/folders cleanup
2026-05-28 21:34:25 +02:00
alex e672bdde54 Merge remote-tracking branch 'origin/dev' into dev 2026-05-28 21:19:41 +02:00
alex c7c4cb45a7 fix(terminal): verify container exists before creating terminal session
The instance status may say 'running' but the actual Docker container
may have been removed (e.g. docker prune, host restart). The old code
created a terminal session which immediately died because docker exec
failed with 'No such container'.

- Add get_container_status check in WebSocket handler before session creation
- Return 4004 with clear message if container is missing
- This prevents spawning zombie terminal sessions
2026-05-28 21:18:47 +02:00
Alex Blank 6e4275a510 fix: resolve Alembic multiple heads
The terminal_sessions migration and drop_tool_configs migration both pointed
to add_tool_definition_manifests as their down_revision, creating two heads.
Update drop migration to depend on terminal_sessions instead, restoring a
single linear chain.
2026-05-28 20:40:40 +02:00
Alex Blank 3ef60be623 Merge branch 'dev' of ssh://git.commumedia.org:2222/alex/headquarter into dev 2026-05-28 20:36:39 +02:00
alex a3d01dd0a5 fix(terminal): loading state, focus handling, debug logging
- Initialize loading=true in useTerminalSessions to prevent auto-create
  from firing before initial load completes
- Remove hasAutoCreated ref from TerminalPage (no longer needed)
- Add focus() to TerminalRef, call on tab switch
- Add term.focus() after term.open() in TerminalComponent
- Add console logging for WebSocket send/receive to debug no-i/o
- Revert backend _read_loop retry logic to original break-on-error
2026-05-28 20:14:54 +02:00
Alex Blank 9bd5fc5c68 refactor: remove Tool Configs and Config Folders
These features are fully superseded by Config Profiles which provide:
- Env vars, file mounts, port overrides, start commands, working dirs
- Git mounts, profile composition, cycle detection
- Default selection, project/tool-type scoping

Changes:
- Delete backend models: ToolConfig, ConfigFolder
- Delete backend APIs: tool_configs.py, config_folders.py
- Delete frontend API clients: tool_configs.ts, config_folders.ts
- Remove Tool Config fetching from start_instance, use ConfigProfile only
- Simplify merge_with_config to accept only profile (no tool_configs)
- Remove configs/folders tabs from Tool Workshop page
- Delete associated integration and unit tests
- Add Alembic migration to drop tool_configs and config_folders tables

Quality gates: backend tests 59 passed, frontend typecheck clean
2026-05-28 20:08:48 +02:00
alex b6e71e32f5 fix(terminal): prevent double session creation, restore sessions on reload
Three fixes for multi-session terminal bugs:

1. Race-condition double creation: The auto-create effect fired twice because
   loadSessions returned 0 while an earlier createSession was still in flight.
   Added hasAutoCreated guard ref to ensure only one auto-create happens.

2. Page reload spawns new sessions: After server restart, list_terminal_sessions
   filtered out DB-only sessions (no in-memory counterpart), so the frontend
   thought no sessions existed and auto-created new ones. Reverted the filter
   so DB rows are always returned. The WebSocket handler now restores the
   in-memory session from the DB row on demand when connecting.

3. No input after connection: The backend _read_loop would break on any send
   error, causing asyncio.wait to cancel the _write_loop. Made _read_loop
   retry up to 3 times before giving up, preventing transient send errors
   from killing input handling.

Quality gates: pytest (15/15 passed), tsc clean
2026-05-28 19:45:59 +02:00
alex 9ccaae04db Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	apps/api/.pi-lens/cache/review-graph.json
#	apps/web/.pi-lens/cache/review-graph.json
2026-05-28 19:14:56 +02:00
alex 9f90624aa6 fix(terminal): prevent xterm.js crash, websocket disconnect cascade, stale sessions
Three related bugs fixed:

1. Frontend xterm.js crash: TerminalPage rendered ALL sessions with display:none
   for inactive ones. xterm.js crashes when initialized in a hidden container
   (Viewport can't read dimensions). Fix: only render the active session's
   TerminalComponent using conditional rendering.

2. Backend websocket disconnect cascade: When client disconnected (due to #1),
   the server tried to send 'connected' status on dead socket, caught the
   WebSocketDisconnect in a generic except block, then tried to close() again
   causing RuntimeError. Fix: catch WebSocketDisconnect specifically and suppress
   close() errors.

3. Stale DB sessions: After server restart, DB still had old terminal session
   rows but no in-memory sessions. list_terminal_sessions returned these ghosts,
   causing the frontend to render dead tabs. Fix: skip DB-only sessions that
   have no live in-memory counterpart.

Quality gates: pytest (15/15 passed), tsc clean, vitest (7/7 passed)
2026-05-28 19:10:22 +02:00
Alex Blank 62c1fb3836 Merge branch 'feat/tool-definition-manifest' into dev
Conflicts resolved:
- models/__init__.py: kept both TerminalSessionModel (from dev) and
  ToolDefinitionManifest (from feature branch)
- alembic migration: kept full migration (already applied to DB)
- openspec/config.yaml: kept full config with SDD settings
2026-05-28 15:49:56 +02:00