llm-switcher 1.3.4 → 1.3.5
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 +8 -0
- package/package.json +1 -1
- package/tests/dashboard.e2e.test.mjs +17 -0
- package/tests/update.e2e.test.mjs +3 -2
- package/ui.html +31 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog — LLM Switcher
|
|
2
2
|
|
|
3
|
+
## Release 1.3.5
|
|
4
|
+
|
|
5
|
+
- **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.
|
|
6
|
+
- **Dashboard elevation:** The dialog, the toast and the model list used a wide 50 px shadow on top of their border. The shadow is now short and tight, so the border defines the edge, in both themes.
|
|
7
|
+
- **Dialog tabs on a phone:** At 390 px wide, the profile dialog cut off the **Model Slots** tab, and the tab bar hides its scrollbar. On a narrow screen the tabs now have less padding, and they wrap if they still do not fit.
|
|
8
|
+
- **API key field:** The field has `autocomplete="off"`, so a password manager does not offer to save the key.
|
|
9
|
+
- **Update now that times out:** The message "The new gateway did not answer" now also says what the last check saw: the gateway from before the update, an HTTP status, or a network error.
|
|
10
|
+
|
|
3
11
|
## Release 1.3.4
|
|
4
12
|
|
|
5
13
|
- **The dashboard page carries no token:** `/ui` sent `admin.token` in the page, and any program on the machine can load `/ui`. The page now gets the token only from the `#token=` of the private launcher that `switch ui` opens (a file with mode 0600). The tab keeps it in `sessionStorage`, so it does not stay in the browser after the tab closes. A tab opened at `/ui` directly says to use `switch ui`. `switch status` and the gateway start line say the same.
|
package/package.json
CHANGED
|
@@ -776,6 +776,23 @@ describe('dashboard, driven in a real browser', { skip: skipReason() }, () => {
|
|
|
776
776
|
}
|
|
777
777
|
});
|
|
778
778
|
|
|
779
|
+
it('shows every dialog tab whole on a phone', async () => {
|
|
780
|
+
await page.resize(390, 844);
|
|
781
|
+
try {
|
|
782
|
+
await editProfile('intact-claude');
|
|
783
|
+
await page.waitFor('document.getElementById("profile-modal-overlay").style.display === "grid"', 'the dialog');
|
|
784
|
+
const cut = await page.evaluate(`(() => {
|
|
785
|
+
const bar = document.querySelector('#profile-form .tabs').getBoundingClientRect();
|
|
786
|
+
return [...document.querySelectorAll('#profile-form .tabs .tab-btn')]
|
|
787
|
+
.filter(b => { const r = b.getBoundingClientRect(); return r.left < bar.left - 0.5 || r.right > bar.right + 0.5; })
|
|
788
|
+
.map(b => b.textContent.trim());
|
|
789
|
+
})()`);
|
|
790
|
+
assert.deepEqual(cut, [], 'these tabs are cut off by the tab bar at 390 px');
|
|
791
|
+
} finally {
|
|
792
|
+
await page.resize(1280, 900);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
|
|
779
796
|
it('does not let a long profile name break the dialog header', async () => {
|
|
780
797
|
const cfg = fx.config();
|
|
781
798
|
cfg.profiles['intact-claude'].name = 'N'.repeat(300);
|
|
@@ -186,7 +186,7 @@ test('switch update asks the running gateway and waits for the new one', { timeo
|
|
|
186
186
|
test('the dashboard Update now button installs the release and reloads on the new gateway', { timeout: 90000, skip: skipReason() }, async (t) => {
|
|
187
187
|
const fx = await fixture(t, { registryUrl: await registry(t, '9.9.12') });
|
|
188
188
|
await fx.start();
|
|
189
|
-
await fx.waitFor(fx.pid, 'the gateway');
|
|
189
|
+
const firstPid = await fx.waitFor(fx.pid, 'the gateway');
|
|
190
190
|
fx.release('9.9.12');
|
|
191
191
|
const browser = await launchBrowser();
|
|
192
192
|
t.after(() => browser.close());
|
|
@@ -200,7 +200,8 @@ test('the dashboard Update now button installs the release and reloads on the ne
|
|
|
200
200
|
} catch (err) {
|
|
201
201
|
// A CI runner cannot be watched: put what the gateway and the page saw into the failure.
|
|
202
202
|
const stage = await page.evaluate(`document.getElementById('update-status')?.textContent || ''`).catch(() => '(page gone)');
|
|
203
|
-
|
|
203
|
+
// The pid seen from this process tells a gateway that does not answer from a page that cannot reach it.
|
|
204
|
+
err.message += `\nupdate status on the page: ${stage}\npid on /health from the test: ${await fx.pid()} (first gateway ${firstPid})\ngateway log:\n${fx.log()}\npage problems: ${JSON.stringify(page.problems)}`;
|
|
204
205
|
throw err;
|
|
205
206
|
}
|
|
206
207
|
assert.equal(await page.evaluate(`document.getElementById('update-notice').hidden`), true);
|
package/ui.html
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
<style>
|
|
18
18
|
:root {
|
|
19
19
|
color-scheme: dark;
|
|
20
|
+
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
20
21
|
--bg: #0c1017;
|
|
21
22
|
--side: #10151e;
|
|
22
23
|
--card: #141a25;
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
--err: #fb7185;
|
|
38
39
|
--err-soft: rgba(251, 113, 133, 0.12);
|
|
39
40
|
--code: #5eead4;
|
|
40
|
-
--shadow: 0
|
|
41
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 12px -6px rgba(0, 0, 0, 0.55);
|
|
41
42
|
|
|
42
43
|
/* Compatibility aliases */
|
|
43
44
|
--surface: var(--side);
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
--err: #be123c;
|
|
90
91
|
--err-soft: rgba(190, 18, 60, 0.08);
|
|
91
92
|
--code: #0f766e;
|
|
92
|
-
--shadow: 0
|
|
93
|
+
--shadow: 0 1px 2px rgba(20, 26, 37, 0.08), 0 8px 12px -6px rgba(20, 26, 37, 0.16);
|
|
93
94
|
--border-strong: #cbd5e1;
|
|
94
95
|
}
|
|
95
96
|
}
|
|
@@ -115,7 +116,7 @@
|
|
|
115
116
|
--err: #be123c;
|
|
116
117
|
--err-soft: rgba(190, 18, 60, 0.08);
|
|
117
118
|
--code: #0f766e;
|
|
118
|
-
--shadow: 0
|
|
119
|
+
--shadow: 0 1px 2px rgba(20, 26, 37, 0.08), 0 8px 12px -6px rgba(20, 26, 37, 0.16);
|
|
119
120
|
--border-strong: #cbd5e1;
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -593,7 +594,7 @@
|
|
|
593
594
|
.switch { position: relative; width: 36px; height: 20px; flex: none; display: inline-block; vertical-align: middle; }
|
|
594
595
|
.switch input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; min-height: 0; }
|
|
595
596
|
.switch span { position: absolute; inset: 0; border-radius: 999px; background: var(--line); box-shadow: inset 0 0 0 1.5px var(--faint); transition: background-color 0.25s, box-shadow 0.25s; pointer-events: none; }
|
|
596
|
-
.switch span::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 999px; background: #ffffff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.25s
|
|
597
|
+
.switch span::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 999px; background: #ffffff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.25s var(--ease-out), width 0.15s; }
|
|
597
598
|
.switch input:active + span::after { width: 20px; }
|
|
598
599
|
.switch input:checked + span { background: var(--accent-strong); box-shadow: none; }
|
|
599
600
|
.switch input:checked + span::after { transform: translateX(16px); }
|
|
@@ -627,7 +628,7 @@
|
|
|
627
628
|
border: solid var(--accent-fg);
|
|
628
629
|
border-width: 0 2px 2px 0;
|
|
629
630
|
transform: rotate(45deg) scale(0);
|
|
630
|
-
transition: transform 0.15s
|
|
631
|
+
transition: transform 0.15s var(--ease-out);
|
|
631
632
|
}
|
|
632
633
|
input[type="checkbox"]:not(.switch input):checked { background: var(--accent-strong); border-color: var(--accent-strong); }
|
|
633
634
|
input[type="checkbox"]:not(.switch input):checked::before { transform: rotate(45deg) scale(1); }
|
|
@@ -646,7 +647,7 @@
|
|
|
646
647
|
opacity: 1;
|
|
647
648
|
transition: opacity 0.18s;
|
|
648
649
|
}
|
|
649
|
-
.overlay .modal { transition: transform 0.18s
|
|
650
|
+
.overlay .modal { transition: transform 0.18s var(--ease-out); }
|
|
650
651
|
@starting-style { .overlay { opacity: 0; } .overlay .modal { transform: scale(0.96) translateY(8px); } }
|
|
651
652
|
.modal {
|
|
652
653
|
background: var(--card);
|
|
@@ -693,6 +694,7 @@
|
|
|
693
694
|
scrollbar-width: none;
|
|
694
695
|
}
|
|
695
696
|
.tabs::-webkit-scrollbar { display: none; }
|
|
697
|
+
.tabs-wrap { padding: 0.85rem 1.25rem 0.5rem; background: var(--card2); }
|
|
696
698
|
.tabs button.tab-btn {
|
|
697
699
|
padding: 0.55rem 0.95rem;
|
|
698
700
|
color: var(--muted);
|
|
@@ -712,6 +714,13 @@
|
|
|
712
714
|
border-bottom-color: var(--accent);
|
|
713
715
|
font-weight: 600;
|
|
714
716
|
}
|
|
717
|
+
/* A phone has no room for all three tabs at full padding, and the bar hides its scrollbar:
|
|
718
|
+
tighten them, and wrap rather than hide a tab when even that is not enough. */
|
|
719
|
+
@media (max-width: 480px) {
|
|
720
|
+
.tabs-wrap { padding-inline: 1rem; }
|
|
721
|
+
.tabs { flex-wrap: wrap; }
|
|
722
|
+
.tabs button.tab-btn { padding-inline: 0.6rem; }
|
|
723
|
+
}
|
|
715
724
|
|
|
716
725
|
/* Log Items */
|
|
717
726
|
.log-item {
|
|
@@ -757,7 +766,7 @@
|
|
|
757
766
|
gap: 0.6rem;
|
|
758
767
|
overflow: hidden;
|
|
759
768
|
max-width: 90vw;
|
|
760
|
-
animation: fadeIn 0.25s
|
|
769
|
+
animation: fadeIn 0.25s var(--ease-out);
|
|
761
770
|
}
|
|
762
771
|
.toast::before { content: ""; width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
|
|
763
772
|
.toast::after { content: ""; position: absolute; left: 0; bottom: 0; width: 100%; height: 2px; background: var(--accent); transform-origin: left; animation: toastbar 3.2s linear forwards; }
|
|
@@ -782,7 +791,7 @@
|
|
|
782
791
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);
|
|
783
792
|
opacity: 1;
|
|
784
793
|
translate: 0 0;
|
|
785
|
-
transition: opacity 0.15s, translate 0.2s
|
|
794
|
+
transition: opacity 0.15s, translate 0.2s var(--ease-out), display 0.15s;
|
|
786
795
|
transition-behavior: allow-discrete;
|
|
787
796
|
}
|
|
788
797
|
.tip[hidden] { display: none; opacity: 0; translate: 0 4px; }
|
|
@@ -1123,7 +1132,7 @@
|
|
|
1123
1132
|
|
|
1124
1133
|
<form id="profile-form" onsubmit="saveProfile(event)" onkeydown="blockImplicitSubmit(event)" oninput="formDirty = true" onchange="formDirty = true" style="display:flex; flex-direction:column; overflow:hidden;">
|
|
1125
1134
|
<!-- Modal Inset Tabs -->
|
|
1126
|
-
<div
|
|
1135
|
+
<div class="tabs-wrap">
|
|
1127
1136
|
<div class="tabs">
|
|
1128
1137
|
<button type="button" id="tab-button-general" class="tab-btn active" onclick="switchTab('tab-general')">General</button>
|
|
1129
1138
|
<button type="button" id="tab-button-routing" class="tab-btn" onclick="switchTab('tab-routing')">Routing & Protocol</button>
|
|
@@ -1160,7 +1169,7 @@
|
|
|
1160
1169
|
</label>
|
|
1161
1170
|
|
|
1162
1171
|
<label class="f">API Key
|
|
1163
|
-
<input type="password" id="p-key-val" placeholder="sk-...">
|
|
1172
|
+
<input type="password" id="p-key-val" placeholder="sk-..." autocomplete="off">
|
|
1164
1173
|
</label>
|
|
1165
1174
|
</div>
|
|
1166
1175
|
|
|
@@ -2777,15 +2786,22 @@
|
|
|
2777
2786
|
return;
|
|
2778
2787
|
}
|
|
2779
2788
|
say('Installed v' + result.to + '. Restarting the gateway...');
|
|
2789
|
+
let last = 'no check ran';
|
|
2780
2790
|
for (let n = 0; n < 60; n++) {
|
|
2781
2791
|
await new Promise(r => setTimeout(r, 500));
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2792
|
+
try {
|
|
2793
|
+
const r = await fetch('/health?challenge=ui', { cache: 'no-store' });
|
|
2794
|
+
const pid = (await r.json()).pid ?? null;
|
|
2795
|
+
if (pid && pid !== before) {
|
|
2796
|
+
location.reload();
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
last = pid ? 'the gateway from before the update (pid ' + pid + ')' : 'HTTP ' + r.status + ' with no pid';
|
|
2800
|
+
} catch (err) {
|
|
2801
|
+
last = err.message;
|
|
2786
2802
|
}
|
|
2787
2803
|
}
|
|
2788
|
-
throw new Error('The new gateway did not answer. Read proxy.log.');
|
|
2804
|
+
throw new Error('The new gateway did not answer (last check: ' + last + '). Read proxy.log.');
|
|
2789
2805
|
} catch (err) {
|
|
2790
2806
|
say(err.message);
|
|
2791
2807
|
showToast('Update failed: ' + err.message, true);
|