Skip to content

Commit 919d2ac

Browse files
committed
feat: enhance connection settings with manual API port and cache mode options in browser and VS Code extensions
1 parent 7e62234 commit 919d2ac

10 files changed

Lines changed: 225 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
99

1010
### Changed
1111

12-
- **Extension connection settings** — the browser extension popup now lets users set a manual API port and choose the discovered-port cache duration. The VS Code extension gained a new `timelens.apiBaseUrlCacheSeconds` setting for the same purpose. Both extensions still scan the fallback range automatically when no manual port is set.
12+
- **Extension connection settings** — the browser extension popup now lets users set a manual API port, choose between duration-based cache or "until next startup" cache, and configure the cache duration. The VS Code extension gained `timelens.apiBaseUrlCacheMode` (`duration` / `startup`) and `timelens.apiBaseUrlCacheSeconds` settings for the same behavior. If a manually configured localhost port fails repeatedly, both extensions temporarily ignore it and automatically scan the fallback port range.
1313
- **Settings page UI redesign** — rewrote the Settings page to use a reusable card-based layout consistent with Backup & Restore v2, including `glass-card` containers, icon-header cards, and inner content wells.
1414

1515
### Fixed

browser-extension/api.js

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,74 @@
11
const STORAGE_KEYS = {
22
apiPort: "timelens.apiPort",
3+
apiCacheMode: "timelens.apiCacheMode",
34
apiCacheSeconds: "timelens.apiCacheSeconds",
45
};
56

67
const DEFAULT_API_PORT = 49152;
78
const API_PORT_FALLBACK_COUNT = 1000;
89
const DEFAULT_CACHE_SECONDS = 60;
910
const FALLBACK_CACHE_MS = 5_000;
11+
const MANUAL_PORT_FAILURE_THRESHOLD = 5;
1012

1113
let discoveredApiBaseCache = null;
14+
let manualPortFailureCount = 0;
15+
let manualPortDisabledUntil = 0;
16+
17+
function log(...args) {
18+
console.log("[TimeLens API]", ...args);
19+
}
20+
21+
function logWarn(...args) {
22+
console.warn("[TimeLens API]", ...args);
23+
}
1224

