# v0.4.28 (2026-05-10)
## Features - Add bun:sqlite adapter with automatic runtime detection (Bun/Node) - Add bulk API key import (format: `name|sk-key`, one per line) ## Fixes - Fix add API key for custom providers
This commit is contained in:
1 parent
b39eb61c33
commit
530dc9cb3b
14 files changed
+282
-71
No files matched your search
@@ -72,6 +72,8 @@ export default function APIPageClient({ machineId }) {
|
||||
const [tsLoading, setTsLoading] = useState(false);
|
||||
const [tsProgress, setTsProgress] = useState("");
|
||||
const [tsStatus, setTsStatus] = useState(null);
|
||||
const [tsAuthUrl, setTsAuthUrl] = useState("");
|
||||
const [tsAuthLabel, setTsAuthLabel] = useState("");
|
||||
const [tsInstalled, setTsInstalled] = useState(null); // null=checking, true/false
|
||||
const [tsInstalling, setTsInstalling] = useState(false);
|
||||
const [tsInstallLog, setTsInstallLog] = useState([]);
|
||||
@@ -492,12 +494,16 @@ export default function APIPageClient({ machineId }) {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Open auth URL only when actually needed (avoids blank popup flash on success path).
|
||||
// Falls back to status message with clickable link if popup blocker prevents opening.
|
||||
const openAuthUrl = (url) => {
|
||||
const w = window.open(url, "tailscale_auth", "width=600,height=700");
|
||||
if (!w) setTsStatus({ type: "warning", message: `Popup blocked. Open manually: ${url}` });
|
||||
return w;
|
||||
// Show inline login button instead of auto-opening popup (browsers block popups
|
||||
// opened after async work because the user gesture is lost).
|
||||
const requestUserAuth = (url, label) => {
|
||||
setTsAuthUrl(url);
|
||||
setTsAuthLabel(label);
|
||||
};
|
||||
|
||||
const clearUserAuth = () => {
|
||||
setTsAuthUrl("");
|
||||
setTsAuthLabel("");
|
||||
};
|
||||
|
||||
const handleConnectTailscale = async () => {
|
||||
@@ -506,6 +512,7 @@ export default function APIPageClient({ machineId }) {
|
||||
setTsLoading(true);
|
||||
setTsStatus(null);
|
||||
setTsProgress("Connecting...");
|
||||
clearUserAuth();
|
||||
try {
|
||||
const res = await fetch("/api/tunnel/tailscale-enable", { method: "POST" });
|
||||
const data = await res.json();
|
||||
@@ -519,8 +526,8 @@ export default function APIPageClient({ machineId }) {
|
||||
}
|
||||
|
||||
if (data.needsLogin && data.authUrl) {
|
||||
openAuthUrl(data.authUrl);
|
||||
setTsProgress("Waiting for login...");
|
||||
requestUserAuth(data.authUrl, "Open Login Page");
|
||||
setTsProgress("Login required — click \"Open Login Page\" to continue");
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
try {
|
||||
@@ -528,6 +535,7 @@ export default function APIPageClient({ machineId }) {
|
||||
if (r2.ok) {
|
||||
const check = await r2.json();
|
||||
if (check.loggedIn) {
|
||||
clearUserAuth();
|
||||
setTsProgress("Starting funnel...");
|
||||
const res2 = await fetch("/api/tunnel/tailscale-enable", { method: "POST" });
|
||||
const data2 = await res2.json();
|
||||
@@ -546,6 +554,7 @@ export default function APIPageClient({ machineId }) {
|
||||
}
|
||||
} catch { /* retry */ }
|
||||
}
|
||||
clearUserAuth();
|
||||
setTsStatus({ type: "error", message: "Login timed out. Please try again." });
|
||||
return;
|
||||
}
|
||||
@@ -562,18 +571,20 @@ export default function APIPageClient({ machineId }) {
|
||||
setTsLoading(false);
|
||||
setTsConnecting(false);
|
||||
setTsProgress("");
|
||||
clearUserAuth();
|
||||
}
|
||||
};
|
||||
|
||||
const pollFunnelEnable = async (enableUrl) => {
|
||||
openAuthUrl(enableUrl);
|
||||
setTsProgress("Enable Funnel in browser, waiting...");
|
||||
requestUserAuth(enableUrl, "Open Funnel Settings");
|
||||
setTsProgress("Click \"Open Funnel Settings\" to enable Funnel...");
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
try {
|
||||
const res = await fetch("/api/tunnel/tailscale-enable", { method: "POST" });
|
||||
const data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
clearUserAuth();
|
||||
setTsUrl(data.tunnelUrl || "");
|
||||
const ok3 = await pingTsHealth(data.tunnelUrl);
|
||||
setTsEnabled(true);
|
||||
@@ -582,11 +593,13 @@ export default function APIPageClient({ machineId }) {
|
||||
}
|
||||
if (data.funnelNotEnabled) continue;
|
||||
if (data.error) {
|
||||
clearUserAuth();
|
||||
setTsStatus({ type: "error", message: data.error });
|
||||
return;
|
||||
}
|
||||
} catch { /* retry */ }
|
||||
}
|
||||
clearUserAuth();
|
||||
setTsStatus({ type: "error", message: "Timed out waiting for Funnel to be enabled." });
|
||||
};
|
||||
|
||||
@@ -614,8 +627,13 @@ export default function APIPageClient({ machineId }) {
|
||||
const handleOpenTsModal = async () => {
|
||||
setTsStatus(null);
|
||||
setTsInstallLog([]);
|
||||
setShowTsModal(true);
|
||||
await checkTailscaleInstalled();
|
||||
const data = await checkTailscaleInstalled();
|
||||
if (data?.installed) {
|
||||
// Skip modal, connect directly when already installed
|
||||
handleConnectTailscale();
|
||||
} else {
|
||||
setShowTsModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateKey = async () => {
|
||||
@@ -857,8 +875,17 @@ export default function APIPageClient({ machineId }) {
|
||||
<span className="material-symbols-outlined animate-spin text-sm">progress_activity</span>
|
||||
{tsProgress || "Connecting..."}
|
||||
</div>
|
||||
{tsAuthUrl && (
|
||||
<Button
|
||||
size="sm"
|
||||
icon="open_in_new"
|
||||
onClick={() => window.open(tsAuthUrl, "tailscale_auth", "width=600,height=700,noopener,noreferrer")}
|
||||
>
|
||||
{tsAuthLabel || "Open"}
|
||||
</Button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setTsLoading(false); setTsConnecting(false); setTsProgress(""); }}
|
||||
onClick={() => { setTsLoading(false); setTsConnecting(false); setTsProgress(""); clearUserAuth(); }}
|
||||
className="p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
|
||||
title="Stop"
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user