Skip to content

Commit f6e67b6

Browse files
Copilotbinaricat
andcommitted
fix: move auto-start to app level and remove indicator icon
Co-authored-by: binaricat <16399091+binaricat@users.noreply.github.com>
1 parent a86c74e commit f6e67b6

5 files changed

Lines changed: 148 additions & 117 deletions

File tree

App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from 'react';
22
import { activeTabStore, useActiveTabId, useIsSftpActive, useIsTerminalLayerVisible, useIsVaultActive } from './application/state/activeTabStore';
33
import { useAutoSync } from './application/state/useAutoSync';
4+
import { usePortForwardingAutoStart } from './application/state/usePortForwardingAutoStart';
45
import { useSessionState } from './application/state/useSessionState';
56
import { useSettingsState } from './application/state/useSettingsState';
67
import { useUpdateCheck } from './application/state/useUpdateCheck';
@@ -284,6 +285,12 @@ function App({ settings }: { settings: SettingsState }) {
284285
}
285286
}, [updateState.hasUpdate, updateState.latestRelease, t, openReleasePage, dismissUpdate]);
286287

288+
// Auto-start port forwarding rules on app launch
289+
usePortForwardingAutoStart({
290+
hosts,
291+
keys: keys.map((k) => ({ id: k.id, privateKey: k.privateKey })),
292+
});
293+
287294
// Debounce ref for moveFocus to prevent double-triggering when focus switches
288295
const lastMoveFocusTimeRef = useRef<number>(0);
289296
const MOVE_FOCUS_DEBOUNCE_MS = 200;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* Hook for auto-starting port forwarding rules on app launch.
3+
* This should be used at the App level to ensure auto-start happens
4+
* when the application starts, not when the user navigates to the port forwarding page.
5+
*/
6+
import { useEffect, useRef } from "react";
7+
import { Host, PortForwardingRule } from "../../domain/models";
8+
import { STORAGE_KEY_PORT_FORWARDING } from "../../infrastructure/config/storageKeys";
9+
import { localStorageAdapter } from "../../infrastructure/persistence/localStorageAdapter";
10+
import {
11+
getActiveConnection,
12+
setReconnectCallback,
13+
startPortForward,
14+
syncWithBackend,
15+
} from "../../infrastructure/services/portForwardingService";
16+
import { logger } from "../../lib/logger";
17+
18+
export interface UsePortForwardingAutoStartOptions {
19+
hosts: Host[];
20+
keys: { id: string; privateKey: string }[];
21+
}
22+
23+
/**
24+
* Auto-starts port forwarding rules that have autoStart enabled.
25+
* This hook should be called at the App level to run on app launch.
26+
*/
27+
export const usePortForwardingAutoStart = ({
28+
hosts,
29+
keys,
30+
}: UsePortForwardingAutoStartOptions): void => {
31+
const autoStartExecutedRef = useRef(false);
32+
const hostsRef = useRef<Host[]>(hosts);
33+
const keysRef = useRef<{ id: string; privateKey: string }[]>(keys);
34+
35+
// Keep refs in sync
36+
useEffect(() => {
37+
hostsRef.current = hosts;
38+
}, [hosts]);
39+
40+
useEffect(() => {
41+
keysRef.current = keys;
42+
}, [keys]);
43+
44+
// Set up the reconnect callback
45+
useEffect(() => {
46+
const handleReconnect = async (
47+
ruleId: string,
48+
onStatusChange: (status: PortForwardingRule["status"], error?: string) => void,
49+
) => {
50+
// Load the current rules from storage
51+
const rules = localStorageAdapter.read<PortForwardingRule[]>(
52+
STORAGE_KEY_PORT_FORWARDING,
53+
) ?? [];
54+
55+
const rule = rules.find((r) => r.id === ruleId);
56+
if (!rule || !rule.hostId) {
57+
return { success: false, error: "Rule or host not found" };
58+
}
59+
60+
const host = hostsRef.current.find((h) => h.id === rule.hostId);
61+
if (!host) {
62+
return { success: false, error: "Host not found" };
63+
}
64+
65+
return startPortForward(rule, host, keysRef.current, onStatusChange, true);
66+
};
67+
68+
setReconnectCallback(handleReconnect);
69+
return () => {
70+
setReconnectCallback(null);
71+
};
72+
}, []);
73+
74+
// Auto-start rules on app launch
75+
useEffect(() => {
76+
if (autoStartExecutedRef.current) return;
77+
if (hosts.length === 0) return;
78+
79+
const runAutoStart = async () => {
80+
// First sync with backend to get any active tunnels
81+
await syncWithBackend();
82+
83+
// Load rules from storage
84+
const rules = localStorageAdapter.read<PortForwardingRule[]>(
85+
STORAGE_KEY_PORT_FORWARDING,
86+
) ?? [];
87+
88+
// Only start rules that are not already active
89+
const autoStartRules = rules.filter((r) => {
90+
if (!r.autoStart || !r.hostId) return false;
91+
// Check if there's an active connection for this rule
92+
const conn = getActiveConnection(r.id);
93+
// Only start if not already connecting or active
94+
return !conn || conn.status === 'inactive' || conn.status === 'error';
95+
});
96+
97+
if (autoStartRules.length === 0) return;
98+
99+
autoStartExecutedRef.current = true;
100+
logger.info(`[PortForwardingAutoStart] Starting ${autoStartRules.length} auto-start rules`);
101+
102+
// Start each auto-start rule
103+
for (const rule of autoStartRules) {
104+
const host = hosts.find((h) => h.id === rule.hostId);
105+
if (host) {
106+
void startPortForward(
107+
rule,
108+
host,
109+
keys,
110+
(status, error) => {
111+
// Update the rule status in storage
112+
const currentRules = localStorageAdapter.read<PortForwardingRule[]>(
113+
STORAGE_KEY_PORT_FORWARDING,
114+
) ?? [];
115+
116+
const updatedRules = currentRules.map((r) =>
117+
r.id === rule.id
118+
? {
119+
...r,
120+
status,
121+
error,
122+
lastUsedAt: status === "active" ? Date.now() : r.lastUsedAt,
123+
}
124+
: r,
125+
);
126+
127+
localStorageAdapter.write(STORAGE_KEY_PORT_FORWARDING, updatedRules);
128+
},
129+
true, // Enable reconnect for auto-start rules
130+
);
131+
}
132+
}
133+
};
134+
135+
void runAutoStart();
136+
}, [hosts, keys]);
137+
};

