@@ -698,7 +738,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
- {stats.total > 0 && (
+ {(stats.total > 0 || usesNoAuthToggle) && (
{
e.preventDefault();
e.stopPropagation();
- onToggle(!allDisabled ? false : true);
+ handleToggleClick();
}}
>
{}}
- title={allDisabled ? "Enable provider" : "Disable provider"}
+ title={effectiveDisabled ? "Enable provider" : "Disable provider"}
/>
)}
@@ -752,15 +792,21 @@ ProviderCard.propTypes = {
name: PropTypes.string.isRequired,
color: PropTypes.string,
textIcon: PropTypes.string,
+ noAuth: PropTypes.bool,
}).isRequired,
stats: PropTypes.shape({
connected: PropTypes.number,
error: PropTypes.number,
errorCode: PropTypes.string,
errorTime: PropTypes.string,
+ total: PropTypes.number,
+ allDisabled: PropTypes.bool,
}).isRequired,
authType: PropTypes.string,
onToggle: PropTypes.func,
+ providerStrategies: PropTypes.object,
+ onToggleNoAuth: PropTypes.func,
+ isFree: PropTypes.bool,
};
function ApiKeyProviderCard({
diff --git a/src/app/api/settings/route.js b/src/app/api/settings/route.js
index 6a713433..1119b916 100644
--- a/src/app/api/settings/route.js
+++ b/src/app/api/settings/route.js
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { getSettings, updateSettings } from "@/lib/localDb";
import { applyOutboundProxyEnv } from "@/lib/network/outboundProxy";
import { resetComboRotation } from "open-sse/services/combo.js";
+import { setLogLevel } from "@/sse/utils/logger";
import bcrypt from "bcryptjs";
export const dynamic = "force-dynamic";
@@ -78,6 +79,11 @@ export async function PATCH(request) {
const settings = await updateSettings(body);
+ // Apply log level immediately (no restart required)
+ if (Object.prototype.hasOwnProperty.call(body, "logLevel")) {
+ setLogLevel(body.logLevel);
+ }
+
// Apply outbound proxy settings immediately (no restart required)
if (
Object.prototype.hasOwnProperty.call(body, "outboundProxyEnabled") ||
diff --git a/src/lib/db/repos/settingsRepo.js b/src/lib/db/repos/settingsRepo.js
index 7bae5b9f..8eb12076 100644
--- a/src/lib/db/repos/settingsRepo.js
+++ b/src/lib/db/repos/settingsRepo.js
@@ -27,6 +27,7 @@ const DEFAULT_SETTINGS = {
requireApiKey: true,
tunnelDashboardAccess: true,
authMode: "password",
+ logLevel: "info",
oidcIssuerUrl: "",
oidcClientId: "",
oidcClientSecret: "",
diff --git a/src/shared/services/initializeApp.js b/src/shared/services/initializeApp.js
index 5b27f774..6994c209 100644
--- a/src/shared/services/initializeApp.js
+++ b/src/shared/services/initializeApp.js
@@ -3,6 +3,7 @@ import { fileURLToPath } from "url";
import { dirname, join } from "path";
import { existsSync } from "fs";
import { cleanupProviderConnections, getSettings, updateSettings, getApiKeys } from "@/lib/localDb";
+import { setLogLevel } from "@/sse/utils/logger";
import {
enableTunnel, enableTailscale,
isTunnelManuallyDisabled, isTunnelReconnecting, isTailscaleReconnecting,
@@ -83,6 +84,14 @@ async function runHeavyStartup() {
await cleanupProviderConnections();
const settings = await getSettings();
+ // Apply persisted log level (dashboard Settings โ Logging) so production
+ // logs stay quiet (ERROR only) even after a restart.
+ try {
+ setLogLevel(settings.logLevel);
+ } catch (e) {
+ console.warn("[InitApp] setLogLevel failed:", e.message);
+ }
+
// Auto-resume tunnel (once per process)
if (settings.tunnelEnabled && !g.tunnelAutoResumed) {
g.tunnelAutoResumed = true;
diff --git a/src/sse/services/auth.js b/src/sse/services/auth.js
index b07db1fc..5e4a620e 100644
--- a/src/sse/services/auth.js
+++ b/src/sse/services/auth.js
@@ -41,10 +41,19 @@ export async function getProviderCredentials(provider, excludeConnectionIds = nu
// Resolve alias to provider ID (e.g., "kc" -> "kilocode")
const providerId = resolveProviderId(provider);
+ // Any free-tier provider can be toggled off via settings (enabled: false),
+ // same as disabling connections. Checked before both the noAuth branch and
+ // the normal connections branch so e.g. gemini-cli (free OAuth) can also be turned off.
+ const earlySettings = await getSettings();
+ const earlyOverride = (earlySettings.providerStrategies || {})[providerId] || {};
+ if (FREE_PROVIDERS[providerId] && earlyOverride.enabled === false) {
+ log.debug("AUTH", `${provider} | free provider disabled via settings`);
+ return null;
+ }
+
// Inject a virtual connection for no-auth free providers (with optional proxy pool from settings)
if (FREE_PROVIDERS[providerId]?.noAuth) {
- const settings = await getSettings();
- const override = (settings.providerStrategies || {})[providerId] || {};
+ const override = earlyOverride;
const strategy = override.rotateStrategy || "none";
let pickedId = override.proxyPoolId || null;
if (strategy !== "none") {
diff --git a/src/sse/utils/logger.js b/src/sse/utils/logger.js
index a9c631ed..bbabb1e1 100644
--- a/src/sse/utils/logger.js
+++ b/src/sse/utils/logger.js
@@ -7,7 +7,23 @@ const LOG_LEVELS = {
ERROR: 3
};
-const LEVEL = LOG_LEVELS[process.env.LOG_LEVEL?.toUpperCase?.()] ?? LOG_LEVELS.INFO;
+// Runtime log level. Defaults from LOG_LEVEL env, but can be changed at runtime
+// via setLogLevel (dashboard Settings โ Logging). WARN/ERROR hide the noisy
+// INFO request lines (โถ POST / ๐ DONE / [COMBO] / [CHAT] ...) in production.
+let LEVEL = LOG_LEVELS[process.env.LOG_LEVEL?.toUpperCase?.()] ?? LOG_LEVELS.INFO;
+
+export function setLogLevel(level) {
+ const normalized = String(level || "").toUpperCase();
+ if (Object.prototype.hasOwnProperty.call(LOG_LEVELS, normalized)) {
+ LEVEL = LOG_LEVELS[normalized];
+ return true;
+ }
+ return false;
+}
+
+export function getLogLevel() {
+ return Object.keys(LOG_LEVELS).find((key) => LOG_LEVELS[key] === LEVEL) || "INFO";
+}
function formatTime() {
return new Date().toLocaleTimeString("en-US", { hour12: false });
@@ -33,6 +49,7 @@ export function tagForSession(seed) {
}
// Print one correlated line: [time] tag symbol message
+// Visible at INFO and below (hidden by WARN/ERROR log levels in production).
export function line(tag, symbol, message) {
if (LEVEL > LOG_LEVELS.INFO) return;
console.log(`[${formatTime()}] ${tag} ${symbol} ${message}`);
@@ -95,17 +112,20 @@ export function error(tag, message, data) {
}
export function request(method, path, extra) {
+ if (LEVEL > LOG_LEVELS.INFO) return;
const dataStr = extra ? ` ${formatData(extra)}` : "";
console.log(`\x1b[36m[${formatTime()}] ๐ฅ ${method} ${path}${dataStr}\x1b[0m`);
}
export function response(status, duration, extra) {
+ if (LEVEL > LOG_LEVELS.INFO) return;
const icon = status < 400 ? "๐ค" : "๐ฅ";
const dataStr = extra ? ` ${formatData(extra)}` : "";
console.log(`[${formatTime()}] ${icon} ${status} (${duration}ms)${dataStr}`);
}
export function stream(event, data) {
+ if (LEVEL > LOG_LEVELS.INFO) return;
const dataStr = data ? ` ${formatData(data)}` : "";
console.log(`[${formatTime()}] ๐ [STREAM] ${event}${dataStr}`);
}