Three-panel layout with 240px sidebar, tabbed session area, and
status bar. Sidebar has Connections/SFTP toggle, search input, and
collapsible group tree with protocol-colored dots. Tab bar shows
active sessions with color-coded indicators and close buttons.
Status bar displays connection info, theme, encoding, and terminal
size. All backed by connection and session Pinia stores with mock
data until Wails bindings are connected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add the unlock screen that gates entry to the main app. Includes
app store (unlocked state, firstRun flag), a centered dark-themed
unlock card with WRAITH branding, password validation for first-run
vault creation, and conditional rendering in App.vue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keyboard events now check if focus is on an input, textarea, select, or
contenteditable element and let the browser handle them normally. Also
fixes connectRdp type (Awaited<>) and async onMounted in RdpCanvas.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend sends verifyId with host-key-accept so backend can correlate the
verification response. Backend tracks pre-ready connections in pendingClients
map, destroys on error/disconnect, and guards against calling verify() after
the connection has already timed out or errored (_destruct crash fix).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
28 tests across 4 spec files. Vitest + happy-dom configured with Nuxt auto-import
shims ($$fetch, navigateTo, defineNuxtRouteMiddleware) so stores and composables
resolve cleanly outside the Nuxt runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Default protocol color strips on all host cards (wraith-blue for SSH, purple for RDP)
- Deterministic tag colors from 8-color palette (teal, amber, violet, rose, emerald, sky, orange, indigo)
- Last-connected recency coloring (green=today, amber=this week, gray=older)
- Section header dots (wraith-400 for Recent, gray for All Hosts)
- Active nav link highlighting (wraith-400)
- Group headers get subtle wraith-500 left border accent
- Tree host dots default to protocol color instead of gray
- Fixed rogue modal using hardcoded #1a1a2e/#e94560 — now uses design system
- Fixed sky-600 save buttons → wraith-600 for brand consistency
- Credential type badges: SSH Key=wraith, Password=amber (was purple/blue)
- Colored tags in right sidebar detail panel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
C-2: JWT moved from localStorage to httpOnly cookie (eliminates XSS token theft)
C-3: WebSocket auth via short-lived single-use tickets (JWT no longer in URLs)
H-1: JWT expiry reduced from 7 days to 4 hours
H-3: TOTP secrets encrypted at rest with vault EncryptionService (auto-migrates plaintext)
H-6: Rate limiting via @nestjs/throttler (60 req/min global, tighten on auth)
H-8: Constant-time login — Argon2id verify runs against dummy hash for non-existent users
H-9: Password hashing upgraded from bcrypt(10) to Argon2id (auto-upgrades on login)
H-10: Credential list API no longer returns encrypted blobs
H-16: Admin pages use Nuxt route middleware instead of client-side guard
Plus: auth bootstrap plugin, cookie-parser middleware, all frontend Authorization headers removed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Monaco can't mount in a popup window — it references document.activeElement
from the main window context, causing cross-window DOM errors.
Replaced with a fullscreen overlay teleported to <body>:
- Same dark theme toolbar with save/close/dirty indicator
- Ctrl+S to save, Esc to close
- Status bar shows language and keyboard shortcuts
- File tree stays visible underneath (overlay dismisses to it)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Multi-session tabs + home navigation:
- Tab bar with Home button persists above sessions
- Clicking Home shows the underlying page (hosts, vault, etc.)
- Clicking a session tab switches back to that session
- Header nav links also trigger home view
- Sessions stay alive in background when viewing home
Monaco editor in popup window:
- Opening a file in SFTP launches a detached popup with Monaco
- Full syntax highlighting, minimap, Ctrl+S save
- File tree stays visible while editing
- Toolbar with save/close buttons and dirty indicator
Drag-and-drop upload:
- Drop files anywhere on the SFTP sidebar to upload
- Visual overlay with dashed border on drag-over
- Supports multiple files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add admin-only "Users" nav link in header
- Create /admin/users page with full CRUD:
create user, edit, delete, reset password, reset TOTP
- Matches existing wraith dark theme
- Client-side admin guard redirects non-admins
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full per-user data isolation across all tables:
- Migration adds userId FK to hosts, host_groups, credentials, ssh_keys,
connection_logs. Backfills existing data to admin@wraith.local.
- All services scope queries by userId from JWT (req.user.sub).
Users can only see/modify their own data. Cross-user access returns 403.
- Two roles: admin (full access + user management) and user (own data only).
- Admin endpoints: list/create/edit/delete users, reset password, reset TOTP.
Protected by AdminGuard. Admins cannot delete themselves or remove own role.
- JWT payload now includes role. Frontend auth store exposes isAdmin getter.
- Seed script fixed: checks for admin@wraith.local specifically (not any user).
Uses upsert, seeds with role=admin. Migration cleans up duplicate users.
- Connection logs now attributed to the connecting user via WS auth.
- Deleting a user CASCADEs to all their hosts, credentials, keys, and logs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Inject shell integration (PROMPT_COMMAND/precmd) on SSH connect that
emits OSC 7 escape sequences reporting the working directory on every
prompt. Supports bash and zsh.
- Frontend captures OSC 7 via xterm.js parser, updates session store CWD.
- SFTP sidebar watches session CWD and navigates when it changes.
- SFTP starts at ~/ (user home) instead of / on initial connect, resolved
via SFTP realpath('.') on the backend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove CSS width/height !important override that broke Guacamole's
internal rendering pipeline. Replace with display.scale() auto-fitting
using ResizeObserver for responsive container sizing. Scale mouse
coordinates back to remote display space to keep input accurate.
Clean up diagnostic instruction logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Log first 50 instructions, then every 200th, plus any draw operation
targeting layer 0 (main display). Need to determine if RDPGFX desktop
frames are arriving or if only cursor operations are being received.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Temporary diagnostics to debug blank screen after successful RDP connection.
Logs first 30 instruction opcodes and display dimensions on ready.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs fixed:
1. TCP stream fragmentation — guacd→browser data pipe treated each TCP
chunk as a complete instruction. TCP is a stream protocol; instructions
WILL be split across chunks (especially display/image data). Added
instruction buffer that accumulates data and only forwards complete
instructions (terminated by ';').
2. Missing client.onerror — when guacd fails the RDP connection (NLA,
auth, TLS), it sends a Guacamole error instruction. No handler was
registered, so errors were silently swallowed. User saw blank canvas
with no feedback. Now surfaces errors via console and gateway callback.
3. Missing client.onstatechange — no connection state tracking. Added
state transition logging for diagnostics.
Also improved CONNECT handshake logging to surface connection parameters
(host, port, user, domain, security mode) without exposing passwords.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SFTP: Added console logging to diagnose, plus a watcher that sends
the pending list when sessionId becomes available (covers the race
where WS opens before sessionId is set).
RDP: connectHost() was returning early for non-SSH protocols.
Removed the guard and use host.protocol instead of hardcoded 'ssh'.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The list('/') call fired immediately after connect(), but the
WebSocket was still in CONNECTING state so send() silently dropped
the message. Now buffers the initial list request and sends it
in the onopen callback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When replaceSession changed the session ID from pending-XXX to a
real UUID, Vue's :key="session.id" treated it as a new element,
destroyed and recreated TerminalInstance, which called connectToHost
again, got another UUID, replaced again — infinite loop.
Added a stable `key` field to sessions that never changes after
creation, used as the Vue :key instead of the mutable `id`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When SSH connection fails, close the WebSocket immediately and
auto-remove the pending session after 3 seconds so the user sees
the error message before the panel clears. Prevents stuck sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NPM forwards /api/* correctly but silently drops WebSocket upgrades on
/ws/* despite toggle being enabled and custom nginx config. Moving
gateways to /api/ws/terminal and /api/ws/sftp so they ride the same
proxy rules that already work for REST endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: TerminalInstance.onMounted() called sessions.removeSession()
on the pending session, dropping sessions.length to 0. SessionContainer's
v-if="hasSessions" went false, unmounting the entire terminal UI before
the WebSocket could establish and add the real session.
Fix: Added replaceSession() to session store. TerminalInstance no longer
removes the pending session — instead passes its ID to connectToHost(),
which swaps it in-place when the backend responds with the real session ID.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Save editingHost.id to local var before nulling the ref.
Prevents TypeError on Array.find during post-save refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Edit now populates name, hostname, port, protocol, group, credential, tags
- Modal title shows "Edit Host" vs "New Host" appropriately
- Save calls updateHost for existing hosts, createHost for new
- Added group dropdown selector to host modal
- Button shows "Update" when editing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Added credential dropdown to New Host modal (loads from vault API)
- Fixed xterm.js "Cannot read dimensions" crash by guarding fitAddon.fit()
with requestAnimationFrame and container dimension checks
- Added WebGL context loss handler
- credentialId now passed when creating hosts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Tags field added to New Host modal (comma separated)
- Clicking + on a group now passes groupId to the new host
- Shows "Adding to: GroupName" in modal when creating from a group
- Refreshes tree after host creation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Settings now opens as a slide-over sidebar from nav bar (no page nav)
- Removed App Theme toggle (dark mode only — no working light mode)
- Terminal theme picker, font size, scrollback all in sidebar
- Removed standalone /settings page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Shows × on hover next to the + button. Confirms before deleting.
Hosts in deleted groups become ungrouped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Left sidebar:
- Groups now show recursive host count badges
- Hosts in tree show up to 3 tags inline
Right sidebar (Host Details panel):
- Click any host card to open details panel on the right
- Shows address, port, protocol, group, credential, tags, color, notes
- Connect, Edit, Delete action buttons at bottom
- Selected card gets ring highlight
Terminal themes (10 prebuilt):
- Wraith (default), Dracula, Nord, Solarized Dark, Monokai, One Dark,
Gruvbox Dark, Tokyo Night, Catppuccin Mocha, Cyberpunk
- Visual theme picker in Settings with color preview + sample text
- Persisted to /api/settings and localStorage for immediate use
- useTerminal reads theme on terminal creation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause of hosts not displaying: Nuxt auto-imports components in
subdirectories with the directory name as prefix. HostCard → ConnectionsHostCard,
HostTree → ConnectionsHostTree, QuickConnect → ConnectionsQuickConnect.
Unprefixed names silently resolve to nothing — no errors, just invisible components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same root cause as host/group dialogs — components in subdirectories
with Teleport don't render at runtime in Nuxt SPA builds. Inlined
CredentialForm and KeyImportDialog directly into their parent pages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dialogs: bypassed component-based dialogs entirely — inlined modals
directly in index.vue with inline style fallbacks for z-index/colors.
If button clicks work, we see the modal. Period.
Profile 500: created UpdateProfileDto with class-validator decorators
so ValidationPipe processes it correctly. Added error logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mismatched div count was silently breaking the component in
production builds. 18 opens, 19 closes → now 18/18.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PrimeVue Dialog wasn't rendering regardless of theme config.
Rewrote both Host and Group edit dialogs using Teleport + Tailwind,
matching the rest of the app's styling pattern.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
theme: 'none' meant Dialog/InputText/Select/Button had zero CSS.
Configured Aura preset with sky primary + dark mode selector.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Home/Profile links to nav bar alongside Vault/Settings/Logout
- Profile page: change email, display name, password
- TOTP 2FA: setup with QR code, verify, disable with password
- Login flow: two-step TOTP challenge when 2FA is enabled
- Backend: new endpoints PUT /profile, POST /totp/setup|verify|disable
- Migration: add totp_secret and totp_enabled columns to users
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>