diff --git a/CHANGELOG.md b/CHANGELOG.md index 56c2b99..f66dfbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 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 + # v0.4.27 (2026-05-09) ## Features diff --git a/next.config.mjs b/next.config.mjs index 31644a3..99d5557 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { output: "standalone", - serverExternalPackages: ["better-sqlite3"], + serverExternalPackages: ["better-sqlite3", "sql.js", "node:sqlite", "bun:sqlite"], images: { unoptimized: true }, diff --git a/package.json b/package.json index 2efa813..8e47141 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "9router-app", - "version": "0.4.27", + "version": "0.4.28", "description": "9Router web dashboard", "private": true, "scripts": { - "dev": "next dev --webpack --hostname 0.0.0.0 --port 20128", + "dev": "next dev --webpack --port 20128", "build": "NODE_ENV=production next build --webpack", "start": "NODE_ENV=production next start", "dev:bun": "bun --bun next dev --webpack --port 20128", diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index 57a2287..cb18377 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -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 }) { progress_activity {tsProgress || "Connecting..."} + {tsAuthUrl && ( + + )} + + + + {mode === "bulk" && ( +
+

One key per line. Format: name|apiKey or just apiKey (auto-named by index).

+