application/state/usePortForwardingState.ts

Lines changed: 2 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1+
import { useCallback, useEffect, useMemo, useState } from "react";
22
import { Host, PortForwardingRule } from "../../domain/models";
33
import {
44
STORAGE_KEY_PF_PREFER_FORM_MODE,
@@ -10,7 +10,6 @@ import {
1010
clearReconnectTimer,
1111
getActiveConnection,
1212
getActiveRuleIds,
13-
setReconnectCallback,
1413
startPortForward,
1514
stopPortForward,
1615
syncWithBackend,
@@ -64,16 +63,7 @@ export interface UsePortForwardingStateResult {
6463
selectedRule: PortForwardingRule | undefined;
6564
}
6665

67-
export interface UsePortForwardingStateOptions {
68-
hosts?: Host[];
69-
keys?: { id: string; privateKey: string }[];
70-
}
71-
72-
export const usePortForwardingState = (
73-
options: UsePortForwardingStateOptions = {},
74-
): UsePortForwardingStateResult => {
75-
const { hosts = [], keys = [] } = options;
76-
66+
export const usePortForwardingState = (): UsePortForwardingStateResult => {
7767
const [rules, setRules] = useState<PortForwardingRule[]>([]);
7868
const [selectedRuleId, setSelectedRuleId] = useState<string | null>(null);
7969
const [viewMode, setViewMode] = useStoredViewMode(
@@ -91,27 +81,6 @@ export const usePortForwardingState = (
9181
localStorageAdapter.writeBoolean(STORAGE_KEY_PF_PREFER_FORM_MODE, prefer);
9282
}, []);
9383

94-
// Ref to store the current rules, hosts, and keys for the reconnect callback
95-
const rulesRef = useRef<PortForwardingRule[]>(rules);
96-
const hostsRef = useRef<Host[]>(hosts);
97-
const keysRef = useRef<{ id: string; privateKey: string }[]>(keys);
98-
99-
// Keep refs in sync
100-
useEffect(() => {
101-
rulesRef.current = rules;
102-
}, [rules]);
103-
104-
useEffect(() => {
105-
hostsRef.current = hosts;
106-
}, [hosts]);
107-
108-
useEffect(() => {
109-
keysRef.current = keys;
110-
}, [keys]);
111-
112-
// Track if auto-start has been executed
113-
const autoStartExecutedRef = useRef(false);
114-
11584
// Load rules from storage on mount and sync with backend
11685
useEffect(() => {
11786
const loadAndSync = async () => {
@@ -145,76 +114,6 @@ export const usePortForwardingState = (
145114
localStorageAdapter.write(STORAGE_KEY_PORT_FORWARDING, updatedRules);
146115
}, []);
147116

148-
// Reconnect callback - used by the service layer to trigger reconnection
149-
const handleReconnect = useCallback(
150-
async (
151-
ruleId: string,
152-
onStatusChange: (status: PortForwardingRule["status"], error?: string) => void,
153-
) => {
154-
const rule = rulesRef.current.find((r) => r.id === ruleId);
155-
if (!rule || !rule.hostId) {
156-
return { success: false, error: "Rule or host not found" };
157-
}
158-
159-
const host = hostsRef.current.find((h) => h.id === rule.hostId);
160-
if (!host) {
161-
return { success: false, error: "Host not found" };
162-
}
163-
164-
return startPortForward(rule, host, keysRef.current, onStatusChange, true);
165-
},
166-
[],
167-
);
168-
169-
// Set up the reconnect callback in the service layer
170-
useEffect(() => {
171-
setReconnectCallback(handleReconnect);
172-
return () => {
173-
setReconnectCallback(null);
174-
};
175-
}, [handleReconnect]);
176-
177-
// Auto-start rules when hosts and keys become available
178-
useEffect(() => {
179-
if (autoStartExecutedRef.current) return;
180-
if (rules.length === 0 || hosts.length === 0) return;
181-
182-
const autoStartRules = rules.filter(
183-
(r) => r.autoStart && r.status === "inactive" && r.hostId,
184-
);
185-
186-
if (autoStartRules.length === 0) return;
187-
188-
autoStartExecutedRef.current = true;
189-
190-
// Start each auto-start rule
191-
for (const rule of autoStartRules) {
192-
const host = hosts.find((h) => h.id === rule.hostId);
193-
if (host) {
194-
void startPortForward(
195-
rule,
196-
host,
197-
keys,
198-
(status, error) => {
199-
setRules((prev) =>
200-
prev.map((r) =>
201-
r.id === rule.id
202-
? {
203-
...r,
204-
status,
205-
error,
206-
lastUsedAt: status === "active" ? Date.now() : r.lastUsedAt,
207-
}
208-
: r,
209-
),
210-
);
211-
},
212-
true, // Enable reconnect for auto-start rules
213-
);
214-
}
215-
}
216-
}, [rules, hosts, keys]);
217-
218117
const addRule = useCallback(
219118
(
220119
rule: Omit<PortForwardingRule, "id" | "createdAt" | "status">,

components/PortForwardingNew.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,7 @@ const PortForwarding: React.FC<PortForwardingProps> = ({
100100
selectedRule: _selectedRule,
101101
preferFormMode,
102102
setPreferFormMode,
103-
} = usePortForwardingState({
104-
hosts,
105-
keys: keys.map((k) => ({ id: k.id, privateKey: k.privateKey })),
106-
});
103+
} = usePortForwardingState();
107104

108105
// Track connecting/stopping states
109106
const [pendingOperations, setPendingOperations] = useState<Set<string>>(

components/port-forwarding/RuleCard.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Port Forwarding Rule Card
33
* Displays a single port forwarding rule in grid or list view
44
*/
5-
import { Copy,Loader2,Pencil,Play,RotateCcw,Square,Trash2 } from 'lucide-react';
5+
import { Copy,Loader2,Pencil,Play,Square,Trash2 } from 'lucide-react';
66
import React from 'react';
77
import { useI18n } from '../../application/i18n/I18nProvider';
88
import { PortForwardingRule } from '../../domain/models';
@@ -65,15 +65,6 @@ export const RuleCard: React.FC<RuleCardProps> = ({
6565
<div className="flex-1 min-w-0">
6666
<div className="flex items-center gap-2">
6767
<span className="text-sm font-semibold truncate">{rule.label}</span>
68-
{rule.autoStart && (
69-
<RotateCcw
70-
size={10}
71-
className="text-muted-foreground flex-shrink-0"
72-
title={t('pf.form.autoStart')}
73-
aria-label={t('pf.form.autoStart')}
74-
role="img"
75-
/>
76-
)}
7768
<span
7869
className={cn(
7970
"h-2 w-2 rounded-full flex-shrink-0",

0 commit comments

Comments
 (0)