diff --git a/frontend/src/css/forms.css b/frontend/src/css/forms.css
index d5ed6fe..b787e29 100644
--- a/frontend/src/css/forms.css
+++ b/frontend/src/css/forms.css
@@ -486,12 +486,66 @@
}
.jump-host-row {
- display: grid;
- grid-template-columns: 1fr 80px 1fr auto;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ padding: var(--space-2);
+ background: var(--surface-2);
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+}
+
+.jump-host-fields {
+ display: flex;
gap: var(--space-2);
align-items: center;
}
+/* -----------------------------------------------------------------------
+ Port-forwarding rows
+ ----------------------------------------------------------------------- */
+.port-forwards-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+
+.port-forward-row {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ padding: var(--space-2);
+ background: var(--surface-2);
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+}
+
+.port-forward-fields {
+ display: flex;
+ gap: var(--space-2);
+ align-items: center;
+}
+
+.port-forward-arrow {
+ color: var(--text-muted);
+ font-size: var(--text-sm);
+ flex-shrink: 0;
+}
+
+.port-forward-enabled-label {
+ display: flex;
+ gap: var(--space-1);
+ align-items: center;
+ font-size: var(--text-xs);
+ color: var(--text-secondary);
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.port-forward-enabled-label input[type="checkbox"] {
+ accent-color: var(--accent);
+}
+
/* -----------------------------------------------------------------------
Payload preset rows
----------------------------------------------------------------------- */
diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js
index ea6c8d5..e862cd5 100644
--- a/frontend/src/js/app.js
+++ b/frontend/src/js/app.js
@@ -180,6 +180,21 @@ class App {
);
});
+ // Port-forwarding events
+ window.runtime.EventsOn('session:forward:started', (payload) => {
+ const fwd = payload.forward || {};
+ const label = fwd.label || `${fwd.bindHost || ''}:${fwd.bindPort} → ${fwd.destHost}:${fwd.destPort}`;
+ this.notifications.success(`Forward started: ${label}`);
+ });
+
+ window.runtime.EventsOn('session:forward:stopped', (payload) => {
+ this.notifications.info(`Forward stopped: ${payload.forwardID}`);
+ });
+
+ window.runtime.EventsOn('session:forward:error', (payload) => {
+ this.notifications.error(`Forward error: ${payload.error}`);
+ });
+
// Telnet negotiation debug messages
window.runtime.EventsOn('session:debug', (payload) => {
console.debug('[session:debug]', payload);
diff --git a/frontend/src/js/profiles.js b/frontend/src/js/profiles.js
index 2bbfb14..4bc153b 100644
--- a/frontend/src/js/profiles.js
+++ b/frontend/src/js/profiles.js
@@ -24,6 +24,7 @@ const DEFAULTS = {
keepaliveCountMax: 3,
compression: false,
jumpHosts: [],
+ portForwards: [],
startupCommand: '',
envVars: {},
},
@@ -373,6 +374,11 @@ class ProfileEditor {
this._renderJumpHostsList(ssh.jumpHosts || []),
], true));
+ // Port Forwarding
+ sections.push(this._renderSection('Port Forwarding', [
+ this._renderPortForwardsList(ssh.portForwards || []),
+ ], true));
+
// Startup
sections.push(this._renderSection('Startup', [
this._field('text', 'ssh.startupCommand', 'Startup Command', ssh.startupCommand, {
@@ -712,10 +718,15 @@ class ProfileEditor {
const jh = jumpHosts[i];
const row = createElement('div', 'jump-host-row');
row.innerHTML = `
-
-
-
-
+
+
+
+
+
+
+
+
+
`;
row.querySelector('.jump-host-remove').addEventListener('click', () => {
jumpHosts.splice(i, 1);
@@ -734,6 +745,65 @@ class ProfileEditor {
return wrapper;
}
+ /**
+ * Renders the port-forwarding rules list editor.
+ * @param {Array