Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,15 @@ const proxyClientMaxBodySize = process.env.NINEROUTER_PROXY_CLIENT_MAX_BODY_SIZE

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
distDir: process.env.NEXT_DIST_DIR || ".next",
output: "standalone",
// basePath is required for middleware to receive request.nextUrl.basePath,
// for client-side navigation (router.push, Link) to auto-prepend the prefix,
// and for NEXT_PUBLIC_BASE_PATH to be inlined at build time.
// assetPrefix only prefixes static asset URLs and does NOT populate basePath in middleware.
basePath: process.env.NINEROUTER_BASE_PATH || "",

serverExternalPackages: ["better-sqlite3", "sql.js", "node:sqlite", "bun:sqlite"],
turbopack: {
root: tracingRoot
Expand All @@ -24,7 +31,9 @@ const nextConfig = {
images: {
unoptimized: true
},
env: {},
env: {
NEXT_PUBLIC_BASE_PATH: process.env.NINEROUTER_BASE_PATH || "",
},
experimental: {
// #1529/#1572: LLM clients can send long context or base64 image payloads through /v1 rewrites.
proxyClientMaxBodySize,
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
"description": "9Router web dashboard",
"private": true,
"scripts": {
"dev": "next dev --webpack --port 20128",
"build": "next build --webpack",
"dev": "next dev --port 20128",
"prebuild": "bash scripts/prebuild.sh",
"build": "next build",
"postbuild": "bash scripts/postbuild.sh",
"start": "next start",
"dev:bun": "bun --bun next dev --webpack --port 20128",
"build:bun": "bun --bun next build --webpack",
"dev:bun": "bun --bun next dev --port 20128",
"build:bun": "bun --bun next build",
"start:bun": "bun ./.next/standalone/server.js"
},
"dependencies": {
Expand All @@ -27,7 +29,7 @@
"marked": "^18.0.1",
"material-symbols": "^0.44.6",
"monaco-editor": "^0.55.1",
"next": "^16.1.6",
"next": "16.2.8",
"node-forge": "^1.3.3",
"node-machine-id": "^1.1.12",
"open": "^11.0.0",
Expand Down
5 changes: 5 additions & 0 deletions postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
"@tailwindcss/postcss": {},
},
};
File renamed without changes.
12 changes: 12 additions & 0 deletions postcss.config.mjs.original
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import path from "node:path";
import { fileURLToPath } from "node:url";

const projectRoot = path.dirname(fileURLToPath(import.meta.url));

export default {
plugins: {
"@tailwindcss/postcss": {
base: projectRoot,
},
},
};
52 changes: 52 additions & 0 deletions scripts/postbuild.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/bin/bash
# Fix the standalone static symlink after rebuild
# The rebuild destroys the symlink; this recreates it unconditionally.
# Also copies proxy/middleware manifest files (REQUIRED for proxy registration in production)
set -e

STANDALONE_DIR=".next/standalone"
MAIN_STATIC=".next/static"
STANDALONE_STATIC="${STANDALONE_DIR}/.next/static"

if [ -d "${STANDALONE_DIR}" ]; then
# Remove stale symlink or directory if present
if [ -L "${STANDALONE_STATIC}" ] || [ -d "${STANDALONE_STATIC}" ]; then
rm -rf "${STANDALONE_STATIC}"
fi
ln -sf "$(pwd)/${MAIN_STATIC}" "$(pwd)/${STANDALONE_STATIC}"
echo "Created symlink: ${STANDALONE_STATIC} -> $(pwd)/${MAIN_STATIC}"
fi

# Copy proxy/middleware manifest files to standalone (REQUIRED for proxy registration)
# Next.js 16 uses .next/server/middleware/ subdirectory for the manifest
mkdir -p "${STANDALONE_DIR}/.next/server/middleware" 2>/dev/null || true

# Next.js 16 (Turbopack) — proxy manifest at .next/server/middleware/
if [ -f ".next/server/middleware/middleware-manifest.json" ]; then
cp .next/server/middleware/middleware-manifest.json "${STANDALONE_DIR}/.next/server/middleware/middleware-manifest.json"
echo "Copied Next.js 16 proxy manifest (middleware/ subdirectory)"
fi

# Legacy / webpack — middleware manifest at .next/server/ root
if [ -f ".next/server/middleware-manifest.json" ]; then
cp .next/server/middleware-manifest.json "${STANDALONE_DIR}/.next/server/middleware-manifest.json" 2>/dev/null || true
cp .next/server/middleware-build-manifest.js "${STANDALONE_DIR}/.next/server/middleware-build-manifest.js" 2>/dev/null || true
cp .next/server/middleware.js "${STANDALONE_DIR}/.next/server/middleware.js" 2>/dev/null || true
echo "Copied legacy middleware manifest files to standalone"
fi

# Also copy proxy manifest if present
if [ -f ".next/server/proxy-manifest.json" ]; then
mkdir -p "${STANDALONE_DIR}/.next/server/proxy" 2>/dev/null || true
cp .next/server/proxy-manifest.json "${STANDALONE_DIR}/.next/server/proxy/proxy-manifest.json" 2>/dev/null || true
echo "Copied proxy manifest to standalone"
fi

# Copy edge chunks (required for proxy runtime)
if [ -d ".next/server/edge/chunks" ]; then
mkdir -p "${STANDALONE_DIR}/.next/server/edge/chunks"
cp -r .next/server/edge/chunks/* "${STANDALONE_DIR}/.next/server/edge/chunks/" 2>/dev/null || true
echo "Copied edge chunks to standalone"
fi

echo "Post-build complete"
10 changes: 10 additions & 0 deletions scripts/prebuild.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
# Ensure SQLite WAL files exist for the standalone build copy
set -e
DB_DIR="data/db"
mkdir -p "${DB_DIR}"
for f in data.sqlite-shm data.sqlite-wal; do
if [ ! -f "${DB_DIR}/${f}" ]; then
touch "${DB_DIR}/${f}"
fi
done
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ export default function BasicChatPageClient() {
setLoadError("");

try {
const providersRes = await fetch("/api/providers", { cache: "no-store" });
const providersRes = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/providers", { cache: "no-store" });
const providersData = await providersRes.json().catch(() => ({}));
const connections = Array.isArray(providersData.connections)
? providersData.connections.filter((connection) => connection?.isActive !== false)
Expand Down Expand Up @@ -636,7 +636,7 @@ export default function BasicChatPageClient() {
}));

try {
const response = await fetch("/api/dashboard/chat/completions", {
const response = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/dashboard/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ export default function ToolDetailClient({ toolId, machineId }) {
(async () => {
try {
const [provRes, settingsRes, tunnelRes, keysRes] = await Promise.all([
fetch("/api/providers"),
fetch("/api/settings"),
fetch("/api/tunnel/status"),
fetch("/api/keys"),
fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/providers"),
fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/settings"),
fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/tunnel/status"),
fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/keys"),
]);
if (!mounted) return;
if (provRes.ok) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function AntigravityToolCard({

const loadSavedMappings = async () => {
try {
const res = await fetch("/api/cli-tools/antigravity-mitm/alias?tool=antigravity");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/antigravity-mitm/alias?tool=antigravity");
if (res.ok) {
const data = await res.json();
const aliases = data.aliases || {};
Expand All @@ -67,7 +67,7 @@ export default function AntigravityToolCard({

const fetchModelAliases = async () => {
try {
const res = await fetch("/api/models/alias");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/models/alias");
const data = await res.json();
if (res.ok) setModelAliases(data.aliases || {});
} catch (error) {
Expand All @@ -77,7 +77,7 @@ export default function AntigravityToolCard({

const fetchStatus = async () => {
try {
const res = await fetch("/api/cli-tools/antigravity-mitm");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/antigravity-mitm");
if (res.ok) {
const data = await res.json();
setStatus(data);
Expand Down Expand Up @@ -120,7 +120,7 @@ export default function AntigravityToolCard({
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|| (!cloudEnabled ? "sk_9router" : null);

const res = await fetch("/api/cli-tools/antigravity-mitm", {
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/antigravity-mitm", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ apiKey: keyToUse, sudoPassword: password }),
Expand Down Expand Up @@ -149,7 +149,7 @@ export default function AntigravityToolCard({
setLoading(true);
setMessage(null);
try {
const res = await fetch("/api/cli-tools/antigravity-mitm", {
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/antigravity-mitm", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sudoPassword: password }),
Expand Down Expand Up @@ -209,7 +209,7 @@ export default function AntigravityToolCard({
setMessage(null);

try {
const res = await fetch("/api/cli-tools/antigravity-mitm/alias", {
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/antigravity-mitm/alias", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ tool: "antigravity", mappings: modelMappings }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ export default function ClaudeToolCard({
}, [isExpanded]);

useEffect(() => {
fetch("/api/settings").then(r => r.json()).then(data => {
fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/settings").then(r => r.json()).then(data => {
setCcFilterNaming(!!data.ccFilterNaming);
}).catch(() => {});
}, []);

const handleCcFilterNamingToggle = async (e) => {
const value = e.target.checked;
setCcFilterNaming(value);
await fetch("/api/settings", {
await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ccFilterNaming: value }),
Expand All @@ -87,7 +87,7 @@ export default function ClaudeToolCard({

const fetchModelAliases = async () => {
try {
const res = await fetch("/api/models/alias");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/models/alias");
const data = await res.json();
if (res.ok) setModelAliases(data.aliases || {});
} catch (error) {
Expand Down Expand Up @@ -120,7 +120,7 @@ export default function ClaudeToolCard({
const checkClaudeStatus = async () => {
setCheckingClaude(true);
try {
const res = await fetch("/api/cli-tools/claude-settings");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/claude-settings");
const data = await res.json();
setClaudeStatus(data);
} catch (error) {
Expand Down Expand Up @@ -159,7 +159,7 @@ export default function ClaudeToolCard({
const targetModel = modelMappings[model.alias];
if (targetModel && model.envKey) env[model.envKey] = targetModel;
});
const res = await fetch("/api/cli-tools/claude-settings", {
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/claude-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ env }),
Expand All @@ -182,7 +182,7 @@ export default function ClaudeToolCard({
setRestoring(true);
setMessage(null);
try {
const res = await fetch("/api/cli-tools/claude-settings", { method: "DELETE" });
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/claude-settings", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setMessage({ type: "success", text: "Settings reset successfully!" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function ClineToolCard({ tool, isExpanded, onToggle, baseUrl, api

const fetchModelAliases = async () => {
try {
const res = await fetch("/api/models/alias");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/models/alias");
const data = await res.json();
if (res.ok) setModelAliases(data.aliases || {});
} catch (error) {
Expand All @@ -70,7 +70,7 @@ export default function ClineToolCard({ tool, isExpanded, onToggle, baseUrl, api
const checkStatus = async () => {
setChecking(true);
try {
const res = await fetch("/api/cli-tools/cline-settings");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/cline-settings");
const data = await res.json();
setStatus(data);
} catch (error) {
Expand All @@ -88,7 +88,7 @@ export default function ClineToolCard({ tool, isExpanded, onToggle, baseUrl, api
? selectedApiKey
: (!cloudEnabled ? "sk_9router" : selectedApiKey);

const res = await fetch("/api/cli-tools/cline-settings", {
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/cline-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, model: selectedModel }),
Expand All @@ -111,7 +111,7 @@ export default function ClineToolCard({ tool, isExpanded, onToggle, baseUrl, api
setRestoring(true);
setMessage(null);
try {
const res = await fetch("/api/cli-tools/cline-settings", { method: "DELETE" });
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/cline-settings", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setMessage({ type: "success", text: "Settings reset successfully!" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api

const fetchModelAliases = async () => {
try {
const res = await fetch("/api/models/alias");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/models/alias");
const data = await res.json();
if (res.ok) setModelAliases(data.aliases || {});
} catch (error) {
Expand Down Expand Up @@ -84,7 +84,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
const checkCodexStatus = async () => {
setCheckingCodex(true);
try {
const res = await fetch("/api/cli-tools/codex-settings");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/codex-settings");
const data = await res.json();
setCodexStatus(data);
} catch (error) {
Expand All @@ -103,7 +103,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
? selectedApiKey
: (!cloudEnabled ? "sk_9router" : selectedApiKey);

const res = await fetch("/api/cli-tools/codex-settings", {
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/codex-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
Expand Down Expand Up @@ -131,7 +131,7 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
setRestoring(true);
setMessage(null);
try {
const res = await fetch("/api/cli-tools/codex-settings", { method: "DELETE" });
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/codex-settings", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setMessage({ type: "success", text: "Settings reset successfully!" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a

const fetchModelAliases = async () => {
try {
const res = await fetch("/api/models/alias");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/models/alias");
const data = await res.json();
if (res.ok) setModelAliases(data.aliases || {});
} catch (error) {
Expand All @@ -68,7 +68,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
const keyToUse = (selectedApiKey && selectedApiKey.trim())
? selectedApiKey
: (!cloudEnabled ? "sk_9router" : selectedApiKey);
await fetch("/api/cli-tools/copilot-settings", {
await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/copilot-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, models }),
Expand Down Expand Up @@ -99,7 +99,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
const checkStatus = async () => {
setChecking(true);
try {
const res = await fetch("/api/cli-tools/copilot-settings");
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/copilot-settings");
const data = await res.json();
setStatus(data);
} catch (error) {
Expand All @@ -117,7 +117,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
? selectedApiKey
: (!cloudEnabled ? "sk_9router" : selectedApiKey);

const res = await fetch("/api/cli-tools/copilot-settings", {
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/copilot-settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, models: selectedModels }),
Expand All @@ -140,7 +140,7 @@ export default function CopilotToolCard({ tool, isExpanded, onToggle, baseUrl, a
setRestoring(true);
setMessage(null);
try {
const res = await fetch("/api/cli-tools/copilot-settings", { method: "DELETE" });
const res = await fetch((process.env.NEXT_PUBLIC_BASE_PATH || "") + "/api/cli-tools/copilot-settings", { method: "DELETE" });
const data = await res.json();
if (res.ok) {
setMessage({ type: "success", text: "Settings reset successfully!" });
Expand Down
Loading