feat: persist RDP/SSH passwords as base64, fix card layouts and modal UX
CI / Build & Test (push) Successful in 3m52s
CI / Build & Test (push) Successful in 3m52s
- Encode passwords as base64 (passwordB64) before persisting RDP and SSH connections to native host state, decoded back on load/refresh - Pre-fill password field when editing existing RDP or SSH connections - Fix Quick Actions "Add SSH/RDP Connection" buttons to call openAddSshModal(null) / openAddRdpModal(null) so forms are properly reset instead of showing stale data - Autofocus xterm.js terminal when SSH WebSocket connection is established - Redesign SSH and RDP connection cards with a two-row layout: label + actions in the top row, full hs:// key in a wrapping monospace meta row below; removes all JS truncation and CSS ellipsis clipping
This commit is contained in:
+44
-12
@@ -706,12 +706,18 @@
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
transition: border-color var(--transition), background var(--transition);
|
transition: border-color var(--transition), background var(--transition);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.rdp-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
|
.rdp-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
|
||||||
|
.rdp-conn-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.rdp-conn-icon {
|
.rdp-conn-icon {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
@@ -725,10 +731,20 @@
|
|||||||
}
|
}
|
||||||
.rdp-conn-icon svg { width: 18px; height: 18px; }
|
.rdp-conn-icon svg { width: 18px; height: 18px; }
|
||||||
.rdp-conn-info { flex: 1; min-width: 0; }
|
.rdp-conn-info { flex: 1; min-width: 0; }
|
||||||
.rdp-conn-label { font-size: 14px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.rdp-conn-label { font-size: 14px; font-weight: 600; color: var(--text); word-break: break-word; }
|
||||||
.rdp-conn-meta { font-size: 12px; color: var(--text3); margin-top: 2px; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.rdp-conn-actions { display: flex; gap: 6px; flex-shrink: 0; margin-left: auto; }
|
||||||
.rdp-conn-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
.rdp-conn-meta {
|
||||||
.rdp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--base);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.rdp-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
|
||||||
|
|
||||||
/* ── RDP Viewer modal ───────────────────────────── */
|
/* ── RDP Viewer modal ───────────────────────────── */
|
||||||
.modal-rdp-viewer {
|
.modal-rdp-viewer {
|
||||||
@@ -781,12 +797,18 @@
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 10px;
|
||||||
transition: border-color var(--transition), background var(--transition);
|
transition: border-color var(--transition), background var(--transition);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
.ssh-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
|
.ssh-conn-card:hover { border-color: var(--border2); background: var(--elevated); }
|
||||||
|
.ssh-conn-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
.ssh-conn-icon {
|
.ssh-conn-icon {
|
||||||
width: 38px;
|
width: 38px;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
@@ -801,10 +823,20 @@
|
|||||||
}
|
}
|
||||||
.ssh-conn-icon svg { width: 18px; height: 18px; }
|
.ssh-conn-icon svg { width: 18px; height: 18px; }
|
||||||
.ssh-conn-info { flex: 1; min-width: 0; }
|
.ssh-conn-info { flex: 1; min-width: 0; }
|
||||||
.ssh-conn-label { font-size: 14px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.ssh-conn-label { font-size: 14px; font-weight: 600; color: var(--text); word-break: break-word; }
|
||||||
.ssh-conn-meta { font-size: 12px; color: var(--text3); margin-top: 2px; font-family: 'JetBrains Mono', monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
.ssh-conn-actions { display: flex; gap: 6px; flex-shrink: 0; margin-left: auto; }
|
||||||
.ssh-conn-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
.ssh-conn-meta {
|
||||||
.ssh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--base);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.ssh-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
|
||||||
|
|
||||||
/* ── SSH Terminal modal ──────────────────────────── */
|
/* ── SSH Terminal modal ──────────────────────────── */
|
||||||
.modal-terminal {
|
.modal-terminal {
|
||||||
|
|||||||
+49
-45
@@ -260,33 +260,34 @@ function renderRdpGrid() {
|
|||||||
const typeBadge = conn.type === 'rdp'
|
const typeBadge = conn.type === 'rdp'
|
||||||
? `<span class="badge badge-amber" style="font-size:10px;padding:2px 7px;">RDP</span>`
|
? `<span class="badge badge-amber" style="font-size:10px;padding:2px 7px;">RDP</span>`
|
||||||
: `<span class="badge badge-cyan" style="font-size:10px;padding:2px 7px;">VNC</span>`;
|
: `<span class="badge badge-cyan" style="font-size:10px;padding:2px 7px;">VNC</span>`;
|
||||||
const meta = (conn.type === 'rdp' ? (conn.username ? conn.username + '@rdp' : 'rdp') : 'vnc')
|
const metaUser = conn.type === 'rdp' && conn.username ? escapeHtml(conn.username) + '@rdp' : conn.type.toUpperCase();
|
||||||
+ ' · ' + escapeHtml(truncate(conn.hsUrl, 28));
|
|
||||||
return `
|
return `
|
||||||
<div class="rdp-conn-card" data-rdp-id="${escapeHtml(conn.id)}">
|
<div class="rdp-conn-card" data-rdp-id="${escapeHtml(conn.id)}">
|
||||||
<div class="rdp-conn-icon">
|
<div class="rdp-conn-card-top">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<div class="rdp-conn-icon">
|
||||||
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
<rect x="2" y="3" width="20" height="14" rx="2"/>
|
||||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||||
</svg>
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||||
</div>
|
</svg>
|
||||||
<div class="rdp-conn-info">
|
</div>
|
||||||
<div class="rdp-conn-label">${escapeHtml(conn.label || (conn.type === 'rdp' ? 'RDP Desktop' : 'VNC Desktop'))} ${typeBadge}</div>
|
<div class="rdp-conn-info">
|
||||||
<div class="rdp-conn-meta">${escapeHtml(meta)}</div>
|
<div class="rdp-conn-label">${escapeHtml(conn.label || (conn.type === 'rdp' ? 'RDP Desktop' : 'VNC Desktop'))} ${typeBadge}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rdp-conn-actions">
|
<div class="rdp-conn-actions">
|
||||||
<button class="btn btn-primary" data-rdp-connect="${escapeHtml(conn.id)}" style="padding:6px 14px;font-size:12px;">
|
<button class="btn btn-primary" data-rdp-connect="${escapeHtml(conn.id)}" style="padding:6px 14px;font-size:12px;">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px;"><polyline points="5,12 19,12"/><polyline points="12,5 19,12 12,19"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px;"><polyline points="5,12 19,12"/><polyline points="12,5 19,12 12,19"/></svg>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ghost" data-rdp-edit="${escapeHtml(conn.id)}" style="padding:6px 10px;" title="Edit">
|
<button class="btn btn-ghost" data-rdp-edit="${escapeHtml(conn.id)}" style="padding:6px 10px;" title="Edit">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ghost" data-rdp-remove="${escapeHtml(conn.id)}" style="padding:6px 10px;color:var(--red);" title="Remove">
|
<button class="btn btn-ghost" data-rdp-remove="${escapeHtml(conn.id)}" style="padding:6px 10px;color:var(--red);" title="Remove">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="rdp-conn-meta">${metaUser} · ${escapeHtml(conn.hsUrl)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
@@ -1992,28 +1993,30 @@ function renderSshGrid() {
|
|||||||
}
|
}
|
||||||
grid.innerHTML = sshConnections.map(conn => `
|
grid.innerHTML = sshConnections.map(conn => `
|
||||||
<div class="ssh-conn-card" data-ssh-id="${escapeHtml(conn.id)}">
|
<div class="ssh-conn-card" data-ssh-id="${escapeHtml(conn.id)}">
|
||||||
<div class="ssh-conn-icon">
|
<div class="ssh-conn-card-top">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<div class="ssh-conn-icon">
|
||||||
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<polyline points="8,10 12,14 16,10"/>
|
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||||
</svg>
|
<polyline points="8,10 12,14 16,10"/>
|
||||||
</div>
|
</svg>
|
||||||
<div class="ssh-conn-info">
|
</div>
|
||||||
<div class="ssh-conn-label">${escapeHtml(conn.label || conn.username + '@ssh')}</div>
|
<div class="ssh-conn-info">
|
||||||
<div class="ssh-conn-meta">${escapeHtml(conn.username)}@ssh · ${escapeHtml(truncate(conn.hsUrl, 32))}</div>
|
<div class="ssh-conn-label">${escapeHtml(conn.label || conn.username + '@ssh')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ssh-conn-actions">
|
<div class="ssh-conn-actions">
|
||||||
<button class="btn btn-primary" data-ssh-connect="${escapeHtml(conn.id)}" style="padding:6px 14px;font-size:12px;">
|
<button class="btn btn-primary" data-ssh-connect="${escapeHtml(conn.id)}" style="padding:6px 14px;font-size:12px;">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px;"><polyline points="5,12 19,12"/><polyline points="12,5 19,12 12,19"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px;"><polyline points="5,12 19,12"/><polyline points="12,5 19,12 12,19"/></svg>
|
||||||
Connect
|
Connect
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ghost" data-ssh-edit="${escapeHtml(conn.id)}" style="padding:6px 10px;" title="Edit">
|
<button class="btn btn-ghost" data-ssh-edit="${escapeHtml(conn.id)}" style="padding:6px 10px;" title="Edit">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ghost" data-ssh-remove="${escapeHtml(conn.id)}" style="padding:6px 10px;color:var(--red);" title="Remove">
|
<button class="btn btn-ghost" data-ssh-remove="${escapeHtml(conn.id)}" style="padding:6px 10px;color:var(--red);" title="Remove">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ssh-conn-meta">${escapeHtml(conn.username)}@ssh · ${escapeHtml(conn.hsUrl)}</div>
|
||||||
</div>`).join('');
|
</div>`).join('');
|
||||||
|
|
||||||
// Wire up card buttons
|
// Wire up card buttons
|
||||||
@@ -2172,6 +2175,7 @@ async function connectSsh(conn) {
|
|||||||
// Send a ready-signal so the native host knows the browser WebSocket is
|
// Send a ready-signal so the native host knows the browser WebSocket is
|
||||||
// fully open and can safely flush buffered PTY output (MOTD, prompt).
|
// fully open and can safely flush buffered PTY output (MOTD, prompt).
|
||||||
ws.send('\x00');
|
ws.send('\x00');
|
||||||
|
term.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
let firstMessage = true;
|
let firstMessage = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user