-
Claudeflare
+
ccflare
Powerful proxy for Claude Code
diff --git a/packages/dashboard-web/src/index.html b/packages/dashboard-web/src/index.html
index d66198fa..c48d7fcf 100644
--- a/packages/dashboard-web/src/index.html
+++ b/packages/dashboard-web/src/index.html
@@ -4,7 +4,7 @@
-
Claudeflare Dashboard
+
ccflare Dashboard
diff --git a/packages/dashboard-web/styles/globals.css b/packages/dashboard-web/styles/globals.css
index a3fe9597..92104dbe 100644
--- a/packages/dashboard-web/styles/globals.css
+++ b/packages/dashboard-web/styles/globals.css
@@ -11,43 +11,43 @@
--card-foreground: hsl(240 10% 3.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(240 10% 3.9%);
- --primary: hsl(24 89% 56%); /* Claudeflare Orange #F38020 */
+ --primary: hsl(24 89% 56%); /* ccflare Orange #F38020 */
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(240 4.8% 95.9%);
--secondary-foreground: hsl(240 5.9% 10%);
--muted: hsl(240 4.8% 95.9%);
--muted-foreground: hsl(240 3.8% 46.1%);
- --accent: hsl(24 89% 56%); /* Claudeflare Orange */
+ --accent: hsl(24 89% 56%); /* ccflare Orange */
--accent-foreground: hsl(0 0% 100%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(240 5.9% 90%);
--input: hsl(240 5.9% 90%);
- --ring: hsl(24 89% 56%); /* Claudeflare Orange */
+ --ring: hsl(24 89% 56%); /* ccflare Orange */
--radius: 0.375rem;
--cf-orange: #f38020;
}
.dark {
- --background: hsl(220 13% 8%); /* Dark background like Claudeflare */
+ --background: hsl(220 13% 8%); /* Dark background like ccflare */
--foreground: hsl(0 0% 95%);
--card: hsl(220 13% 12%);
--card-foreground: hsl(0 0% 95%);
--popover: hsl(220 13% 12%);
--popover-foreground: hsl(0 0% 95%);
- --primary: hsl(24 89% 56%); /* Claudeflare Orange */
+ --primary: hsl(24 89% 56%); /* ccflare Orange */
--primary-foreground: hsl(0 0% 100%);
--secondary: hsl(220 13% 18%);
--secondary-foreground: hsl(0 0% 95%);
--muted: hsl(220 13% 18%);
--muted-foreground: hsl(220 9% 55%);
- --accent: hsl(24 89% 56%); /* Claudeflare Orange */
+ --accent: hsl(24 89% 56%); /* ccflare Orange */
--accent-foreground: hsl(0 0% 100%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(0 0% 98%);
--border: hsl(220 13% 20%);
--input: hsl(220 13% 18%);
- --ring: hsl(24 89% 56%); /* Claudeflare Orange */
+ --ring: hsl(24 89% 56%); /* ccflare Orange */
--cf-orange: #f38020;
}
diff --git a/packages/database/package.json b/packages/database/package.json
index fbdf60f1..4db92d5f 100644
--- a/packages/database/package.json
+++ b/packages/database/package.json
@@ -1,5 +1,5 @@
{
- "name": "@claudeflare/database",
+ "name": "@ccflare/database",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
@@ -10,6 +10,6 @@
"typecheck": "bunx tsc --noEmit"
},
"dependencies": {
- "@claudeflare/core": "workspace:*"
+ "@ccflare/core": "workspace:*"
}
}
diff --git a/packages/database/src/async-writer.ts b/packages/database/src/async-writer.ts
index 31ee8c46..92c0fc8a 100644
--- a/packages/database/src/async-writer.ts
+++ b/packages/database/src/async-writer.ts
@@ -1,5 +1,5 @@
-import type { Disposable } from "@claudeflare/core";
-import { Logger } from "@claudeflare/logger";
+import type { Disposable } from "@ccflare/core";
+import { Logger } from "@ccflare/logger";
const logger = new Logger("async-db-writer");
diff --git a/packages/database/src/factory.ts b/packages/database/src/factory.ts
index 337b00c5..854e020e 100644
--- a/packages/database/src/factory.ts
+++ b/packages/database/src/factory.ts
@@ -1,4 +1,4 @@
-import { registerDisposable, unregisterDisposable } from "@claudeflare/core";
+import { registerDisposable, unregisterDisposable } from "@ccflare/core";
import { DatabaseOperations, type RuntimeConfig } from "./index";
let instance: DatabaseOperations | null = null;
diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts
index c323a022..06c4b8be 100644
--- a/packages/database/src/index.ts
+++ b/packages/database/src/index.ts
@@ -7,7 +7,7 @@ import {
type Disposable,
type StrategyStore,
toAccount,
-} from "@claudeflare/core";
+} from "@ccflare/core";
import { ensureSchema, runMigrations } from "./migrations";
import { resolveDbPath } from "./paths";
diff --git a/packages/database/src/paths.ts b/packages/database/src/paths.ts
index 6a058c7d..81c20600 100644
--- a/packages/database/src/paths.ts
+++ b/packages/database/src/paths.ts
@@ -1,14 +1,14 @@
import { join } from "node:path";
-import { getPlatformConfigDir } from "@claudeflare/config";
+import { getPlatformConfigDir } from "@ccflare/config";
export function resolveDbPath(): string {
// Check for explicit DB path from environment
- const explicitPath = process.env.CLAUDEFLARE_DB_PATH;
+ const explicitPath = process.env.ccflare_DB_PATH;
if (explicitPath) {
return explicitPath;
}
// Use common platform config directory
const configDir = getPlatformConfigDir();
- return join(configDir, "claudeflare.db");
+ return join(configDir, "ccflare.db");
}
diff --git a/packages/http-api/package.json b/packages/http-api/package.json
index 0956896f..827ac56a 100644
--- a/packages/http-api/package.json
+++ b/packages/http-api/package.json
@@ -1,5 +1,5 @@
{
- "name": "@claudeflare/http-api",
+ "name": "@ccflare/http-api",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
@@ -10,9 +10,9 @@
"typecheck": "bunx tsc --noEmit"
},
"dependencies": {
- "@claudeflare/core": "workspace:*",
- "@claudeflare/database": "workspace:*",
- "@claudeflare/config": "workspace:*",
- "@claudeflare/types": "workspace:*"
+ "@ccflare/core": "workspace:*",
+ "@ccflare/database": "workspace:*",
+ "@ccflare/config": "workspace:*",
+ "@ccflare/types": "workspace:*"
}
}
diff --git a/packages/http-api/src/handlers/accounts.ts b/packages/http-api/src/handlers/accounts.ts
index 80df478a..16768b6e 100644
--- a/packages/http-api/src/handlers/accounts.ts
+++ b/packages/http-api/src/handlers/accounts.ts
@@ -1,8 +1,8 @@
import type { Database } from "bun:sqlite";
-import * as cliCommands from "@claudeflare/cli-commands";
-import { Config } from "@claudeflare/config";
-import type { DatabaseOperations } from "@claudeflare/database";
-import { generatePKCE, getOAuthProvider } from "@claudeflare/providers";
+import * as cliCommands from "@ccflare/cli-commands";
+import { Config } from "@ccflare/config";
+import type { DatabaseOperations } from "@ccflare/database";
+import { generatePKCE, getOAuthProvider } from "@ccflare/providers";
import type { AccountDeleteRequest, AccountResponse } from "../types";
/**
diff --git a/packages/http-api/src/handlers/analytics.ts b/packages/http-api/src/handlers/analytics.ts
index 548b1746..e81b3e50 100644
--- a/packages/http-api/src/handlers/analytics.ts
+++ b/packages/http-api/src/handlers/analytics.ts
@@ -1,4 +1,4 @@
-import { NO_ACCOUNT_ID } from "@claudeflare/core";
+import { NO_ACCOUNT_ID } from "@ccflare/core";
import type { AnalyticsResponse, APIContext } from "../types";
interface BucketConfig {
diff --git a/packages/http-api/src/handlers/config.ts b/packages/http-api/src/handlers/config.ts
index 25365473..69c0b19e 100644
--- a/packages/http-api/src/handlers/config.ts
+++ b/packages/http-api/src/handlers/config.ts
@@ -1,5 +1,5 @@
-import type { Config } from "@claudeflare/config";
-import { isValidStrategy, STRATEGIES } from "@claudeflare/core";
+import type { Config } from "@ccflare/config";
+import { isValidStrategy, STRATEGIES } from "@ccflare/core";
import type { ConfigResponse, StrategyUpdateRequest } from "../types";
import { BadRequest, jsonResponse } from "../utils/http-error";
diff --git a/packages/http-api/src/handlers/health.ts b/packages/http-api/src/handlers/health.ts
index 6a9eb104..d0e8b44e 100644
--- a/packages/http-api/src/handlers/health.ts
+++ b/packages/http-api/src/handlers/health.ts
@@ -1,5 +1,5 @@
import type { Database } from "bun:sqlite";
-import type { Config } from "@claudeflare/config";
+import type { Config } from "@ccflare/config";
import type { HealthResponse } from "../types";
/**
diff --git a/packages/http-api/src/handlers/logs-history.ts b/packages/http-api/src/handlers/logs-history.ts
index 385e8e2e..85fd6650 100644
--- a/packages/http-api/src/handlers/logs-history.ts
+++ b/packages/http-api/src/handlers/logs-history.ts
@@ -1,4 +1,4 @@
-import { logFileWriter } from "@claudeflare/logger";
+import { logFileWriter } from "@ccflare/logger";
/**
* Create a logs history handler to fetch past logs
diff --git a/packages/http-api/src/handlers/logs.ts b/packages/http-api/src/handlers/logs.ts
index 37395a70..4102411c 100644
--- a/packages/http-api/src/handlers/logs.ts
+++ b/packages/http-api/src/handlers/logs.ts
@@ -1,5 +1,5 @@
-import { logBus } from "@claudeflare/logger";
-import type { LogEvent } from "@claudeflare/types";
+import { logBus } from "@ccflare/logger";
+import type { LogEvent } from "@ccflare/types";
/**
* Create a logs stream handler using Server-Sent Events
diff --git a/packages/http-api/src/handlers/requests.ts b/packages/http-api/src/handlers/requests.ts
index d2e8d501..930b6ecf 100644
--- a/packages/http-api/src/handlers/requests.ts
+++ b/packages/http-api/src/handlers/requests.ts
@@ -1,5 +1,5 @@
import type { Database } from "bun:sqlite";
-import type { DatabaseOperations } from "@claudeflare/database";
+import type { DatabaseOperations } from "@ccflare/database";
import type { RequestResponse } from "../types";
/**
diff --git a/packages/http-api/src/handlers/stats.ts b/packages/http-api/src/handlers/stats.ts
index 98696d3c..45736137 100644
--- a/packages/http-api/src/handlers/stats.ts
+++ b/packages/http-api/src/handlers/stats.ts
@@ -1,6 +1,6 @@
import type { Database } from "bun:sqlite";
-import { NO_ACCOUNT_ID } from "@claudeflare/core";
-import type { DatabaseOperations } from "@claudeflare/database";
+import { NO_ACCOUNT_ID } from "@ccflare/core";
+import type { DatabaseOperations } from "@ccflare/database";
import { jsonResponse } from "../utils/http-error";
/**
diff --git a/packages/http-api/src/types.ts b/packages/http-api/src/types.ts
index 629623cc..fd83121b 100644
--- a/packages/http-api/src/types.ts
+++ b/packages/http-api/src/types.ts
@@ -1,6 +1,6 @@
import type { Database } from "bun:sqlite";
-import type { Config } from "@claudeflare/config";
-import type { DatabaseOperations } from "@claudeflare/database";
+import type { Config } from "@ccflare/config";
+import type { DatabaseOperations } from "@ccflare/database";
export interface APIContext {
db: Database;
diff --git a/packages/load-balancer/package.json b/packages/load-balancer/package.json
index da61067c..e9b9f12f 100644
--- a/packages/load-balancer/package.json
+++ b/packages/load-balancer/package.json
@@ -1,5 +1,5 @@
{
- "name": "@claudeflare/load-balancer",
+ "name": "@ccflare/load-balancer",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
@@ -10,8 +10,8 @@
"typecheck": "bunx tsc --noEmit"
},
"dependencies": {
- "@claudeflare/core": "workspace:*",
- "@claudeflare/database": "workspace:*",
- "@claudeflare/logger": "workspace:*"
+ "@ccflare/core": "workspace:*",
+ "@ccflare/database": "workspace:*",
+ "@ccflare/logger": "workspace:*"
}
}
diff --git a/packages/load-balancer/src/strategies/index.ts b/packages/load-balancer/src/strategies/index.ts
index d669147a..305d631a 100644
--- a/packages/load-balancer/src/strategies/index.ts
+++ b/packages/load-balancer/src/strategies/index.ts
@@ -3,9 +3,9 @@ import type {
LoadBalancingStrategy,
RequestMeta,
StrategyStore,
-} from "@claudeflare/core";
-import { isAccountAvailable } from "@claudeflare/core";
-import { Logger } from "@claudeflare/logger";
+} from "@ccflare/core";
+import { isAccountAvailable } from "@ccflare/core";
+import { Logger } from "@ccflare/logger";
export class SessionStrategy implements LoadBalancingStrategy {
private sessionDurationMs: number;
diff --git a/packages/logger/package.json b/packages/logger/package.json
index 246d16a3..0441ec4b 100644
--- a/packages/logger/package.json
+++ b/packages/logger/package.json
@@ -1,5 +1,5 @@
{
- "name": "@claudeflare/logger",
+ "name": "@ccflare/logger",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
@@ -10,7 +10,7 @@
"typecheck": "bunx tsc --noEmit"
},
"dependencies": {
- "@claudeflare/core": "workspace:*",
- "@claudeflare/types": "workspace:*"
+ "@ccflare/core": "workspace:*",
+ "@ccflare/types": "workspace:*"
}
}
diff --git a/packages/logger/src/file-writer.ts b/packages/logger/src/file-writer.ts
index 0fa67d7c..36463b90 100644
--- a/packages/logger/src/file-writer.ts
+++ b/packages/logger/src/file-writer.ts
@@ -1,8 +1,8 @@
import { createWriteStream, existsSync, mkdirSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
-import { type Disposable, registerDisposable } from "@claudeflare/core";
-import type { LogEvent } from "@claudeflare/types";
+import { type Disposable, registerDisposable } from "@ccflare/core";
+import type { LogEvent } from "@ccflare/types";
export class LogFileWriter implements Disposable {
private logDir: string;
@@ -12,7 +12,7 @@ export class LogFileWriter implements Disposable {
constructor() {
// Create log directory in tmp folder
- this.logDir = join(tmpdir(), "claudeflare-logs");
+ this.logDir = join(tmpdir(), "ccflare-logs");
if (!existsSync(this.logDir)) {
mkdirSync(this.logDir, { recursive: true });
}
diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts
index a005c1b0..b154098a 100644
--- a/packages/logger/src/index.ts
+++ b/packages/logger/src/index.ts
@@ -1,5 +1,5 @@
import { EventEmitter } from "node:events";
-import type { LogEvent } from "@claudeflare/types";
+import type { LogEvent } from "@ccflare/types";
import { logFileWriter } from "./file-writer";
export enum LogLevel {
@@ -24,9 +24,9 @@ export class Logger {
this.prefix = prefix;
this.level = this.getLogLevelFromEnv() || level;
this.format = (process.env.LOG_FORMAT as LogFormat) || "pretty";
- // Only show console output in debug mode or if CLAUDEFLARE_DEBUG is set
+ // Only show console output in debug mode or if ccflare_DEBUG is set
this.silentConsole = !(
- process.env.CLAUDEFLARE_DEBUG === "1" || this.level === LogLevel.DEBUG
+ process.env.ccflare_DEBUG === "1" || this.level === LogLevel.DEBUG
);
}
@@ -122,7 +122,7 @@ export class Logger {
this.level = level;
// Update silentConsole when level changes
this.silentConsole = !(
- process.env.CLAUDEFLARE_DEBUG === "1" || this.level === LogLevel.DEBUG
+ process.env.ccflare_DEBUG === "1" || this.level === LogLevel.DEBUG
);
}
diff --git a/packages/providers/package.json b/packages/providers/package.json
index 37f60195..e47b2c27 100644
--- a/packages/providers/package.json
+++ b/packages/providers/package.json
@@ -1,5 +1,5 @@
{
- "name": "@claudeflare/providers",
+ "name": "@ccflare/providers",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
@@ -10,6 +10,6 @@
"typecheck": "bunx tsc --noEmit"
},
"dependencies": {
- "@claudeflare/core": "workspace:*"
+ "@ccflare/core": "workspace:*"
}
}
diff --git a/packages/providers/src/base.ts b/packages/providers/src/base.ts
index eca40a0b..117a7aef 100644
--- a/packages/providers/src/base.ts
+++ b/packages/providers/src/base.ts
@@ -1,4 +1,4 @@
-import type { Account } from "@claudeflare/core";
+import type { Account } from "@ccflare/core";
import type { Provider, RateLimitInfo, TokenRefreshResult } from "./types";
export abstract class BaseProvider implements Provider {
diff --git a/packages/providers/src/providers/anthropic/provider.ts b/packages/providers/src/providers/anthropic/provider.ts
index d6428241..d0d16116 100644
--- a/packages/providers/src/providers/anthropic/provider.ts
+++ b/packages/providers/src/providers/anthropic/provider.ts
@@ -1,5 +1,5 @@
-import type { Account } from "@claudeflare/core";
-import { Logger } from "@claudeflare/logger";
+import type { Account } from "@ccflare/core";
+import { Logger } from "@ccflare/logger";
import { BaseProvider } from "../../base";
import type { RateLimitInfo, TokenRefreshResult } from "../../types";
diff --git a/packages/providers/src/types.ts b/packages/providers/src/types.ts
index 95f3debe..3356b4fd 100644
--- a/packages/providers/src/types.ts
+++ b/packages/providers/src/types.ts
@@ -1,4 +1,4 @@
-import type { Account } from "@claudeflare/core";
+import type { Account } from "@ccflare/core";
export interface TokenRefreshResult {
accessToken: string;
diff --git a/packages/proxy/package.json b/packages/proxy/package.json
index baaed91b..2af08672 100644
--- a/packages/proxy/package.json
+++ b/packages/proxy/package.json
@@ -1,5 +1,5 @@
{
- "name": "@claudeflare/proxy",
+ "name": "@ccflare/proxy",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
@@ -10,10 +10,10 @@
"typecheck": "bunx tsc --noEmit"
},
"dependencies": {
- "@claudeflare/core": "workspace:*",
- "@claudeflare/database": "workspace:*",
- "@claudeflare/config": "workspace:*",
- "@claudeflare/logger": "workspace:*",
- "@claudeflare/providers": "workspace:*"
+ "@ccflare/core": "workspace:*",
+ "@ccflare/database": "workspace:*",
+ "@ccflare/config": "workspace:*",
+ "@ccflare/logger": "workspace:*",
+ "@ccflare/providers": "workspace:*"
}
}
diff --git a/packages/proxy/src/index.ts b/packages/proxy/src/index.ts
index f7b6d6f6..30dcf73f 100644
--- a/packages/proxy/src/index.ts
+++ b/packages/proxy/src/index.ts
@@ -1,14 +1,14 @@
-// Re-export provider-related types and functions from @claudeflare/providers
+// Re-export provider-related types and functions from @ccflare/providers
export type {
Provider,
RateLimitInfo,
TokenRefreshResult,
-} from "@claudeflare/providers";
+} from "@ccflare/providers";
export {
getProvider,
listProviders,
registerProvider,
-} from "@claudeflare/providers";
+} from "@ccflare/providers";
export {
getUsageWorker,
handleProxy,
diff --git a/packages/proxy/src/post-processor.worker.ts b/packages/proxy/src/post-processor.worker.ts
index 17a6b258..1a430a69 100644
--- a/packages/proxy/src/post-processor.worker.ts
+++ b/packages/proxy/src/post-processor.worker.ts
@@ -1,8 +1,8 @@
declare var self: Worker;
-import { estimateCostUSD, NO_ACCOUNT_ID } from "@claudeflare/core";
-import { AsyncDbWriter, DatabaseOperations } from "@claudeflare/database";
-import { Logger } from "@claudeflare/logger";
+import { estimateCostUSD, NO_ACCOUNT_ID } from "@ccflare/core";
+import { AsyncDbWriter, DatabaseOperations } from "@ccflare/database";
+import { Logger } from "@ccflare/logger";
import { combineChunks } from "./stream-tee";
import type {
ChunkMessage,
diff --git a/packages/proxy/src/proxy.ts b/packages/proxy/src/proxy.ts
index 57a9e414..e4763fea 100644
--- a/packages/proxy/src/proxy.ts
+++ b/packages/proxy/src/proxy.ts
@@ -1,13 +1,13 @@
import crypto from "node:crypto";
-import type { RuntimeConfig } from "@claudeflare/config";
+import type { RuntimeConfig } from "@ccflare/config";
import type {
Account,
LoadBalancingStrategy,
RequestMeta,
-} from "@claudeflare/core";
-import type { AsyncDbWriter, DatabaseOperations } from "@claudeflare/database";
-import { Logger } from "@claudeflare/logger";
-import type { Provider, TokenRefreshResult } from "@claudeflare/providers";
+} from "@ccflare/core";
+import type { AsyncDbWriter, DatabaseOperations } from "@ccflare/database";
+import { Logger } from "@ccflare/logger";
+import type { Provider, TokenRefreshResult } from "@ccflare/providers";
import { forwardToClient } from "./response-handler";
import type { ControlMessage } from "./worker-messages";
diff --git a/packages/proxy/src/response-handler.ts b/packages/proxy/src/response-handler.ts
index f31d8f3f..7c1e6277 100644
--- a/packages/proxy/src/response-handler.ts
+++ b/packages/proxy/src/response-handler.ts
@@ -1,4 +1,4 @@
-import type { Account } from "@claudeflare/core";
+import type { Account } from "@ccflare/core";
import type { ProxyContext } from "./proxy";
import type { ChunkMessage, EndMessage, StartMessage } from "./worker-messages";
diff --git a/packages/tui-core/package.json b/packages/tui-core/package.json
index 67f7da5a..08eeadff 100644
--- a/packages/tui-core/package.json
+++ b/packages/tui-core/package.json
@@ -1,7 +1,7 @@
{
- "name": "@claudeflare/tui-core",
+ "name": "@ccflare/tui-core",
"version": "1.0.0",
- "description": "Core controller logic for Claudeflare TUI",
+ "description": "Core controller logic for ccflare TUI",
"type": "module",
"main": "./src/index.ts",
"exports": {
@@ -11,11 +11,11 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@claudeflare/cli-commands": "workspace:*",
- "@claudeflare/database": "workspace:*",
- "@claudeflare/logger": "workspace:*",
- "@claudeflare/core": "workspace:*",
- "@claudeflare/types": "workspace:*"
+ "@ccflare/cli-commands": "workspace:*",
+ "@ccflare/database": "workspace:*",
+ "@ccflare/logger": "workspace:*",
+ "@ccflare/core": "workspace:*",
+ "@ccflare/types": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.0.0"
diff --git a/packages/tui-core/src/accounts.ts b/packages/tui-core/src/accounts.ts
index 2efdd87f..5019b390 100644
--- a/packages/tui-core/src/accounts.ts
+++ b/packages/tui-core/src/accounts.ts
@@ -1,13 +1,13 @@
-import type { AccountListItem } from "@claudeflare/cli-commands";
-import * as cliCommands from "@claudeflare/cli-commands";
-import { openBrowser } from "@claudeflare/cli-commands";
-import { Config } from "@claudeflare/config";
-import { DatabaseFactory } from "@claudeflare/database";
+import type { AccountListItem } from "@ccflare/cli-commands";
+import * as cliCommands from "@ccflare/cli-commands";
+import { openBrowser } from "@ccflare/cli-commands";
+import { Config } from "@ccflare/config";
+import { DatabaseFactory } from "@ccflare/database";
import {
generatePKCE,
getOAuthProvider,
type OAuthConfig,
-} from "@claudeflare/providers";
+} from "@ccflare/providers";
export interface AddAccountOptions {
name: string;
diff --git a/packages/tui-core/src/logs.ts b/packages/tui-core/src/logs.ts
index 0068fe1d..79799e3c 100644
--- a/packages/tui-core/src/logs.ts
+++ b/packages/tui-core/src/logs.ts
@@ -1,5 +1,5 @@
-import { logBus, logFileWriter } from "@claudeflare/logger";
-import type { LogEvent } from "@claudeflare/types";
+import { logBus, logFileWriter } from "@ccflare/logger";
+import type { LogEvent } from "@ccflare/types";
export function streamLogs(callback: (log: LogEvent) => void): () => void {
const listener = (event: LogEvent) => {
diff --git a/packages/tui-core/src/requests.ts b/packages/tui-core/src/requests.ts
index 01caeb53..1d02a247 100644
--- a/packages/tui-core/src/requests.ts
+++ b/packages/tui-core/src/requests.ts
@@ -1,4 +1,4 @@
-import { DatabaseFactory } from "@claudeflare/database";
+import { DatabaseFactory } from "@ccflare/database";
export interface RequestPayload {
id: string;
diff --git a/packages/tui-core/src/stats.ts b/packages/tui-core/src/stats.ts
index 33d23246..54cb2a81 100644
--- a/packages/tui-core/src/stats.ts
+++ b/packages/tui-core/src/stats.ts
@@ -1,4 +1,4 @@
-import { DatabaseFactory } from "@claudeflare/database";
+import { DatabaseFactory } from "@ccflare/database";
export interface Stats {
totalRequests: number;
diff --git a/packages/tui-core/src/strategy.ts b/packages/tui-core/src/strategy.ts
index f4f4e2a7..1a501b82 100644
--- a/packages/tui-core/src/strategy.ts
+++ b/packages/tui-core/src/strategy.ts
@@ -1,4 +1,4 @@
-import { Config } from "@claudeflare/config";
+import { Config } from "@ccflare/config";
async function getPort(): Promise
{
const config = new Config();
diff --git a/packages/tui-core/src/tui-adapter.ts b/packages/tui-core/src/tui-adapter.ts
index dfa41f7b..1bb5156a 100644
--- a/packages/tui-core/src/tui-adapter.ts
+++ b/packages/tui-core/src/tui-adapter.ts
@@ -1,4 +1,4 @@
-import type { PromptAdapter } from "@claudeflare/cli-commands";
+import type { PromptAdapter } from "@ccflare/cli-commands";
/**
* Special error thrown when TUI needs to collect authorization code
diff --git a/packages/types/package.json b/packages/types/package.json
index 5e8adc6e..73be6f07 100644
--- a/packages/types/package.json
+++ b/packages/types/package.json
@@ -1,5 +1,5 @@
{
- "name": "@claudeflare/types",
+ "name": "@ccflare/types",
"version": "0.1.0",
"type": "module",
"main": "./src/index.ts",
diff --git a/tsconfig.json b/tsconfig.json
index 0402a37b..37349498 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -16,7 +16,7 @@
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
- "@claudeflare/*": ["packages/*/src"]
+ "@ccflare/*": ["packages/*/src"]
}
},
"include": ["packages/*/src/**/*", "apps/*/src/**/*"]
From f8968e26e8925bfa5a9e7cac06257969622f437a Mon Sep 17 00:00:00 2001
From: snipeship
Date: Mon, 28 Jul 2025 17:46:47 -0300
Subject: [PATCH 04/17] docs(package.json): update project description
Revises the description in package.json for improved clarity and
marketing, changing it from "Claude load balancer proxy" to
"Ultimate CC Proxy". Enhances readability and better reflects the
project's purpose.
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index b4c6a5ec..c7a7b39d 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "ccflare",
"version": "0.0.2",
- "description": "Claude load balancer proxy",
+ "description": "Ultimate CC Proxy",
"author": "",
"license": "MIT",
"main": "index.js",
From de65bd8d9dcaaa1c06816557242af552dee1566e Mon Sep 17 00:00:00 2001
From: Reese
Date: Tue, 29 Jul 2025 14:07:03 +0100
Subject: [PATCH 05/17] Dockerfile for building
---
Dockerfile | 42 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 Dockerfile
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..41458dfd
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,42 @@
+# Multi-stage build for ccflare
+FROM oven/bun:1-alpine AS builder
+
+WORKDIR /app
+
+# Copy package files
+COPY package.json bun.lock* ./
+COPY apps/*/package.json ./apps/*/
+COPY packages/*/package.json ./packages/*/
+
+# Install dependencies
+RUN bun install --frozen-lockfile
+
+# Copy source code
+COPY . .
+
+# Build the project
+RUN bun run build
+
+# Production stage
+FROM oven/bun:1-alpine AS runner
+
+WORKDIR /app
+
+# Create non-root user
+RUN addgroup -g 1001 -S ccflare && \
+ adduser -S ccflare -u 1001
+
+# Copy built application
+COPY --from=builder --chown=ccflare:ccflare /app .
+
+# Create data directory for SQLite database
+RUN mkdir -p /app/data && chown ccflare:ccflare /app/data
+
+# Switch to non-root user
+USER ccflare
+
+# Expose port
+EXPOSE 8080
+
+# Start the server (not TUI)
+CMD ["bun", "run", "server"]
\ No newline at end of file
From 423208ee91bc326bfecb12e20c1c1fffc24a412b Mon Sep 17 00:00:00 2001
From: Reese
Date: Tue, 29 Jul 2025 17:55:03 +0100
Subject: [PATCH 06/17] Docker build fixes
---
Dockerfile | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 41458dfd..0ccc83f4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,17 +3,15 @@ FROM oven/bun:1-alpine AS builder
WORKDIR /app
-# Copy package files
+# Copy package files for dependency caching
COPY package.json bun.lock* ./
-COPY apps/*/package.json ./apps/*/
-COPY packages/*/package.json ./packages/*/
+
+# Copy all source code (required for workspace dependencies)
+COPY . .
# Install dependencies
RUN bun install --frozen-lockfile
-# Copy source code
-COPY . .
-
# Build the project
RUN bun run build
From 12e49dc3ef58f1a8023cff45b595eaa628200d52 Mon Sep 17 00:00:00 2001
From: Reese
Date: Tue, 29 Jul 2025 18:40:52 +0100
Subject: [PATCH 07/17] Dockerfile: update DB path
---
Dockerfile | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Dockerfile b/Dockerfile
index 0ccc83f4..2129644f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -32,6 +32,8 @@ RUN mkdir -p /app/data && chown ccflare:ccflare /app/data
# Switch to non-root user
USER ccflare
+# Set database path to persistent volume mount
+ENV ccflare_DB_PATH=/app/data/ccflare.db
# Expose port
EXPOSE 8080
From cbe4cf6094f41d32021b3d25bd6e13df46495b9b Mon Sep 17 00:00:00 2001
From: Reese
Date: Tue, 29 Jul 2025 19:00:34 +0100
Subject: [PATCH 08/17] Remote deploy: basic key auth
---
Dockerfile | 4 ++
README.md | 28 +++++++++++++-
apps/server/src/server.ts | 22 ++++++++---
deploy/k8-yaml/k8s-deployment.yaml | 59 ++++++++++++++++++++++++++++++
4 files changed, 107 insertions(+), 6 deletions(-)
create mode 100644 deploy/k8-yaml/k8s-deployment.yaml
diff --git a/Dockerfile b/Dockerfile
index 2129644f..46cd6c70 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -32,6 +32,10 @@ RUN mkdir -p /app/data && chown ccflare:ccflare /app/data
# Switch to non-root user
USER ccflare
+
+# Set API key for authentication (change this in production!)
+ENV API_KEY=ccflare-default-key
+
# Set database path to persistent volume mount
ENV ccflare_DB_PATH=/app/data/ccflare.db
diff --git a/README.md b/README.md
index 6204000e..61951099 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,11 @@ bun install
# Start ccflare (TUI + Server)
bun run ccflare
-# Configure Claude SDK
+# Configure Claude SDK (local development)
export ANTHROPIC_BASE_URL=http://localhost:8080
+
+# Windows
+$env:ANTHROPIC_BASE_URL="http://localhost:8080"
```
## Features
@@ -47,11 +50,34 @@ export ANTHROPIC_BASE_URL=http://localhost:8080
- REST API for automation
### 🔒 Production Ready
+- Optional API key authentication for remote hosting
- Automatic failover between accounts
- OAuth token refresh handling
- SQLite database for persistence
- Configurable retry logic
+## Security & Remote Hosting
+
+### Local Development (No Authentication)
+```bash
+# No API key needed for localhost
+export ANTHROPIC_BASE_URL=http://localhost:8080
+```
+
+### Remote/Production Deployment (With Authentication)
+```bash
+# Set API key for security
+export API_KEY=your-secret-key-here
+
+# Configure clients to use authenticated endpoint
+export ANTHROPIC_BASE_URL=http://yourserver.com/your-secret-key-here
+```
+
+**Docker Example:**
+```bash
+docker run -e API_KEY=my-secret-key -p 8080:8080 ccflare
+```
+
## Documentation
Full documentation available in [`docs/`](docs/):
diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts
index 6d042608..f2f6d0f6 100644
--- a/apps/server/src/server.ts
+++ b/apps/server/src/server.ts
@@ -171,13 +171,25 @@ const server = serve({
}
}
- // Only proxy requests to Anthropic API
- if (!url.pathname.startsWith("/v1/")) {
+ // Handle API authentication and proxying
+ const apiKey = process.env.API_KEY;
+
+ if (apiKey) {
+ // Auth required - check for /key/v1/ format
+ const pathParts = url.pathname.split('/').filter(Boolean);
+ if (pathParts[0] === apiKey && pathParts[1] === 'v1') {
+ // Valid auth - rewrite path and proxy
+ url.pathname = '/' + pathParts.slice(1).join('/');
+ return handleProxy(req, url, proxyContext);
+ }
return new Response("Not Found", { status: 404 });
+ } else {
+ // No auth required - allow direct /v1/ access
+ if (!url.pathname.startsWith("/v1/")) {
+ return new Response("Not Found", { status: 404 });
+ }
+ return handleProxy(req, url, proxyContext);
}
-
- // Handle proxy request
- return handleProxy(req, url, proxyContext);
},
});
diff --git a/deploy/k8-yaml/k8s-deployment.yaml b/deploy/k8-yaml/k8s-deployment.yaml
new file mode 100644
index 00000000..f7fc9562
--- /dev/null
+++ b/deploy/k8-yaml/k8s-deployment.yaml
@@ -0,0 +1,59 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: ccflare-data
+ namespace: coder
+spec:
+ accessModes:
+ - ReadWriteMany
+ resources:
+ requests:
+ storage: 10Gi
+ storageClassName: ceph-filesystem
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: ccflare
+ namespace: coder
+ labels:
+ app: ccflare
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: ccflare
+ template:
+ metadata:
+ labels:
+ app: ccflare
+ spec:
+ securityContext:
+ runAsUser: 1001
+ runAsGroup: 1001
+ fsGroup: 1001
+ containers:
+ - name: ccflare
+ image: 192.168.96.61:30009/library/ccflare-fork:latest
+ ports:
+ - containerPort: 8080
+ volumeMounts:
+ - name: ccflare-data
+ mountPath: /app/data
+ volumes:
+ - name: ccflare-data
+ persistentVolumeClaim:
+ claimName: ccflare-data
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: ccflare-service
+ namespace: coder
+spec:
+ selector:
+ app: ccflare
+ ports:
+ - port: 8080
+ targetPort: 8080
+ type: ClusterIP
\ No newline at end of file
From 17e1462973257af1b30b5e6718a9d333a6ca450e Mon Sep 17 00:00:00 2001
From: Reese
Date: Tue, 29 Jul 2025 19:45:54 +0100
Subject: [PATCH 09/17] feat: implement SQLite retry mechanism and distributed
filesystem optimizations
- Add comprehensive retry logic with exponential backoff for SQLITE_BUSY errors
- Configure SQLite with WAL mode, busy timeout, and optimizations for Rook Ceph
- Extend configuration system to support database-specific settings
- Apply retry logic to critical read operations (getAllAccounts, getAccount, etc.)
- Add retry support to key write operations (updateAccountTokens, markAccountRateLimited)
- Maintain backward compatibility with existing async writer and proxy systems
Resolves database locking issues causing 'loading requests' page hangs on distributed storage
---
packages/config/src/index.ts | 65 ++++++
packages/database/src/factory.ts | 20 +-
packages/database/src/index.ts | 352 +++++++++++++++++++++----------
packages/database/src/retry.ts | 171 +++++++++++++++
4 files changed, 496 insertions(+), 112 deletions(-)
create mode 100644 packages/database/src/retry.ts
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
index 1565ec2a..e53345fa 100644
--- a/packages/config/src/index.ts
+++ b/packages/config/src/index.ts
@@ -13,6 +13,19 @@ export interface RuntimeConfig {
retry: { attempts: number; delayMs: number; backoff: number };
sessionDurationMs: number;
port: number;
+ database?: {
+ walMode?: boolean;
+ busyTimeoutMs?: number;
+ cacheSize?: number;
+ synchronous?: 'OFF' | 'NORMAL' | 'FULL';
+ mmapSize?: number;
+ retry?: {
+ attempts?: number;
+ delayMs?: number;
+ backoff?: number;
+ maxDelayMs?: number;
+ };
+ };
}
export interface ConfigData {
@@ -23,6 +36,16 @@ export interface ConfigData {
retry_backoff?: number;
session_duration_ms?: number;
port?: number;
+ // Database configuration
+ db_wal_mode?: boolean;
+ db_busy_timeout_ms?: number;
+ db_cache_size?: number;
+ db_synchronous?: 'OFF' | 'NORMAL' | 'FULL';
+ db_mmap_size?: number;
+ db_retry_attempts?: number;
+ db_retry_delay_ms?: number;
+ db_retry_backoff?: number;
+ db_retry_max_delay_ms?: number;
[key: string]: string | number | boolean | undefined;
}
@@ -134,6 +157,19 @@ export class Config extends EventEmitter {
},
sessionDurationMs: 5 * 60 * 60 * 1000, // 5 hours
port: 8080,
+ database: {
+ walMode: true,
+ busyTimeoutMs: 5000,
+ cacheSize: -20000, // 20MB cache
+ synchronous: 'NORMAL',
+ mmapSize: 268435456, // 256MB
+ retry: {
+ attempts: 3,
+ delayMs: 100,
+ backoff: 2,
+ maxDelayMs: 5000,
+ },
+ },
};
// Override with environment variables if present
@@ -176,6 +212,35 @@ export class Config extends EventEmitter {
defaults.port = this.data.port;
}
+ // Database configuration overrides
+ if (typeof this.data.db_wal_mode === "boolean") {
+ defaults.database!.walMode = this.data.db_wal_mode;
+ }
+ if (typeof this.data.db_busy_timeout_ms === "number") {
+ defaults.database!.busyTimeoutMs = this.data.db_busy_timeout_ms;
+ }
+ if (typeof this.data.db_cache_size === "number") {
+ defaults.database!.cacheSize = this.data.db_cache_size;
+ }
+ if (typeof this.data.db_synchronous === "string") {
+ defaults.database!.synchronous = this.data.db_synchronous as 'OFF' | 'NORMAL' | 'FULL';
+ }
+ if (typeof this.data.db_mmap_size === "number") {
+ defaults.database!.mmapSize = this.data.db_mmap_size;
+ }
+ if (typeof this.data.db_retry_attempts === "number") {
+ defaults.database!.retry!.attempts = this.data.db_retry_attempts;
+ }
+ if (typeof this.data.db_retry_delay_ms === "number") {
+ defaults.database!.retry!.delayMs = this.data.db_retry_delay_ms;
+ }
+ if (typeof this.data.db_retry_backoff === "number") {
+ defaults.database!.retry!.backoff = this.data.db_retry_backoff;
+ }
+ if (typeof this.data.db_retry_max_delay_ms === "number") {
+ defaults.database!.retry!.maxDelayMs = this.data.db_retry_max_delay_ms;
+ }
+
return defaults;
}
}
diff --git a/packages/database/src/factory.ts b/packages/database/src/factory.ts
index 854e020e..71229454 100644
--- a/packages/database/src/factory.ts
+++ b/packages/database/src/factory.ts
@@ -1,13 +1,14 @@
import { registerDisposable, unregisterDisposable } from "@ccflare/core";
-import { DatabaseOperations, type RuntimeConfig } from "./index";
+import type { RuntimeConfig as ConfigRuntimeConfig } from "@ccflare/config";
+import { DatabaseOperations, type DatabaseConfig, type DatabaseRetryConfig } from "./index";
let instance: DatabaseOperations | null = null;
let dbPath: string | undefined;
-let runtimeConfig: RuntimeConfig | undefined;
+let runtimeConfig: ConfigRuntimeConfig | undefined;
export function initialize(
dbPathParam?: string,
- runtimeConfigParam?: RuntimeConfig,
+ runtimeConfigParam?: ConfigRuntimeConfig,
): void {
dbPath = dbPathParam;
runtimeConfig = runtimeConfigParam;
@@ -15,7 +16,18 @@ export function initialize(
export function getInstance(): DatabaseOperations {
if (!instance) {
- instance = new DatabaseOperations(dbPath);
+ // Extract database configuration from runtime config
+ const dbConfig: DatabaseConfig | undefined = runtimeConfig?.database ? {
+ walMode: runtimeConfig.database.walMode,
+ busyTimeoutMs: runtimeConfig.database.busyTimeoutMs,
+ cacheSize: runtimeConfig.database.cacheSize,
+ synchronous: runtimeConfig.database.synchronous,
+ mmapSize: runtimeConfig.database.mmapSize,
+ } : undefined;
+
+ const retryConfig: DatabaseRetryConfig | undefined = runtimeConfig?.database?.retry;
+
+ instance = new DatabaseOperations(dbPath, dbConfig, retryConfig);
if (runtimeConfig) {
instance.setRuntimeConfig(runtimeConfig);
}
diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts
index 06c4b8be..963011c4 100644
--- a/packages/database/src/index.ts
+++ b/packages/database/src/index.ts
@@ -8,65 +8,187 @@ import {
type StrategyStore,
toAccount,
} from "@ccflare/core";
+import type { RuntimeConfig as ConfigRuntimeConfig } from "@ccflare/config";
import { ensureSchema, runMigrations } from "./migrations";
import { resolveDbPath } from "./paths";
+import { withDatabaseRetry, withDatabaseRetrySync } from "./retry";
+
+/**
+ * Apply SQLite pragmas for optimal performance on distributed filesystems
+ */
+function configureSqlite(db: Database, config: DatabaseConfig): void {
+ // Enable WAL mode for better concurrency
+ if (config.walMode !== false) {
+ db.run("PRAGMA journal_mode = WAL");
+ }
+
+ // Set busy timeout for lock handling
+ if (config.busyTimeoutMs !== undefined) {
+ db.run(`PRAGMA busy_timeout = ${config.busyTimeoutMs}`);
+ }
+
+ // Configure cache size
+ if (config.cacheSize !== undefined) {
+ db.run(`PRAGMA cache_size = ${config.cacheSize}`);
+ }
+
+ // Set synchronous mode
+ if (config.synchronous !== undefined) {
+ db.run(`PRAGMA synchronous = ${config.synchronous}`);
+ }
+
+ // Configure memory-mapped I/O
+ if (config.mmapSize !== undefined) {
+ db.run(`PRAGMA mmap_size = ${config.mmapSize}`);
+ }
+
+ // Additional optimizations for distributed filesystems
+ db.run("PRAGMA temp_store = MEMORY");
+ db.run("PRAGMA foreign_keys = ON");
+}
export interface RuntimeConfig {
sessionDurationMs?: number;
}
+export interface DatabaseConfig {
+ /** Enable WAL (Write-Ahead Logging) mode for better concurrency */
+ walMode?: boolean;
+ /** SQLite busy timeout in milliseconds */
+ busyTimeoutMs?: number;
+ /** Cache size in pages (negative value = KB) */
+ cacheSize?: number;
+ /** Synchronous mode: OFF, NORMAL, FULL */
+ synchronous?: 'OFF' | 'NORMAL' | 'FULL';
+ /** Memory-mapped I/O size in bytes */
+ mmapSize?: number;
+}
+
+export interface DatabaseRetryConfig {
+ /** Maximum number of retry attempts for database operations */
+ attempts?: number;
+ /** Initial delay between retries in milliseconds */
+ delayMs?: number;
+ /** Backoff multiplier for exponential backoff */
+ backoff?: number;
+ /** Maximum delay between retries in milliseconds */
+ maxDelayMs?: number;
+}
+
export class DatabaseOperations implements StrategyStore, Disposable {
private db: Database;
private runtime?: RuntimeConfig;
+ private dbConfig: DatabaseConfig;
+ private retryConfig: DatabaseRetryConfig;
- constructor(dbPath?: string) {
+ constructor(dbPath?: string, dbConfig?: DatabaseConfig, retryConfig?: DatabaseRetryConfig) {
const resolvedPath = dbPath ?? resolveDbPath();
+ // Default database configuration optimized for distributed filesystems
+ this.dbConfig = {
+ walMode: true,
+ busyTimeoutMs: 5000,
+ cacheSize: -20000, // 20MB cache
+ synchronous: 'NORMAL',
+ mmapSize: 268435456, // 256MB
+ ...dbConfig
+ };
+
+ // Default retry configuration for database operations
+ this.retryConfig = {
+ attempts: 3,
+ delayMs: 100,
+ backoff: 2,
+ maxDelayMs: 5000,
+ ...retryConfig
+ };
+
// Ensure the directory exists
const dir = dirname(resolvedPath);
mkdirSync(dir, { recursive: true });
this.db = new Database(resolvedPath, { create: true });
+
+ // Apply SQLite configuration for distributed filesystem optimization
+ configureSqlite(this.db, this.dbConfig);
+
ensureSchema(this.db);
runMigrations(this.db);
}
- setRuntimeConfig(runtime: RuntimeConfig): void {
- this.runtime = runtime;
+ setRuntimeConfig(runtime: ConfigRuntimeConfig): void {
+ this.runtime = runtime as any; // Keep backward compatibility
+
+ // Update retry config from runtime config if available
+ if (runtime.database?.retry) {
+ this.retryConfig = {
+ ...this.retryConfig,
+ ...runtime.database.retry
+ };
+ }
}
getDatabase(): Database {
return this.db;
}
+ /**
+ * Get the current retry configuration
+ */
+ getRetryConfig(): DatabaseRetryConfig {
+ return this.retryConfig;
+ }
+
+ /**
+ * Execute a database operation with retry logic
+ */
+ private async withRetry(
+ operation: () => T | Promise,
+ operationName: string
+ ): Promise {
+ return withDatabaseRetry(operation, this.retryConfig, operationName);
+ }
+
+ /**
+ * Execute a synchronous database operation with retry logic
+ */
+ private withRetrySync(
+ operation: () => T,
+ operationName: string
+ ): T {
+ return withDatabaseRetrySync(operation, this.retryConfig, operationName);
+ }
+
getAllAccounts(): Account[] {
- const rows = this.db
- .query(`
- SELECT
- id,
- name,
- provider,
- api_key,
- refresh_token,
- access_token,
- expires_at,
- created_at,
- last_used,
- request_count,
- total_requests,
- rate_limited_until,
- session_start,
- session_request_count,
- COALESCE(account_tier, 1) as account_tier,
- COALESCE(paused, 0) as paused,
- rate_limit_reset,
- rate_limit_status,
- rate_limit_remaining
- FROM accounts
- `)
- .all();
-
- return rows.map(toAccount);
+ return this.withRetrySync(() => {
+ const rows = this.db
+ .query(`
+ SELECT
+ id,
+ name,
+ provider,
+ api_key,
+ refresh_token,
+ access_token,
+ expires_at,
+ created_at,
+ last_used,
+ request_count,
+ total_requests,
+ rate_limited_until,
+ session_start,
+ session_request_count,
+ COALESCE(account_tier, 1) as account_tier,
+ COALESCE(paused, 0) as paused,
+ rate_limit_reset,
+ rate_limit_status,
+ rate_limit_remaining
+ FROM accounts
+ `)
+ .all();
+
+ return rows.map(toAccount);
+ }, "getAllAccounts");
}
updateAccountTokens(
@@ -75,17 +197,19 @@ export class DatabaseOperations implements StrategyStore, Disposable {
expiresAt: number,
refreshToken?: string,
): void {
- if (refreshToken) {
- this.db.run(
- `UPDATE accounts SET access_token = ?, expires_at = ?, refresh_token = ? WHERE id = ?`,
- [accessToken, expiresAt, refreshToken, accountId],
- );
- } else {
- this.db.run(
- `UPDATE accounts SET access_token = ?, expires_at = ? WHERE id = ?`,
- [accessToken, expiresAt, accountId],
- );
- }
+ this.withRetrySync(() => {
+ if (refreshToken) {
+ this.db.run(
+ `UPDATE accounts SET access_token = ?, expires_at = ?, refresh_token = ? WHERE id = ?`,
+ [accessToken, expiresAt, refreshToken, accountId],
+ );
+ } else {
+ this.db.run(
+ `UPDATE accounts SET access_token = ?, expires_at = ? WHERE id = ?`,
+ [accessToken, expiresAt, accountId],
+ );
+ }
+ }, "updateAccountTokens");
}
updateAccountUsage(accountId: string): void {
@@ -115,10 +239,12 @@ export class DatabaseOperations implements StrategyStore, Disposable {
}
markAccountRateLimited(accountId: string, until: number): void {
- this.db.run(`UPDATE accounts SET rate_limited_until = ? WHERE id = ?`, [
- until,
- accountId,
- ]);
+ this.withRetrySync(() => {
+ this.db.run(`UPDATE accounts SET rate_limited_until = ? WHERE id = ?`, [
+ until,
+ accountId,
+ ]);
+ }, "markAccountRateLimited");
}
updateAccountRateLimitMeta(
@@ -225,34 +351,36 @@ export class DatabaseOperations implements StrategyStore, Disposable {
}
getAccount(accountId: string): Account | null {
- const row = this.db
- .query(`
- SELECT
- id,
- name,
- provider,
- api_key,
- refresh_token,
- access_token,
- expires_at,
- created_at,
- last_used,
- request_count,
- total_requests,
- rate_limited_until,
- session_start,
- session_request_count,
- COALESCE(account_tier, 1) as account_tier,
- COALESCE(paused, 0) as paused,
- rate_limit_reset,
- rate_limit_status,
- rate_limit_remaining
- FROM accounts
- WHERE id = ?
- `)
- .get(accountId);
-
- return row ? toAccount(row) : null;
+ return this.withRetrySync(() => {
+ const row = this.db
+ .query(`
+ SELECT
+ id,
+ name,
+ provider,
+ api_key,
+ refresh_token,
+ access_token,
+ expires_at,
+ created_at,
+ last_used,
+ request_count,
+ total_requests,
+ rate_limited_until,
+ session_start,
+ session_request_count,
+ COALESCE(account_tier, 1) as account_tier,
+ COALESCE(paused, 0) as paused,
+ rate_limit_reset,
+ rate_limit_status,
+ rate_limit_remaining
+ FROM accounts
+ WHERE id = ?
+ `)
+ .get(accountId);
+
+ return row ? toAccount(row) : null;
+ }, "getAccount");
}
updateAccountRequestCount(accountId: string, count: number): void {
@@ -272,49 +400,55 @@ export class DatabaseOperations implements StrategyStore, Disposable {
}
getRequestPayload(id: string): unknown | null {
- const row = this.db
- .query<{ json: string }, [string]>(
- `SELECT json FROM request_payloads WHERE id = ?`,
- )
- .get(id);
-
- if (!row) return null;
-
- try {
- return JSON.parse(row.json);
- } catch {
- return null;
- }
+ return this.withRetrySync(() => {
+ const row = this.db
+ .query<{ json: string }, [string]>(
+ `SELECT json FROM request_payloads WHERE id = ?`,
+ )
+ .get(id);
+
+ if (!row) return null;
+
+ try {
+ return JSON.parse(row.json);
+ } catch {
+ return null;
+ }
+ }, "getRequestPayload");
}
listRequestPayloads(limit = 50): Array<{ id: string; json: string }> {
- return this.db
- .query<{ id: string; json: string }, [number]>(`
- SELECT rp.id, rp.json
- FROM request_payloads rp
- JOIN requests r ON rp.id = r.id
- ORDER BY r.timestamp DESC
- LIMIT ?
- `)
- .all(limit);
+ return this.withRetrySync(() => {
+ return this.db
+ .query<{ id: string; json: string }, [number]>(`
+ SELECT rp.id, rp.json
+ FROM request_payloads rp
+ JOIN requests r ON rp.id = r.id
+ ORDER BY r.timestamp DESC
+ LIMIT ?
+ `)
+ .all(limit);
+ }, "listRequestPayloads");
}
listRequestPayloadsWithAccountNames(
limit = 50,
): Array<{ id: string; json: string; account_name: string | null }> {
- return this.db
- .query<
- { id: string; json: string; account_name: string | null },
- [number]
- >(`
- SELECT rp.id, rp.json, a.name as account_name
- FROM request_payloads rp
- JOIN requests r ON rp.id = r.id
- LEFT JOIN accounts a ON r.account_used = a.id
- ORDER BY r.timestamp DESC
- LIMIT ?
- `)
- .all(limit);
+ return this.withRetrySync(() => {
+ return this.db
+ .query<
+ { id: string; json: string; account_name: string | null },
+ [number]
+ >(`
+ SELECT rp.id, rp.json, a.name as account_name
+ FROM request_payloads rp
+ JOIN requests r ON rp.id = r.id
+ LEFT JOIN accounts a ON r.account_used = a.id
+ ORDER BY r.timestamp DESC
+ LIMIT ?
+ `)
+ .all(limit);
+ }, "listRequestPayloadsWithAccountNames");
}
pauseAccount(accountId: string): void {
@@ -383,3 +517,5 @@ export { DatabaseFactory } from "./factory";
// Re-export migrations for convenience
export { ensureSchema, runMigrations } from "./migrations";
export { resolveDbPath } from "./paths";
+// Re-export retry utilities for external use
+export { withDatabaseRetry, withDatabaseRetrySync } from "./retry";
diff --git a/packages/database/src/retry.ts b/packages/database/src/retry.ts
new file mode 100644
index 00000000..7415c1b9
--- /dev/null
+++ b/packages/database/src/retry.ts
@@ -0,0 +1,171 @@
+import { Logger } from "@ccflare/logger";
+import type { DatabaseRetryConfig } from "./index";
+
+const logger = new Logger("db-retry");
+
+/**
+ * Error codes that indicate database lock contention and should trigger retries
+ */
+const RETRYABLE_SQLITE_ERRORS = [
+ "SQLITE_BUSY",
+ "SQLITE_LOCKED",
+ "database is locked",
+ "database table is locked",
+];
+
+/**
+ * Check if an error is retryable (indicates database lock contention)
+ */
+function isRetryableError(error: unknown): boolean {
+ if (!error) return false;
+
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ const errorCode = (error as any)?.code;
+
+ return RETRYABLE_SQLITE_ERRORS.some(retryableError =>
+ errorMessage.includes(retryableError) || errorCode === retryableError
+ );
+}
+
+/**
+ * Calculate delay for exponential backoff with jitter
+ */
+function calculateDelay(attempt: number, config: Required): number {
+ const baseDelay = config.delayMs * Math.pow(config.backoff, attempt);
+ const jitter = Math.random() * 0.1 * baseDelay; // Add 10% jitter
+ const delayWithJitter = baseDelay + jitter;
+
+ return Math.min(delayWithJitter, config.maxDelayMs);
+}
+
+/**
+ * Sleep for the specified number of milliseconds
+ */
+function sleep(ms: number): Promise {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Retry wrapper for database operations with exponential backoff
+ */
+export async function withDatabaseRetry(
+ operation: () => T | Promise,
+ config: DatabaseRetryConfig = {},
+ operationName = "database operation"
+): Promise {
+ const retryConfig: Required = {
+ attempts: 3,
+ delayMs: 100,
+ backoff: 2,
+ maxDelayMs: 5000,
+ ...config,
+ };
+
+ let lastError: unknown;
+
+ for (let attempt = 0; attempt < retryConfig.attempts; attempt++) {
+ try {
+ const result = await operation();
+
+ // Log successful retry if this wasn't the first attempt
+ if (attempt > 0) {
+ logger.info(`${operationName} succeeded after ${attempt + 1} attempts`);
+ }
+
+ return result;
+ } catch (error) {
+ lastError = error;
+
+ // Check if this is a retryable error
+ if (!isRetryableError(error)) {
+ logger.debug(`${operationName} failed with non-retryable error:`, error);
+ throw error;
+ }
+
+ // If this was the last attempt, throw the error
+ if (attempt === retryConfig.attempts - 1) {
+ logger.error(`${operationName} failed after ${retryConfig.attempts} attempts:`, error);
+ throw error;
+ }
+
+ // Calculate delay and wait before retry
+ const delay = calculateDelay(attempt, retryConfig);
+ logger.warn(
+ `${operationName} failed (attempt ${attempt + 1}/${retryConfig.attempts}), retrying in ${delay.toFixed(0)}ms:`,
+ error instanceof Error ? error.message : String(error)
+ );
+
+ await sleep(delay);
+ }
+ }
+
+ // This should never be reached, but TypeScript requires it
+ throw lastError;
+}
+
+/**
+ * Synchronous retry wrapper for database operations
+ */
+export function withDatabaseRetrySync(
+ operation: () => T,
+ config: DatabaseRetryConfig = {},
+ operationName = "database operation"
+): T {
+ const retryConfig: Required = {
+ attempts: 3,
+ delayMs: 100,
+ backoff: 2,
+ maxDelayMs: 5000,
+ ...config,
+ };
+
+ let lastError: unknown;
+
+ for (let attempt = 0; attempt < retryConfig.attempts; attempt++) {
+ try {
+ const result = operation();
+
+ // Log successful retry if this wasn't the first attempt
+ if (attempt > 0) {
+ logger.info(`${operationName} succeeded after ${attempt + 1} attempts`);
+ }
+
+ return result;
+ } catch (error) {
+ lastError = error;
+
+ // Check if this is a retryable error
+ if (!isRetryableError(error)) {
+ logger.debug(`${operationName} failed with non-retryable error:`, error);
+ throw error;
+ }
+
+ // If this was the last attempt, throw the error
+ if (attempt === retryConfig.attempts - 1) {
+ logger.error(`${operationName} failed after ${retryConfig.attempts} attempts:`, error);
+ throw error;
+ }
+
+ // Calculate delay and wait before retry (synchronous sleep)
+ const delay = calculateDelay(attempt, retryConfig);
+ logger.warn(
+ `${operationName} failed (attempt ${attempt + 1}/${retryConfig.attempts}), retrying in ${delay.toFixed(0)}ms:`,
+ error instanceof Error ? error.message : String(error)
+ );
+
+ // Synchronous sleep using Bun.sleepSync if available, otherwise busy wait
+ if (typeof Bun !== 'undefined' && Bun.sleepSync) {
+ Bun.sleepSync(delay);
+ } else {
+ // Fallback busy wait (not ideal but necessary for sync operations)
+ const start = Date.now();
+ while (Date.now() - start < delay) {
+ // Busy wait
+ }
+ }
+ }
+ }
+
+ // This should never be reached, but TypeScript requires it
+ throw lastError;
+}
From e79080ef4aea48c1a7025798ed25f4366ae784f8 Mon Sep 17 00:00:00 2001
From: Reese
Date: Tue, 29 Jul 2025 20:28:54 +0100
Subject: [PATCH 10/17] =?UTF-8?q?feat:=20protect=20dashboard=20with=20API?=
=?UTF-8?q?=20key=20authentication=E2=80=A6=20incl=20resources?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/server/src/server.ts | 34 ++++++++++++++++++++++++++++------
1 file changed, 28 insertions(+), 6 deletions(-)
diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts
index f2f6d0f6..68afcec2 100644
--- a/apps/server/src/server.ts
+++ b/apps/server/src/server.ts
@@ -113,8 +113,17 @@ const server = serve({
return apiResponse;
}
+ // Check API key for auth protection
+ const apiKey = process.env.API_KEY;
+
// Dashboard routes
- if (url.pathname === "/" || url.pathname === "/dashboard") {
+ if (url.pathname === "/" || url.pathname === "/dashboard" ||
+ (apiKey && url.pathname === `/${apiKey}/`)) {
+
+ // If API key is required, only allow /{key}/ access
+ if (apiKey && url.pathname !== `/${apiKey}/`) {
+ return new Response("Not Found", { status: 404 });
+ }
// Read the HTML file directly
let dashboardPath: string;
try {
@@ -139,18 +148,33 @@ const server = serve({
}
// Serve dashboard static assets
- if ((dashboardManifest as Record)[url.pathname]) {
+ let assetPathname = url.pathname;
+ let isAuthenticatedAssetRequest = false;
+
+ // If API key is set, check for auth-prefixed asset paths
+ if (apiKey && url.pathname.startsWith(`/${apiKey}/`)) {
+ // Strip the key prefix for asset lookup
+ assetPathname = url.pathname.substring(`/${apiKey}`.length);
+ isAuthenticatedAssetRequest = true;
+ }
+
+ if ((dashboardManifest as Record)[assetPathname]) {
+ // If API key is required but request is not authenticated, block access
+ if (apiKey && !isAuthenticatedAssetRequest) {
+ return new Response("Not Found", { status: 404 });
+ }
+
try {
let assetPath: string;
try {
assetPath = Bun.resolveSync(
- `@ccflare/dashboard-web/dist${url.pathname}`,
+ `@ccflare/dashboard-web/dist${assetPathname}`,
dirname(import.meta.path),
);
} catch {
// Fallback to relative path in mono-repo
assetPath = Bun.resolveSync(
- `../../../packages/dashboard-web/dist${url.pathname}`,
+ `../../../packages/dashboard-web/dist${assetPathname}`,
dirname(import.meta.path),
);
}
@@ -172,8 +196,6 @@ const server = serve({
}
// Handle API authentication and proxying
- const apiKey = process.env.API_KEY;
-
if (apiKey) {
// Auth required - check for /key/v1/ format
const pathParts = url.pathname.split('/').filter(Boolean);
From c45c5fbbe8e14ef8ee737de923bfa4ec8f3bfb13 Mon Sep 17 00:00:00 2001
From: Reese
Date: Tue, 29 Jul 2025 20:39:37 +0100
Subject: [PATCH 11/17] perf: optimize requests page performance and eliminate
N+1 queries
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix N+1 query problem in getRequests() by using JOIN instead of individual getAccount() calls
- Eliminate redundant API calls in dashboard by enhancing detail handler to include summary data
- Add database index on requests.account_used for better JOIN performance
- Apply retry logic consistently to getRequestSummaries() function
- Reduce database queries from ~201 to 1 for 200 request entries
- Reduce API calls from 2 to 1 in dashboard requests page
Performance improvements:
- Database queries: ~99.5% reduction (201 → 1)
- Network requests: 50% reduction (2 → 1)
- Better resilience with consistent retry mechanisms
---
.../src/components/RequestsTab.tsx | 15 ++--
packages/database/src/migrations.ts | 7 +-
packages/http-api/src/handlers/requests.ts | 79 +++++++++++++++++--
packages/tui-core/src/requests.ts | 55 ++++++-------
4 files changed, 114 insertions(+), 42 deletions(-)
diff --git a/packages/dashboard-web/src/components/RequestsTab.tsx b/packages/dashboard-web/src/components/RequestsTab.tsx
index 399a2cd7..9b683e23 100644
--- a/packages/dashboard-web/src/components/RequestsTab.tsx
+++ b/packages/dashboard-web/src/components/RequestsTab.tsx
@@ -28,16 +28,17 @@ export function RequestsTab() {
const loadRequests = useCallback(async () => {
try {
- const [detailData, summaryData] = await Promise.all([
- api.getRequestsDetail(200),
- api.getRequestsSummary(200),
- ]);
+ // Only fetch detailed data - extract summary info from it
+ const detailData = await api.getRequestsDetail(200);
setRequests(detailData);
- // Create a map of summaries by ID
+ // Extract summary data from the detailed requests (now included in response)
const summaryMap = new Map();
- summaryData.forEach((summary) => {
- summaryMap.set(summary.id, summary);
+ detailData.forEach((request: any) => {
+ // Summary data is now included in the response from the optimized detail handler
+ if (request.summary) {
+ summaryMap.set(request.id, request.summary);
+ }
});
setSummaries(summaryMap);
diff --git a/packages/database/src/migrations.ts b/packages/database/src/migrations.ts
index 29c67d75..a8aa75b4 100644
--- a/packages/database/src/migrations.ts
+++ b/packages/database/src/migrations.ts
@@ -35,11 +35,16 @@ export function ensureSchema(db: Database): void {
)
`);
- // Create index for faster queries
+ // Create indexes for faster queries
db.run(
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp DESC)`,
);
+ // Index for JOIN performance with accounts table
+ db.run(
+ `CREATE INDEX IF NOT EXISTS idx_requests_account_used ON requests(account_used)`,
+ );
+
// Create request_payloads table for storing full request/response data
db.run(`
CREATE TABLE IF NOT EXISTS request_payloads (
diff --git a/packages/http-api/src/handlers/requests.ts b/packages/http-api/src/handlers/requests.ts
index 930b6ecf..df9eea63 100644
--- a/packages/http-api/src/handlers/requests.ts
+++ b/packages/http-api/src/handlers/requests.ts
@@ -70,26 +70,91 @@ export function createRequestsSummaryHandler(db: Database) {
}
/**
- * Create a detailed requests handler with full payload data
+ * Create a detailed requests handler with full payload data and summary info
*/
export function createRequestsDetailHandler(dbOps: DatabaseOperations) {
return (limit = 100): Response => {
- const rows = dbOps.listRequestPayloadsWithAccountNames(limit);
- const parsed = rows.map((r) => {
+ const db = dbOps.getDatabase();
+
+ // Get summary data from requests table
+ const summaries = db
+ .query(
+ `
+ SELECT r.*, a.name as account_name
+ FROM requests r
+ LEFT JOIN accounts a ON r.account_used = a.id
+ ORDER BY r.timestamp DESC
+ LIMIT ?1
+ `,
+ )
+ .all(limit) as Array<{
+ id: string;
+ timestamp: number;
+ method: string;
+ path: string;
+ account_used: string | null;
+ account_name: string | null;
+ status_code: number | null;
+ success: 0 | 1;
+ error_message: string | null;
+ response_time_ms: number | null;
+ failover_attempts: number;
+ model: string | null;
+ prompt_tokens: number | null;
+ completion_tokens: number | null;
+ total_tokens: number | null;
+ input_tokens: number | null;
+ cache_read_input_tokens: number | null;
+ cache_creation_input_tokens: number | null;
+ output_tokens: number | null;
+ cost_usd: number | null;
+ }>;
+
+ // Get payload data
+ const payloadRows = dbOps.listRequestPayloadsWithAccountNames(limit);
+ const payloadMap = new Map();
+
+ payloadRows.forEach((r) => {
try {
const data = JSON.parse(r.json);
- // Add account name to the meta field if available
if (r.account_name && data.meta) {
data.meta.accountName = r.account_name;
}
- return { id: r.id, ...data };
+ payloadMap.set(r.id, data);
} catch {
- return { id: r.id, error: "Failed to parse payload" };
+ payloadMap.set(r.id, { error: "Failed to parse payload" });
}
});
- return new Response(JSON.stringify(parsed), {
+ // Combine summary and payload data
+ const combined = summaries.map((summary) => {
+ const payload = payloadMap.get(summary.id) || {
+ id: summary.id,
+ request: { headers: {}, body: null },
+ response: null,
+ meta: { timestamp: summary.timestamp }
+ };
+
+ // Add summary data to the payload
+ payload.summary = {
+ id: summary.id,
+ model: summary.model || undefined,
+ inputTokens: summary.input_tokens || undefined,
+ outputTokens: summary.output_tokens || undefined,
+ totalTokens: summary.total_tokens || undefined,
+ cacheReadInputTokens: summary.cache_read_input_tokens || undefined,
+ cacheCreationInputTokens: summary.cache_creation_input_tokens || undefined,
+ costUsd: summary.cost_usd || undefined,
+ responseTimeMs: summary.response_time_ms || undefined,
+ };
+
+ return payload;
+ });
+
+ return new Response(JSON.stringify(combined), {
headers: { "Content-Type": "application/json" },
});
};
}
+
+
diff --git a/packages/tui-core/src/requests.ts b/packages/tui-core/src/requests.ts
index 1d02a247..4006b539 100644
--- a/packages/tui-core/src/requests.ts
+++ b/packages/tui-core/src/requests.ts
@@ -1,4 +1,4 @@
-import { DatabaseFactory } from "@ccflare/database";
+import { DatabaseFactory, withDatabaseRetrySync } from "@ccflare/database";
export interface RequestPayload {
id: string;
@@ -37,17 +37,15 @@ export interface RequestSummary {
export async function getRequests(limit = 100): Promise {
const dbOps = DatabaseFactory.getInstance();
- const rows = dbOps.listRequestPayloads(limit);
+ // Use the optimized query that includes account names in a single JOIN
+ const rows = dbOps.listRequestPayloadsWithAccountNames(limit);
- const parsed = rows.map((r: { id: string; json: string }) => {
+ const parsed = rows.map((r: { id: string; json: string; account_name: string | null }) => {
try {
const data = JSON.parse(r.json);
- // Add account name if we have accountId
- if (data.meta?.accountId) {
- const account = dbOps.getAccount(data.meta.accountId);
- if (account) {
- data.meta.accountName = account.name;
- }
+ // Add account name from the JOIN result (no additional query needed)
+ if (r.account_name && data.meta) {
+ data.meta.accountName = r.account_name;
}
return { id: r.id, ...data } as RequestPayload;
} catch {
@@ -68,25 +66,28 @@ export async function getRequestSummaries(
limit = 100,
): Promise
diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts
index 963011c4..7c8a2f6b 100644
--- a/packages/database/src/index.ts
+++ b/packages/database/src/index.ts
@@ -17,34 +17,62 @@ import { withDatabaseRetry, withDatabaseRetrySync } from "./retry";
* Apply SQLite pragmas for optimal performance on distributed filesystems
*/
function configureSqlite(db: Database, config: DatabaseConfig): void {
- // Enable WAL mode for better concurrency
- if (config.walMode !== false) {
- db.run("PRAGMA journal_mode = WAL");
- }
+ try {
+ // Check database integrity first
+ const integrityResult = db.query("PRAGMA integrity_check").get() as { integrity_check: string };
+ if (integrityResult.integrity_check !== "ok") {
+ throw new Error(`Database integrity check failed: ${integrityResult.integrity_check}`);
+ }
- // Set busy timeout for lock handling
- if (config.busyTimeoutMs !== undefined) {
- db.run(`PRAGMA busy_timeout = ${config.busyTimeoutMs}`);
- }
+ // Enable WAL mode for better concurrency (with error handling)
+ if (config.walMode !== false) {
+ try {
+ const result = db.query("PRAGMA journal_mode = WAL").get() as { journal_mode: string };
+ if (result.journal_mode !== "wal") {
+ console.warn("Failed to enable WAL mode, falling back to DELETE mode");
+ db.run("PRAGMA journal_mode = DELETE");
+ }
+ } catch (error) {
+ console.warn("WAL mode failed, using DELETE mode:", error);
+ db.run("PRAGMA journal_mode = DELETE");
+ }
+ }
- // Configure cache size
- if (config.cacheSize !== undefined) {
- db.run(`PRAGMA cache_size = ${config.cacheSize}`);
- }
+ // Set busy timeout for lock handling
+ if (config.busyTimeoutMs !== undefined) {
+ db.run(`PRAGMA busy_timeout = ${config.busyTimeoutMs}`);
+ }
- // Set synchronous mode
- if (config.synchronous !== undefined) {
- db.run(`PRAGMA synchronous = ${config.synchronous}`);
- }
+ // Configure cache size
+ if (config.cacheSize !== undefined) {
+ db.run(`PRAGMA cache_size = ${config.cacheSize}`);
+ }
- // Configure memory-mapped I/O
- if (config.mmapSize !== undefined) {
- db.run(`PRAGMA mmap_size = ${config.mmapSize}`);
- }
+ // Set synchronous mode (more conservative for distributed filesystems)
+ const syncMode = config.synchronous || 'FULL'; // Default to FULL for safety
+ db.run(`PRAGMA synchronous = ${syncMode}`);
- // Additional optimizations for distributed filesystems
- db.run("PRAGMA temp_store = MEMORY");
- db.run("PRAGMA foreign_keys = ON");
+ // Configure memory-mapped I/O (disable on distributed filesystems if problematic)
+ if (config.mmapSize !== undefined && config.mmapSize > 0) {
+ try {
+ db.run(`PRAGMA mmap_size = ${config.mmapSize}`);
+ } catch (error) {
+ console.warn("Memory-mapped I/O failed, disabling:", error);
+ db.run("PRAGMA mmap_size = 0");
+ }
+ }
+
+ // Additional optimizations for distributed filesystems
+ db.run("PRAGMA temp_store = MEMORY");
+ db.run("PRAGMA foreign_keys = ON");
+
+ // Add checkpoint interval for WAL mode
+ db.run("PRAGMA wal_autocheckpoint = 1000");
+
+ } catch (error) {
+ console.error("Database configuration failed:", error);
+ throw new Error(`Failed to configure SQLite database: ${error}`);
+ }
}
export interface RuntimeConfig {
@@ -85,12 +113,13 @@ export class DatabaseOperations implements StrategyStore, Disposable {
const resolvedPath = dbPath ?? resolveDbPath();
// Default database configuration optimized for distributed filesystems
+ // More conservative settings to prevent corruption on Rook Ceph
this.dbConfig = {
walMode: true,
- busyTimeoutMs: 5000,
- cacheSize: -20000, // 20MB cache
- synchronous: 'NORMAL',
- mmapSize: 268435456, // 256MB
+ busyTimeoutMs: 10000, // Increased timeout for distributed storage
+ cacheSize: -10000, // Reduced cache size (10MB) for stability
+ synchronous: 'FULL', // Full synchronous mode for data safety
+ mmapSize: 0, // Disable memory-mapped I/O on distributed filesystems
...dbConfig
};
diff --git a/packages/http-api/src/router.ts b/packages/http-api/src/router.ts
index 16bda766..f56033b6 100644
--- a/packages/http-api/src/router.ts
+++ b/packages/http-api/src/router.ts
@@ -49,7 +49,6 @@ export class APIRouter {
const _accountTierHandler = createAccountTierUpdateHandler(dbOps);
const requestsSummaryHandler = createRequestsSummaryHandler(db);
const requestsDetailHandler = createRequestsDetailHandler(dbOps);
- const requestPayloadHandler = createRequestPayloadHandler(dbOps);
const configHandlers = createConfigHandlers(config);
const logsStreamHandler = createLogsStreamHandler();
const logsHistoryHandler = createLogsHistoryHandler();
@@ -69,11 +68,7 @@ export class APIRouter {
const limit = parseInt(url.searchParams.get("limit") || "100");
return requestsDetailHandler(limit);
});
- this.handlers.set("GET:/api/requests/payload/:id", (_req, url) => {
- const pathParts = url.pathname.split('/');
- const requestId = pathParts[pathParts.length - 1];
- return requestPayloadHandler(requestId);
- });
+ // Note: Dynamic route for request payloads is handled in the route() method
this.handlers.set("GET:/api/config", () => configHandlers.getConfig());
this.handlers.set("GET:/api/config/strategy", () =>
configHandlers.getStrategy(),
@@ -120,6 +115,14 @@ export class APIRouter {
return await this.wrapHandler(handler)(req, url);
}
+ // Check for dynamic request payload endpoints
+ if (path.startsWith("/api/requests/payload/") && method === "GET") {
+ const parts = path.split("/");
+ const requestId = parts[4]; // /api/requests/payload/{id}
+ const requestPayloadHandler = createRequestPayloadHandler(this.context.dbOps);
+ return await this.wrapHandler(() => requestPayloadHandler(requestId))(req, url);
+ }
+
// Check for dynamic account endpoints
if (path.startsWith("/api/accounts/")) {
const parts = path.split("/");
diff --git a/packages/tui-core/src/requests.ts b/packages/tui-core/src/requests.ts
index 4006b539..0c03d91f 100644
--- a/packages/tui-core/src/requests.ts
+++ b/packages/tui-core/src/requests.ts
@@ -37,31 +37,94 @@ export interface RequestSummary {
export async function getRequests(limit = 100): Promise