llm-switcher 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — LLM Switcher
2
2
 
3
+ ## Release 1.3.6
4
+
5
+ - **Dashboard from a bookmark:** In 1.3.4 the tab kept the token in `sessionStorage`. A bookmark, a typed URL or a new tab then had no token: every call got HTTP 401, and the dashboard showed no profiles and every route as OFF. The token is now in `localStorage`, so after one `switch ui` every tab of that browser works. The page still does not carry the token.
6
+ - **Dashboard without a token:** The header now says to open the dashboard with `switch ui`. Before, it said "Gateway is idle", which looked like a config with no routes.
7
+
3
8
  ## Release 1.3.5
4
9
 
5
10
  - **Dashboard motion:** The switch, the checkbox, the dialog, the toast and the tooltip used easing curves that overshoot and bounce back. They now use one smooth ease-out curve (`--ease-out`). The durations did not change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-switcher",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "Zero-dependency multi-protocol edge gateway & provider switcher for Claude Code and Codex, with OpenAI-compatible, Anthropic and Vertex upstreams",
5
5
  "keywords": [
6
6
  "llm",
@@ -24,17 +24,37 @@ describe('dashboard, driven in a real browser', { skip: skipReason() }, () => {
24
24
  await page.goto(fx.tokenUrl());
25
25
  });
26
26
 
27
- it('a tab opened without the launcher token says to open the dashboard with switch ui', async () => {
27
+ // Close the tab itself, then raise the main one: a tab left in the background gets throttled
28
+ // timers and no animation frames, which stalls every later test.
29
+ async function closeTab(tab) {
30
+ await tab.send('Page.close').catch(() => {});
31
+ tab.close();
32
+ await page.send('Page.bringToFront');
33
+ }
34
+
35
+ it('a new tab, opened from a bookmark after switch ui ran once, shows the profiles', async () => {
36
+ const fresh = await browser.newPage();
37
+ try {
38
+ await fresh.goto(fx.dashboardUrl + '#/routes');
39
+ await fresh.waitFor(`document.querySelectorAll('#profiles-grid .pcard').length === ${Object.keys(fx.config().profiles).length}`, 'the profile cards');
40
+ } finally {
41
+ await closeTab(fresh);
42
+ }
43
+ });
44
+
45
+ it('a browser that never got the launcher token says so on the page, not only in a toast', async () => {
28
46
  const fresh = await browser.newPage();
29
47
  try {
30
- await fresh.goto(fx.dashboardUrl);
31
- await fresh.waitFor(`/switch ui/.test(document.getElementById('toast')?.textContent || '')`, 'the switch ui hint');
48
+ await fresh.goto(fx.dashboardUrl + '#/routes');
49
+ await fresh.evaluate(`localStorage.clear(), sessionStorage.clear()`);
50
+ await fresh.reload();
51
+ await fresh.waitFor(`/switch ui/.test(document.getElementById('status-sub')?.textContent || '')`, 'the switch ui hint in the header');
32
52
  } finally {
33
- // Close the tab itself, then raise the main one: a tab left in the background gets throttled
34
- // timers and no animation frames, which stalls every later test.
35
- await fresh.send('Page.close').catch(() => {});
36
- fresh.close();
37
- await page.send('Page.bringToFront');
53
+ await closeTab(fresh);
54
+ // The storage is shared by the origin, so the main tab needs the token again. A #hash change
55
+ // alone fires no load event, so leave the page first.
56
+ await page.goto('about:blank');
57
+ await page.goto(fx.tokenUrl());
38
58
  }
39
59
  });
40
60
 
package/ui.html CHANGED
@@ -1256,12 +1256,13 @@
1256
1256
  <script>
1257
1257
  // State & Globals
1258
1258
  const ADMIN_TOKEN_KEY = 'llm-switcher-admin-token';
1259
- // The token comes only in the #token of the private launcher that `switch ui` opens, and stays in
1260
- // this tab. The page itself never carries it: any process on the machine can load /ui.
1259
+ // The token comes only in the #token of the private launcher that `switch ui` opens. The page itself
1260
+ // never carries it: any process on the machine can load /ui. localStorage, not sessionStorage, so a
1261
+ // bookmark or a new tab in the same browser keeps working after one `switch ui`.
1261
1262
  const NO_TOKEN_HINT = 'This tab has no access token. Open the dashboard with `switch ui`.';
1262
1263
  const hashTokenMatch = location.hash.match(/(?:^#|&)token=([0-9a-f]+)/);
1263
1264
  if (hashTokenMatch) {
1264
- sessionStorage.setItem(ADMIN_TOKEN_KEY, hashTokenMatch[1]);
1265
+ localStorage.setItem(ADMIN_TOKEN_KEY, hashTokenMatch[1]);
1265
1266
  }
1266
1267
  if (hashTokenMatch) {
1267
1268
  history.replaceState(null, '', location.pathname + location.search + '#/routes');
@@ -1289,7 +1290,7 @@
1289
1290
 
1290
1291
  // API Helper
1291
1292
  async function api(path, opts = {}) {
1292
- const token = sessionStorage.getItem(ADMIN_TOKEN_KEY) || '';
1293
+ const token = localStorage.getItem(ADMIN_TOKEN_KEY) || '';
1293
1294
  const headers = {
1294
1295
  ...opts.headers,
1295
1296
  'x-llm-switcher-token': token
@@ -1461,6 +1462,8 @@
1461
1462
  const subEl = document.getElementById('status-sub');
1462
1463
  const actionsEl = document.getElementById('ph-actions');
1463
1464
  if (!titleEl || !subEl || !actionsEl) return;
1465
+ // Without the token every call is a 401, and the empty state would read as "nothing routed".
1466
+ const noToken = !localStorage.getItem(ADMIN_TOKEN_KEY);
1464
1467
 
1465
1468
  const activeMap = state.activeProfiles || (state.config?.activeProfiles || {});
1466
1469
  const activeTargets = Object.entries(activeMap).filter(([_, v]) => Boolean(v));
@@ -1474,7 +1477,7 @@
1474
1477
  if (activeMap.agy) parts.push('agy: ' + activeMap.agy);
1475
1478
  subEl.textContent = 'Port ' + state.port + ' · Active for ' + parts.join(' · ');
1476
1479
  } else {
1477
- subEl.textContent = 'Gateway is idle. Clients connect directly to official endpoints.';
1480
+ subEl.textContent = noToken ? NO_TOKEN_HINT : 'Gateway is idle. Clients connect directly to official endpoints.';
1478
1481
  }
1479
1482
  actionsEl.innerHTML = `
1480
1483
  <button class="btn btn-primary" onclick="openProfileModal(true)">+ Add Profile</button>
@@ -2810,7 +2813,7 @@
2810
2813
  }
2811
2814
 
2812
2815
  document.addEventListener('DOMContentLoaded', () => {
2813
- if (!sessionStorage.getItem(ADMIN_TOKEN_KEY)) showToast(NO_TOKEN_HINT, true);
2816
+ if (!localStorage.getItem(ADMIN_TOKEN_KEY)) showToast(NO_TOKEN_HINT, true);
2814
2817
  // Setup initial theme buttons state
2815
2818
  const currentTheme = document.documentElement.dataset.theme || 'dark';
2816
2819
  document.getElementById('btn-theme-dark')?.setAttribute('aria-pressed', currentTheme === 'dark');