- Added tunnel
- Removed cloud feature
This commit is contained in:
decolua
2026-02-21 16:42:46 +07:00
parent adf57aa0c9
commit 0baa299722
37 changed files with 858 additions and 933 deletions

View File

@@ -1,7 +1,5 @@
import { NextResponse } from "next/server";
import { validateApiKey, getModelAliases, setModelAlias, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { validateApiKey, getModelAliases, setModelAlias } from "@/models";
// PUT /api/cloud/models/alias - Set model alias (for cloud/CLI)
export async function PUT(request) {
@@ -37,9 +35,6 @@ export async function PUT(request) {
// Update alias
await setModelAlias(alias, model);
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({
success: true,
model,
@@ -52,21 +47,6 @@ export async function PUT(request) {
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing aliases to cloud:", error);
}
}
// GET /api/cloud/models/alias - Get all aliases
export async function GET(request) {
try {

View File

@@ -1,7 +1,5 @@
import { NextResponse } from "next/server";
import { getComboById, updateCombo, deleteCombo, getComboByName, isCloudEnabled } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { getComboById, updateCombo, deleteCombo, getComboByName } from "@/lib/localDb";
// Validate combo name: only a-z, A-Z, 0-9, -, _
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
@@ -48,9 +46,6 @@ export async function PUT(request, { params }) {
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
}
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json(combo);
} catch (error) {
console.log("Error updating combo:", error);
@@ -67,9 +62,6 @@ export async function DELETE(request, { params }) {
if (!success) {
return NextResponse.json({ error: "Combo not found" }, { status: 404 });
}
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({ success: true });
} catch (error) {
@@ -77,18 +69,3 @@ export async function DELETE(request, { params }) {
return NextResponse.json({ error: "Failed to delete combo" }, { status: 500 });
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing to cloud:", error);
}
}

View File

@@ -1,7 +1,5 @@
import { NextResponse } from "next/server";
import { getCombos, createCombo, getComboByName, isCloudEnabled } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { getCombos, createCombo, getComboByName } from "@/lib/localDb";
// Validate combo name: only a-z, A-Z, 0-9, -, _
const VALID_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
@@ -40,27 +38,9 @@ export async function POST(request) {
const combo = await createCombo({ name, models: models || [] });
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json(combo, { status: 201 });
} catch (error) {
console.log("Error creating combo:", error);
return NextResponse.json({ error: "Failed to create combo" }, { status: 500 });
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing to cloud:", error);
}
}

View File

@@ -1,7 +1,5 @@
import { NextResponse } from "next/server";
import { deleteApiKey, getApiKeyById, updateApiKey, isCloudEnabled } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { deleteApiKey, getApiKeyById, updateApiKey } from "@/lib/localDb";
// GET /api/keys/[id] - Get single key
export async function GET(request, { params }) {
@@ -34,7 +32,6 @@ export async function PUT(request, { params }) {
if (isActive !== undefined) updateData.isActive = isActive;
const updated = await updateApiKey(id, updateData);
await syncKeysToCloudIfEnabled();
return NextResponse.json({ key: updated });
} catch (error) {
@@ -53,27 +50,9 @@ export async function DELETE(request, { params }) {
return NextResponse.json({ error: "Key not found" }, { status: 404 });
}
// Auto sync to Cloud if enabled
await syncKeysToCloudIfEnabled();
return NextResponse.json({ message: "Key deleted successfully" });
} catch (error) {
console.log("Error deleting key:", error);
return NextResponse.json({ error: "Failed to delete key" }, { status: 500 });
}
}
/**
* Sync API keys to Cloud if enabled
*/
async function syncKeysToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing keys to cloud:", error);
}
}

View File

@@ -1,7 +1,6 @@
import { NextResponse } from "next/server";
import { getApiKeys, createApiKey, isCloudEnabled } from "@/lib/localDb";
import { getApiKeys, createApiKey } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
// GET /api/keys - List API keys
export async function GET() {
@@ -28,9 +27,6 @@ export async function POST(request) {
const machineId = await getConsistentMachineId();
const apiKey = await createApiKey(name, machineId);
// Auto sync to Cloud if enabled
await syncKeysToCloudIfEnabled();
return NextResponse.json({
key: apiKey.key,
name: apiKey.name,
@@ -42,18 +38,3 @@ export async function POST(request) {
return NextResponse.json({ error: "Failed to create key" }, { status: 500 });
}
}
/**
* Sync API keys to Cloud if enabled
*/
async function syncKeysToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing keys to cloud:", error);
}
}

View File

@@ -1,7 +1,5 @@
import { NextResponse } from "next/server";
import { getModelAliases, setModelAlias, deleteModelAlias, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { getModelAliases, setModelAlias, deleteModelAlias } from "@/models";
// GET /api/models/alias - Get all aliases
export async function GET() {
@@ -25,7 +23,6 @@ export async function PUT(request) {
}
await setModelAlias(alias, model);
await syncToCloudIfEnabled();
return NextResponse.json({ success: true, model, alias });
} catch (error) {
@@ -45,7 +42,6 @@ export async function DELETE(request) {
}
await deleteModelAlias(alias);
await syncToCloudIfEnabled();
return NextResponse.json({ success: true });
} catch (error) {
@@ -53,15 +49,3 @@ export async function DELETE(request) {
return NextResponse.json({ error: "Failed to delete alias" }, { status: 500 });
}
}
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing aliases to cloud:", error);
}
}

View File

@@ -6,9 +6,7 @@ import {
requestDeviceCode,
pollForToken
} from "@/lib/oauth/providers";
import { createProviderConnection, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { createProviderConnection } from "@/models";
/**
* Dynamic OAuth API Route
@@ -89,9 +87,6 @@ export async function POST(request, { params }) {
testStatus: "active",
});
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({
success: true,
connection: {
@@ -138,9 +133,6 @@ export async function POST(request, { params }) {
testStatus: "active",
});
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({
success: true,
connection: {
@@ -167,18 +159,3 @@ export async function POST(request, { params }) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing to cloud after OAuth:", error);
}
}

View File

@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { CursorService } from "@/lib/oauth/services/cursor";
import { createProviderConnection, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { createProviderConnection } from "@/models";
/**
* POST /api/oauth/cursor/import
@@ -58,9 +56,6 @@ export async function POST(request) {
testStatus: "active",
});
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({
success: true,
connection: {
@@ -103,18 +98,3 @@ export async function GET() {
],
});
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing to cloud after Cursor import:", error);
}
}

View File

@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { KiroService } from "@/lib/oauth/services/kiro";
import { createProviderConnection, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { createProviderConnection } from "@/models";
/**
* POST /api/oauth/kiro/import
@@ -43,9 +41,6 @@ export async function POST(request) {
testStatus: "active",
});
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({
success: true,
connection: {
@@ -59,18 +54,3 @@ export async function POST(request) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing to cloud after Kiro import:", error);
}
}

View File

@@ -1,8 +1,6 @@
import { NextResponse } from "next/server";
import { KiroService } from "@/lib/oauth/services/kiro";
import { createProviderConnection, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { createProviderConnection } from "@/models";
/**
* POST /api/oauth/kiro/social-exchange
@@ -54,9 +52,6 @@ export async function POST(request) {
testStatus: "active",
});
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({
success: true,
connection: {
@@ -70,18 +65,3 @@ export async function POST(request) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing to cloud after Kiro OAuth:", error);
}
}

View File

@@ -1,7 +1,5 @@
import { NextResponse } from "next/server";
import { getProviderConnectionById, updateProviderConnection, deleteProviderConnection, isCloudEnabled } from "@/models";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { getProviderConnectionById, updateProviderConnection, deleteProviderConnection } from "@/models";
// GET /api/providers/[id] - Get single connection
export async function GET(request, { params }) {
@@ -59,9 +57,6 @@ export async function PUT(request, { params }) {
delete result.refreshToken;
delete result.idToken;
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({ connection: result });
} catch (error) {
console.log("Error updating connection:", error);
@@ -79,27 +74,9 @@ export async function DELETE(request, { params }) {
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
}
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({ message: "Connection deleted successfully" });
} catch (error) {
console.log("Error deleting connection:", error);
return NextResponse.json({ error: "Failed to delete connection" }, { status: 500 });
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing providers to cloud:", error);
}
}

View File

@@ -1,6 +1,4 @@
import { getProviderConnectionById, updateProviderConnection, isCloudEnabled } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import {
GEMINI_CONFIG,
@@ -325,17 +323,5 @@ export async function testSingleConnection(id) {
await updateProviderConnection(id, updateData);
if (result.refreshed) {
try {
const cloudEnabled = await isCloudEnabled();
if (cloudEnabled) {
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
}
} catch (err) {
console.log("Error syncing to cloud after token refresh:", err);
}
}
return { valid: result.valid, error: result.error, latencyMs, testedAt: new Date().toISOString() };
}

View File

@@ -1,9 +1,7 @@
import { NextResponse } from "next/server";
import { getProviderConnections, createProviderConnection, getProviderNodeById, isCloudEnabled } from "@/models";
import { getProviderConnections, createProviderConnection, getProviderNodeById } from "@/models";
import { APIKEY_PROVIDERS } from "@/shared/constants/config";
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { syncToCloud } from "@/app/api/sync/cloud/route";
// GET /api/providers - List all connections
export async function GET() {
@@ -101,27 +99,9 @@ export async function POST(request) {
const result = { ...newConnection };
delete result.apiKey;
// Auto sync to Cloud if enabled
await syncToCloudIfEnabled();
return NextResponse.json({ connection: result }, { status: 201 });
} catch (error) {
console.log("Error creating provider:", error);
return NextResponse.json({ error: "Failed to create provider" }, { status: 500 });
}
}
/**
* Sync to Cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const cloudEnabled = await isCloudEnabled();
if (!cloudEnabled) return;
const machineId = await getConsistentMachineId();
await syncToCloud(machineId);
} catch (error) {
console.log("Error syncing providers to cloud:", error);
}
}

View File

@@ -1,318 +0,0 @@
import { NextResponse } from "next/server";
import { getProviderConnections, getModelAliases, getCombos, getApiKeys, createApiKey, updateProviderConnection, updateSettings, getCloudUrl } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import fs from "fs/promises";
import path from "path";
import os from "os";
const CLOUD_SYNC_TIMEOUT_MS = Number(process.env.CLOUD_SYNC_TIMEOUT_MS || 12000);
async function getResolvedCloudUrl() {
return await getCloudUrl();
}
async function fetchWithTimeout(url, options = {}, timeoutMs = CLOUD_SYNC_TIMEOUT_MS) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(url, { ...options, signal: controller.signal });
} finally {
clearTimeout(timeoutId);
}
}
/**
* POST /api/sync/cloud
* Sync data with Cloud
*/
export async function POST(request) {
try {
const body = await request.json();
const { action } = body;
// Always get machineId from server, don't trust client
const machineId = await getConsistentMachineId();
switch (action) {
case "enable":
await updateSettings({ cloudEnabled: true });
// Auto create key if none exists
const keys = await getApiKeys();
let createdKey = null;
if (keys.length === 0) {
createdKey = await createApiKey("Default Key", machineId);
}
return syncAndVerify(machineId, createdKey?.key, keys);
case "sync": {
const syncResult = await syncToCloud(machineId);
if (syncResult.error) {
return NextResponse.json(syncResult, { status: 502 });
}
return NextResponse.json(syncResult);
}
case "disable":
await updateSettings({ cloudEnabled: false });
return handleDisable(machineId, request);
case "check":
return handleCheck();
default:
return NextResponse.json({ error: "Invalid action" }, { status: 400 });
}
} catch (error) {
console.log("Cloud sync error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
/**
* Sync data to Cloud (exported for reuse)
* @param {string} machineId
* @param {string|null} createdKey - Key created during enable
*/
export async function syncToCloud(machineId, createdKey = null) {
const cloudUrl = await getResolvedCloudUrl();
if (!cloudUrl) {
return { error: "Cloud URL is not configured" };
}
// Get current data from db
const providers = await getProviderConnections();
const modelAliases = await getModelAliases();
const combos = await getCombos();
const apiKeys = await getApiKeys();
let response;
try {
// Send to Cloud
response = await fetchWithTimeout(`${cloudUrl}/sync/${machineId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
providers,
modelAliases,
combos,
apiKeys
})
});
} catch (error) {
const isTimeout = error?.name === "AbortError";
return { error: isTimeout ? "Cloud sync timeout" : "Cloud sync request failed" };
}
if (!response.ok) {
const errorText = await response.text();
console.log("Cloud sync failed:", errorText);
return { error: "Cloud sync failed" };
}
const result = await response.json();
// Update local db with tokens from Cloud (providers stored by ID)
if (result.data && result.data.providers) {
await updateLocalTokens(result.data.providers);
}
const responseData = {
success: true,
message: "Synced successfully",
changes: result.changes
};
if (createdKey) {
responseData.createdKey = createdKey;
}
return responseData;
}
/**
* Sync and verify connection with ping
*/
async function syncAndVerify(machineId, createdKey, existingKeys) {
// Step 1: Sync data to cloud
const syncResult = await syncToCloud(machineId, createdKey);
if (syncResult.error) {
return NextResponse.json(syncResult, { status: 502 });
}
// Step 2: Verify connection by pinging the cloud
const apiKey = createdKey || existingKeys[0]?.key;
if (!apiKey) {
return NextResponse.json({
...syncResult,
verified: false,
verifyError: "No API key available"
});
}
try {
const cloudUrl = await getResolvedCloudUrl();
const pingResponse = await fetchWithTimeout(`${cloudUrl}/${machineId}/v1/verify`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json"
}
});
if (pingResponse.ok) {
return NextResponse.json({
...syncResult,
verified: true
});
} else {
return NextResponse.json({
...syncResult,
verified: false,
verifyError: `Ping failed: ${pingResponse.status}`
});
}
} catch (error) {
return NextResponse.json({
...syncResult,
verified: false,
verifyError: error.message
});
}
}
/**
* Disable Cloud - delete cache and update Claude CLI settings
*/
async function handleDisable(machineId, request) {
const cloudUrl = await getResolvedCloudUrl();
if (!cloudUrl) {
return NextResponse.json({ error: "Cloud URL is not configured" }, { status: 500 });
}
let response;
try {
response = await fetchWithTimeout(`${cloudUrl}/sync/${machineId}`, {
method: "DELETE"
});
} catch (error) {
const isTimeout = error?.name === "AbortError";
return NextResponse.json(
{ error: isTimeout ? "Cloud disable timeout" : "Failed to reach cloud service" },
{ status: 502 }
);
}
if (!response.ok) {
const errorText = await response.text();
console.log("Cloud disable failed:", errorText);
return NextResponse.json({ error: "Failed to disable cloud" }, { status: 502 });
}
// Update Claude CLI settings to use local endpoint
const host = request.headers.get("host") || "localhost:20128";
await updateClaudeSettingsToLocal(machineId, host, cloudUrl);
return NextResponse.json({
success: true,
message: "Cloud disabled"
});
}
/**
* Update Claude CLI settings to use local endpoint (only if currently using cloud)
*/
async function updateClaudeSettingsToLocal(machineId, host, cloudUrl) {
try {
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
const cloudEndpoint = `${cloudUrl}/${machineId}`;
const localUrl = `http://${host}`;
// Read current settings
let settings;
try {
const content = await fs.readFile(settingsPath, "utf-8");
settings = JSON.parse(content);
} catch (error) {
if (error.code === "ENOENT") {
return; // No settings file, nothing to update
}
throw error;
}
// Check if ANTHROPIC_BASE_URL matches cloud URL
const currentUrl = settings.env?.ANTHROPIC_BASE_URL;
if (!currentUrl || currentUrl !== cloudEndpoint) {
return; // Not using cloud URL, don't modify
}
// Update to local URL
settings.env.ANTHROPIC_BASE_URL = localUrl;
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
console.log(`Updated Claude CLI settings: ${cloudEndpoint} → ${localUrl}`);
} catch (error) {
console.log("Failed to update Claude CLI settings:", error.message);
}
}
/**
* Check if cloud worker is reachable
*/
async function handleCheck() {
const cloudUrl = await getResolvedCloudUrl();
if (!cloudUrl) {
return NextResponse.json({ error: "Cloud URL is not configured" }, { status: 400 });
}
try {
const res = await fetchWithTimeout(`${cloudUrl}/health`, { method: "GET" }, 5000);
if (res.ok) {
return NextResponse.json({ success: true, message: "Worker is running" });
}
return NextResponse.json({ error: `Worker responded with ${res.status}` }, { status: 502 });
} catch (error) {
const isTimeout = error?.name === "AbortError";
return NextResponse.json(
{ error: isTimeout ? "Worker request timeout" : "Cannot reach worker" },
{ status: 502 }
);
}
}
/**
* Update local db with data from Cloud
* Simple logic: if Cloud is newer, sync entire provider
* cloudProviders is object keyed by provider ID
*/
async function updateLocalTokens(cloudProviders) {
const localProviders = await getProviderConnections();
for (const localProvider of localProviders) {
const cloudProvider = cloudProviders[localProvider.id];
if (!cloudProvider) continue;
const cloudUpdatedAt = new Date(cloudProvider.updatedAt || 0).getTime();
const localUpdatedAt = new Date(localProvider.updatedAt || 0).getTime();
// Simple logic: if Cloud is newer, sync entire provider
if (cloudUpdatedAt > localUpdatedAt) {
const updates = {
// Tokens
accessToken: cloudProvider.accessToken,
refreshToken: cloudProvider.refreshToken,
expiresAt: cloudProvider.expiresAt,
expiresIn: cloudProvider.expiresIn,
// Provider specific data
providerSpecificData: cloudProvider.providerSpecificData || localProvider.providerSpecificData,
// Status fields
testStatus: cloudProvider.status || "active",
lastError: cloudProvider.lastError,
lastErrorAt: cloudProvider.lastErrorAt,
errorCode: cloudProvider.errorCode,
rateLimitedUntil: cloudProvider.rateLimitedUntil,
// Metadata
updatedAt: cloudProvider.updatedAt
};
await updateProviderConnection(localProvider.id, updates);
}
}
}

View File

@@ -1,36 +0,0 @@
import { NextResponse } from "next/server";
import initializeCloudSync from "@/shared/services/initializeCloudSync";
let syncInitialized = false;
// POST /api/sync/initialize - Initialize cloud sync scheduler
export async function POST(request) {
try {
if (syncInitialized) {
return NextResponse.json({
message: "Cloud sync already initialized"
});
}
await initializeCloudSync();
syncInitialized = true;
return NextResponse.json({
success: true,
message: "Cloud sync initialized successfully"
});
} catch (error) {
console.log("Error initializing cloud sync:", error);
return NextResponse.json({
error: "Failed to initialize cloud sync"
}, { status: 500 });
}
}
// GET /api/sync/status - Check sync initialization status
export async function GET(request) {
return NextResponse.json({
initialized: syncInitialized,
message: syncInitialized ? "Cloud sync is running" : "Cloud sync not initialized"
});
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { disableTunnel } from "@/lib/tunnel/tunnelManager";
export async function POST() {
try {
const result = await disableTunnel();
return NextResponse.json(result);
} catch (error) {
console.error("Tunnel disable error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { enableTunnel } from "@/lib/tunnel/tunnelManager";
export async function POST() {
try {
const result = await enableTunnel();
return NextResponse.json(result);
} catch (error) {
console.error("Tunnel enable error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { getTunnelStatus } from "@/lib/tunnel/tunnelManager";
export async function GET() {
try {
const status = await getTunnelStatus();
return NextResponse.json(status);
} catch (error) {
console.error("Tunnel status error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -1,22 +1,6 @@
import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
import { getMachineId } from "@/shared/utils/machine";
import { getUsageForProvider } from "open-sse/services/usage.js";
import { getExecutor } from "open-sse/executors/index.js";
import { syncToCloud } from "@/app/api/sync/cloud/route";
/**
* Sync to cloud if enabled
*/
async function syncToCloudIfEnabled() {
try {
const machineId = await getMachineId();
if (!machineId) return;
await syncToCloud(machineId);
} catch (error) {
console.error("[Usage API] Error syncing to cloud:", error);
}
}
/**
* Refresh credentials using executor and update database
* @returns {{ connection, refreshed: boolean }}
@@ -119,16 +103,9 @@ export async function GET(request, { params }) {
}
// Refresh credentials if needed using executor
let refreshed = false;
try {
const result = await refreshAndUpdateCredentials(connection);
connection = result.connection;
refreshed = result.refreshed;
// Sync to cloud only if token was refreshed
if (refreshed) {
await syncToCloudIfEnabled();
}
} catch (refreshError) {
console.error("[Usage API] Credential refresh failed:", refreshError);
return Response.json({

View File

@@ -1,33 +1,53 @@
import { getUsageStats, statsEmitter } from "@/lib/usageDb";
import { getUsageStats, statsEmitter, getActiveRequests } from "@/lib/usageDb";
export const dynamic = "force-dynamic";
export async function GET() {
const encoder = new TextEncoder();
const state = { closed: false, keepalive: null, send: null };
const state = { closed: false, keepalive: null, send: null, sendPending: null, cachedStats: null };
const stream = new ReadableStream({
async start(controller) {
// Full stats refresh (heavy) + immediate lightweight push
state.send = async () => {
if (state.closed) return;
try {
const stats = await getUsageStats();
if (stats.activeRequests?.length > 0) {
console.log(`[SSE] Push | active=${stats.activeRequests.length} | ${stats.activeRequests.map(r => r.provider).join(",")}`);
// Push lightweight update immediately so UI reflects changes fast
if (state.cachedStats) {
const { activeRequests, recentRequests, errorProvider } = await getActiveRequests();
const quickStats = { ...state.cachedStats, activeRequests, recentRequests, errorProvider };
controller.enqueue(encoder.encode(`data: ${JSON.stringify(quickStats)}\n\n`));
}
// Then do full recalc and update cache
const stats = await getUsageStats();
state.cachedStats = stats;
controller.enqueue(encoder.encode(`data: ${JSON.stringify(stats)}\n\n`));
} catch {
// Controller closed → self-cleanup
state.closed = true;
statsEmitter.off("update", state.send);
statsEmitter.off("pending", state.sendPending);
clearInterval(state.keepalive);
}
};
// Lightweight push: only refresh activeRequests + recentRequests on pending changes
state.sendPending = async () => {
if (state.closed || !state.cachedStats) return;
try {
const { activeRequests, recentRequests, errorProvider } = await getActiveRequests();
const stats = { ...state.cachedStats, activeRequests, recentRequests, errorProvider };
controller.enqueue(encoder.encode(`data: ${JSON.stringify(stats)}\n\n`));
} catch {
state.closed = true;
statsEmitter.off("pending", state.sendPending);
}
};
await state.send();
console.log(`[SSE] Client connected | listeners=${statsEmitter.listenerCount("update") + 1}`);
statsEmitter.on("update", state.send);
statsEmitter.on("pending", state.sendPending);
state.keepalive = setInterval(() => {
if (state.closed) { clearInterval(state.keepalive); return; }
@@ -43,6 +63,7 @@ export async function GET() {
cancel() {
state.closed = true;
statsEmitter.off("update", state.send);
statsEmitter.off("pending", state.sendPending);
clearInterval(state.keepalive);
console.log("[SSE] Client disconnected");
},