1325
async function getConnectionSettings() {
14-
const { [STORAGE_KEYS.apiPort]: port, [STORAGE_KEYS.apiCacheSeconds]: cacheSeconds } =
15-
await chrome.storage.local.get([STORAGE_KEYS.apiPort, STORAGE_KEYS.apiCacheSeconds]);
16-
return {
17-
manualPort: typeof port === "number" ? port : (parseInt(port, 10) || 0),
18-
cacheSeconds: typeof cacheSeconds === "number" ? cacheSeconds : (parseInt(cacheSeconds, 10) || DEFAULT_CACHE_SECONDS),
26+
const {
27+
[STORAGE_KEYS.apiPort]: port,
28+
[STORAGE_KEYS.apiCacheMode]: cacheMode,
29+
[STORAGE_KEYS.apiCacheSeconds]: cacheSeconds,
30+
} = await chrome.storage.local.get([
31+
STORAGE_KEYS.apiPort,
32+
STORAGE_KEYS.apiCacheMode,
33+
STORAGE_KEYS.apiCacheSeconds,
34+
]);
35+
36+
const manualPort = parseInt(port, 10) || 0;
37+
const settings = {
38+
manualPort,
39+
cacheMode: cacheMode === "startup" ? "startup" : "duration",
40+
cacheSeconds: parseInt(cacheSeconds, 10) || DEFAULT_CACHE_SECONDS,
1941
};
42+
log("Loaded connection settings:", settings);
43+
return settings;
2044
}
2145

2246
/**
2347
* Discover the actual local API port. The desktop backend may bind to a
2448
* fallback port when 49152 is unavailable (e.g. blocked by Windows / AV).
2549
* If the user has set a manual port, that port is tried first.
50+
*
51+
* If the manual port fails repeatedly, it is temporarily ignored and the
52+
* fallback range is scanned automatically. This handles the case where the
53+
* user entered a wrong port or the desktop moved to a different port.
2654
*/
2755
export async function discoverApiBaseUrl() {
2856
const now = Date.now();
2957
if (discoveredApiBaseCache && discoveredApiBaseCache.expiresAt > now) {
3058
return discoveredApiBaseCache.value;
3159
}
3260

33-
const { manualPort, cacheSeconds } = await getConnectionSettings();
34-
const cacheMs = Math.max(0, cacheSeconds) * 1000;
61+
const { manualPort, cacheMode, cacheSeconds } = await getConnectionSettings();
62+
const cacheMs = cacheMode === "startup" ? Number.MAX_SAFE_INTEGER : Math.max(0, cacheSeconds) * 1000;
3563

3664
const portsToTry = [];
37-
if (manualPort > 0 && manualPort <= 65535) {
65+
const manualPortAllowed =
66+
manualPort > 0 &&
67+
manualPort <= 65535 &&
68+
now > manualPortDisabledUntil &&
69+
manualPortFailureCount < MANUAL_PORT_FAILURE_THRESHOLD;
70+
71+
if (manualPortAllowed) {
3872
portsToTry.push(manualPort);
3973
}
4074
for (let offset = 0; offset <= API_PORT_FALLBACK_COUNT; offset += 1) {
@@ -44,7 +78,11 @@ export async function discoverApiBaseUrl() {
4478
}
4579
}
4680

81+
let manualPortTried = false;
4782
for (const port of portsToTry) {
83+
if (port === manualPort) {
84+
manualPortTried = true;
85+
}
4886
const baseUrl = `http://127.0.0.1:${port}`;
4987
try {
5088
const controller = new AbortController();
@@ -56,6 +94,10 @@ export async function discoverApiBaseUrl() {
5694
if (response.ok) {
5795
const data = await response.json();
5896
if (data && typeof data.version === "string") {
97+
if (port === manualPort) {
98+
manualPortFailureCount = 0;
99+
manualPortDisabledUntil = 0;
100+
}
59101
discoveredApiBaseCache = { value: baseUrl, expiresAt: now + cacheMs };
60102
return baseUrl;
61103
}
@@ -65,6 +107,14 @@ export async function discoverApiBaseUrl() {
65107
}
66108
}
67109

110+
if (manualPortTried) {
111+
manualPortFailureCount += 1;
112+
if (manualPortFailureCount >= MANUAL_PORT_FAILURE_THRESHOLD) {
113+
// Temporarily ignore the manual port for 5 minutes so the fallback scan can work.
114+
manualPortDisabledUntil = now + 5 * 60 * 1000;
115+
}
116+
}
117+
68118
discoveredApiBaseCache = { value: "", expiresAt: now + FALLBACK_CACHE_MS };
69119
return null;
70120
}
@@ -77,5 +127,11 @@ export function clearApiBaseUrlCache() {
77127
discoveredApiBaseCache = null;
78128
}
79129

130+
export function resetManualPortFailureTracking() {
131+
manualPortFailureCount = 0;
132+
manualPortDisabledUntil = 0;
133+
}
134+
80135
export const API_STORAGE_KEYS = STORAGE_KEYS;
81136
export const DEFAULT_CACHE_SECONDS_VALUE = DEFAULT_CACHE_SECONDS;
137+
export const MANUAL_PORT_FAILURE_THRESHOLD_VALUE = MANUAL_PORT_FAILURE_THRESHOLD;

browser-extension/background.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313

1414
import { getLocale, t } from "./i18n.js";
15-
import { discoverApiBaseUrl, getApiBaseUrl } from "./api.js";
15+
import { discoverApiBaseUrl, getApiBaseUrl, clearApiBaseUrlCache, resetManualPortFailureTracking } from "./api.js";
1616

1717
const STORAGE_KEYS = {
1818
activeSession: "timelens.activeSession",
@@ -35,13 +35,17 @@ chrome.runtime.onInstalled.addListener(() => {
3535
chrome.alarms.create("timelens-api-heartbeat", { periodInMinutes: 1 });
3636
safeConfigureIdleDetection();
3737
initState();
38+
clearApiBaseUrlCache();
39+
resetManualPortFailureTracking();
3840
pingApiStatus();
3941
});
4042

4143
chrome.runtime.onStartup.addListener(() => {
4244
chrome.alarms.create("timelens-api-heartbeat", { periodInMinutes: 1 });
4345
safeConfigureIdleDetection();
4446
initState();
47+
clearApiBaseUrlCache();
48+
resetManualPortFailureTracking();
4549
flushPendingSessions();
4650
});
4751

browser-extension/i18n.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ const TRANSLATIONS = {
3131
bridgeKeyCleared: "Extension bridge key cleared.",
3232
connectionSettings: "Connection settings",
3333
apiPort: "API Port:",
34-
cacheDuration: "Cache (s):",
35-
connectionSettingsHint: "Set the port to 0 to scan the fallback range. Cache duration controls how long the discovered port is remembered.",
34+
cacheMode: "Cache:",
35+
cacheModeDuration: "Duration",
36+
cacheModeStartup: "Until next startup",
37+
cacheDuration: "Duration (s):",
38+
connectionSettingsHint: "Set the port to 0 to scan the fallback range. If the set port fails repeatedly, the extension will automatically scan again.",
3639
},
3740
"zh-CN": {
3841
connection: "桌面端连接",
@@ -66,8 +69,11 @@ const TRANSLATIONS = {
6669
bridgeKeyCleared: "扩展网桥密钥已清除。",
6770
connectionSettings: "连接设置",
6871
apiPort: "API 端口:",
69-
cacheDuration: "缓存(秒):",
70-
connectionSettingsHint: "端口设为 0 将扫描回退范围。缓存时长控制记住已发现端口的时长。",
72+
cacheMode: "缓存:",
73+
cacheModeDuration: "时长",
74+
cacheModeStartup: "到下次启动",
75+
cacheDuration: "缓存时长(秒):",
76+
connectionSettingsHint: "端口设为 0 将扫描回退范围。若设置的端口持续失败,扩展会自动重新扫描。",
7177
},
7278
"zh-TW": {
7379
connection: "桌面端連線",
@@ -101,8 +107,11 @@ const TRANSLATIONS = {
101107
bridgeKeyCleared: "擴充功能橋接金鑰已清除。",
102108
connectionSettings: "連線設定",
103109
apiPort: "API 連接埠:",
104-
cacheDuration: "快取(秒):",
105-
connectionSettingsHint: "連接埠設為 0 將掃描回退範圍。快取時長控制記住已發現連接埠的時間。",
110+
cacheMode: "快取:",
111+
cacheModeDuration: "時長",
112+
cacheModeStartup: "到下次啟動",
113+
cacheDuration: "快取時長(秒):",
114+
connectionSettingsHint: "連接埠設為 0 將掃描回退範圍。若設定的連接埠持續失敗,擴充功能會自動重新掃描。",
106115
},
107116
};
108117

browser-extension/popup.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,17 @@ <h2 data-i18n="connectionSettings">Connection settings</h2>
5555
/>
5656
</div>
5757
<div style="display: flex; align-items: center; gap: 8px;">
58-
<label for="cache-duration-input" class="muted" style="font-size: 12px; width: 80px;" data-i18n="cacheDuration">Cache (s):</label>
58+
<label for="cache-mode-select" class="muted" style="font-size: 12px; width: 80px;" data-i18n="cacheMode">Cache:</label>
59+
<select
60+
id="cache-mode-select"
61+
style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px;"
62+
>
63+
<option value="duration" data-i18n="cacheModeDuration">Duration</option>
64+
<option value="startup" data-i18n="cacheModeStartup">Until next startup</option>
65+
</select>
66+
</div>
67+
<div id="cache-duration-row" style="display: flex; align-items: center; gap: 8px;">
68+
<label for="cache-duration-input" class="muted" style="font-size: 12px; width: 80px;" data-i18n="cacheDuration">Duration (s):</label>
5969
<input
6070
id="cache-duration-input"
6171
type="number"
@@ -66,7 +76,7 @@ <h2 data-i18n="connectionSettings">Connection settings</h2>
6676
/>
6777
</div>
6878
<button id="save-connection-settings-button" class="ghost-button" type="button" data-i18n="saveOrUpdate">Save / Update</button>
69-
<p class="muted" style="font-size: 11px;" data-i18n="connectionSettingsHint">Set the port to 0 to scan the fallback range. Cache duration controls how long the discovered port is remembered.</p>
79+
<p class="muted" style="font-size: 11px;" data-i18n="connectionSettingsHint">Set the port to 0 to scan the fallback range. If the set port fails repeatedly, the extension will automatically scan again.</p>
7080
</div>
7181
</section>
7282

browser-extension/popup.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const bridgeKeyPanel = document.querySelector("#bridge-key-panel");
2525
const bridgeKeyInput = document.querySelector("#bridge-key-input");
2626
const saveKeyButton = document.querySelector("#save-key-button");
2727
const apiPortInput = document.querySelector("#api-port-input");
28+
const cacheModeSelect = document.querySelector("#cache-mode-select");
29+
const cacheDurationRow = document.querySelector("#cache-duration-row");
2830
const cacheDurationInput = document.querySelector("#cache-duration-input");
2931
const saveConnectionSettingsButton = document.querySelector("#save-connection-settings-button");
3032
const locale = getLocale();
@@ -50,22 +52,45 @@ if (saveKeyButton) {
5052

5153
// Load and save connection settings
5254
async function loadConnectionSettings() {
53-
const { [API_STORAGE_KEYS.apiPort]: port, [API_STORAGE_KEYS.apiCacheSeconds]: cacheSeconds } =
54-
await chrome.storage.local.get([API_STORAGE_KEYS.apiPort, API_STORAGE_KEYS.apiCacheSeconds]);
55+
const {
56+
[API_STORAGE_KEYS.apiPort]: port,
57+
[API_STORAGE_KEYS.apiCacheMode]: cacheMode,
58+
[API_STORAGE_KEYS.apiCacheSeconds]: cacheSeconds,
59+
} = await chrome.storage.local.get([
60+
API_STORAGE_KEYS.apiPort,
61+
API_STORAGE_KEYS.apiCacheMode,
62+
API_STORAGE_KEYS.apiCacheSeconds,
63+
]);
5564
if (apiPortInput) {
5665
apiPortInput.value = port === 0 || port === "0" ? "0" : (port ? String(port) : "");
5766
}
67+
if (cacheModeSelect) {
68+
cacheModeSelect.value = cacheMode === "startup" ? "startup" : "duration";
69+
}
5870
if (cacheDurationInput) {
5971
cacheDurationInput.value = String(cacheSeconds ?? DEFAULT_CACHE_SECONDS_VALUE);
6072
}
73+
if (cacheDurationRow) {
74+
cacheDurationRow.style.display = cacheMode === "startup" ? "none" : "flex";
75+
}
76+
}
77+
78+
if (cacheModeSelect) {
79+
cacheModeSelect.addEventListener("change", () => {
80+
if (cacheDurationRow) {
81+
cacheDurationRow.style.display = cacheModeSelect.value === "startup" ? "none" : "flex";
82+
}
83+
});
6184
}
6285

6386
if (saveConnectionSettingsButton) {
6487
saveConnectionSettingsButton.addEventListener("click", async () => {
6588
const port = parseInt(apiPortInput?.value || "0", 10) || 0;
89+
const cacheMode = cacheModeSelect?.value === "startup" ? "startup" : "duration";
6690
const cacheSeconds = parseInt(cacheDurationInput?.value || String(DEFAULT_CACHE_SECONDS_VALUE), 10) || DEFAULT_CACHE_SECONDS_VALUE;
6791
await chrome.storage.local.set({
6892
[API_STORAGE_KEYS.apiPort]: port,
93+
[API_STORAGE_KEYS.apiCacheMode]: cacheMode,
6994
[API_STORAGE_KEYS.apiCacheSeconds]: cacheSeconds,
7095
});
7196
clearApiBaseUrlCache();

vscode-extension/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@
108108
"maximum": 3600,
109109
"description": "%timelens.apiBaseUrlCacheSeconds.description%"
110110
},
111+
"timelens.apiBaseUrlCacheMode": {
112+
"type": "string",
113+
"default": "duration",
114+
"enum": ["duration", "startup"],
115+
"enumDescriptions": [
116+
"%timelens.apiBaseUrlCacheMode.duration%",
117+
"%timelens.apiBaseUrlCacheMode.startup%"
118+
],
119+
"description": "%timelens.apiBaseUrlCacheMode.description%"
120+
},
111121
"timelens.bridgeKey": {
112122
"type": "string",
113123
"default": "",

vscode-extension/package.nls.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"timelens.enabled.description": "Enable local VS Code usage tracking for TimeLens.",
44
"timelens.apiBaseUrl.description": "Local TimeLens API base URL.",
55
"timelens.apiBaseUrlCacheSeconds.description": "How long to cache the discovered local API port (seconds). Set to 0 to re-discover on every request.",
6+
"timelens.apiBaseUrlCacheMode.description": "Cache discovered port until VS Code restarts, or for a fixed duration.",
7+
"timelens.apiBaseUrlCacheMode.duration": "Cache for the duration configured below.",
8+
"timelens.apiBaseUrlCacheMode.startup": "Cache until VS Code restarts.",
69
"timelens.bridgeKey.description": "Extension bridge key for authenticating with TimeLens local API. Get this from Settings > Local API / Extension Bridge.",
710
"timelens.apiToken.description": "Local API token for authenticating with TimeLens when token-required mode is enabled. Get this from Settings > Local API / Tokens.",
811
"timelens.flushIntervalSeconds.description": "How often to flush queued sessions to TimeLens API.",

vscode-extension/package.nls.zh-CN.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"timelens.enabled.description": "启用 TimeLens 本地 VS Code 使用时长追踪。",
44
"timelens.apiBaseUrl.description": "TimeLens 本地 API 基础 URL。",
55
"timelens.apiBaseUrlCacheSeconds.description": "缓存已发现本地 API 端口的时长(秒)。设为 0 则每次请求都重新发现。",
6+
"timelens.apiBaseUrlCacheMode.description": "缓存已发现端口直到 VS Code 重启,或按下方固定时长缓存。",
7+
"timelens.apiBaseUrlCacheMode.duration": "按下方配置时长缓存。",
8+
"timelens.apiBaseUrlCacheMode.startup": "缓存到 VS Code 重启。",
69
"timelens.bridgeKey.description": "用于向 TimeLens 本地 API 认证的扩展桥接密钥,可在「设置 > 本地 API / 扩展桥接」中获取。",
710
"timelens.apiToken.description": "在启用「需要 API Token」模式时,用于向 TimeLens 本地 API 认证的令牌,可在「设置 > 本地 API / 令牌」中获取。",
811
"timelens.flushIntervalSeconds.description": "将排队会话刷新到 TimeLens API 的频率(秒)。",

0 commit comments

Comments
 (0)