diff --git a/package.json b/package.json index bd1a4f8..0274dc1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.72", + "version": "0.3.73", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js index 466fe0c..020cb86 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/MitmServerCard.js @@ -3,6 +3,8 @@ import { useState, useEffect } from "react"; import { Card, Button, Badge, Input } from "@/shared/components"; +const DEFAULT_MITM_ROUTER_BASE = "http://localhost:20128"; + /** * Shared MITM infrastructure card — manages SSL cert + server start/stop. * DNS per-tool is handled separately in MitmToolCard. @@ -15,6 +17,7 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } const [selectedApiKey, setSelectedApiKey] = useState(""); const [pendingAction, setPendingAction] = useState(null); const [modalError, setModalError] = useState(null); + const [mitmRouterBaseUrl, setMitmRouterBaseUrl] = useState(DEFAULT_MITM_ROUTER_BASE); const isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); const isAdmin = status?.isAdmin !== false; @@ -35,6 +38,9 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } if (res.ok) { const data = await res.json(); setStatus(data); + if (data.mitmRouterBaseUrl) { + setMitmRouterBaseUrl(data.mitmRouterBaseUrl); + } onStatusChange?.(data); } } catch { @@ -68,7 +74,11 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange } await fetch("/api/cli-tools/antigravity-mitm", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ apiKey: keyToUse, sudoPassword: password }), + body: JSON.stringify({ + apiKey: keyToUse, + sudoPassword: password, + mitmRouterBaseUrl: mitmRouterBaseUrl.trim() || DEFAULT_MITM_ROUTER_BASE, + }), }); } else { await fetch("/api/cli-tools/antigravity-mitm", { @@ -137,25 +147,44 @@ export default function MitmServerCard({ apiKeys, cloudEnabled, onStatusChange }
- {/* API Key selector (only when stopped) */} - {!isRunning && ( + {/* Base URL + API Key — same row pattern as Claude Code / cli-tools */} +{connection.email}
-|
-
- {colors.emoji}
- {quota.name}
+
+ |
{/* Limit (Progress + Numbers) */}
-
+ {colors.emoji}
+
+ {quota.name}
+
- |
+
- {/* Provider Cards Grid */}
-
+ |
{/* Reset Time */}
-
{/* Progress bar - always show with border for visibility */}
-
{/* Numbers */}
-
+
{quota.used.toLocaleString()} / {quota.total > 0 ? quota.total.toLocaleString() : "∞"}
@@ -132,22 +139,22 @@ export default function QuotaTable({ quotas = [] }) {
+ |
{countdown !== "-" || resetDisplay ? (
|
diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
index 1c6324d..3f0dc59 100644
--- a/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
+++ b/src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.js
@@ -2,11 +2,12 @@
import { useState, useEffect, useCallback, useRef } from "react";
import ProviderIcon from "@/shared/components/ProviderIcon";
-import ProviderLimitCard from "./ProviderLimitCard";
import QuotaTable from "./QuotaTable";
+import Toggle from "@/shared/components/Toggle";
import { parseQuotaData, calculatePercentage } from "./utils";
import Card from "@/shared/components/Card";
import Button from "@/shared/components/Button";
+import { EditConnectionModal } from "@/shared/components";
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
const REFRESH_INTERVAL_MS = 60000; // 60 seconds
@@ -21,6 +22,11 @@ export default function ProviderLimits() {
const [refreshingAll, setRefreshingAll] = useState(false);
const [countdown, setCountdown] = useState(60);
const [connectionsLoading, setConnectionsLoading] = useState(true);
+ const [deletingId, setDeletingId] = useState(null);
+ const [togglingId, setTogglingId] = useState(null);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [selectedConnection, setSelectedConnection] = useState(null);
+ const [proxyPools, setProxyPools] = useState([]);
const intervalRef = useRef(null);
const countdownRef = useRef(null);
@@ -123,6 +129,97 @@ export default function ProviderLimits() {
[fetchQuota],
);
+ const handleDeleteConnection = useCallback(async (id) => {
+ if (!confirm("Delete this connection?")) return;
+ setDeletingId(id);
+ try {
+ const res = await fetch(`/api/providers/${id}`, { method: "DELETE" });
+ if (res.ok) {
+ setConnections((prev) => prev.filter((c) => c.id !== id));
+ setQuotaData((prev) => {
+ const next = { ...prev };
+ delete next[id];
+ return next;
+ });
+ setLoading((prev) => {
+ const next = { ...prev };
+ delete next[id];
+ return next;
+ });
+ setErrors((prev) => {
+ const next = { ...prev };
+ delete next[id];
+ return next;
+ });
+ }
+ } catch (error) {
+ console.error("Error deleting connection:", error);
+ } finally {
+ setDeletingId(null);
+ }
+ }, []);
+
+ const handleToggleConnectionActive = useCallback(async (id, isActive) => {
+ setTogglingId(id);
+ try {
+ const res = await fetch(`/api/providers/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ isActive }),
+ });
+ if (res.ok) {
+ setConnections((prev) =>
+ prev.map((c) => (c.id === id ? { ...c, isActive } : c)),
+ );
+ }
+ } catch (error) {
+ console.error("Error updating connection status:", error);
+ } finally {
+ setTogglingId(null);
+ }
+ }, []);
+
+ const handleUpdateConnection = useCallback(
+ async (formData) => {
+ if (!selectedConnection?.id) return;
+ const connectionId = selectedConnection.id;
+ const provider = selectedConnection.provider;
+ try {
+ const res = await fetch(`/api/providers/${connectionId}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(formData),
+ });
+ if (res.ok) {
+ await fetchConnections();
+ setShowEditModal(false);
+ setSelectedConnection(null);
+ if (USAGE_SUPPORTED_PROVIDERS.includes(provider)) {
+ await fetchQuota(connectionId, provider);
+ }
+ }
+ } catch (error) {
+ console.error("Error saving connection:", error);
+ }
+ },
+ [selectedConnection, fetchConnections, fetchQuota],
+ );
+
+ useEffect(() => {
+ let cancelled = false;
+ fetch("/api/proxy-pools?isActive=true", { cache: "no-store" })
+ .then((res) => res.json())
+ .then((data) => {
+ if (!cancelled && data?.proxyPools) {
+ setProxyPools(data.proxyPools);
+ }
+ })
+ .catch(() => {});
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
// Refresh all providers
const refreshAll = useCallback(async () => {
if (refreshingAll) return;
@@ -358,81 +455,148 @@ export default function ProviderLimits() {
{countdown !== "-" && (
-
+
in {countdown}
)}
{resetDisplay && (
-
+
) : (
-
{resetDisplay}
)}
N/A
+ N/A
)}
+ {/* Provider cards: 2 columns, compact */}
+
{sortedConnections.map((conn) => {
const quota = quotaData[conn.id];
const isLoading = loading[conn.id];
const error = errors[conn.id];
// Use table layout for all providers
+ const isInactive = conn.isActive === false;
+ const rowBusy = deletingId === conn.id || togglingId === conn.id;
+
return (
-
-
-
-
+
+
-
+
+
-
-
+
+ refresh
+
+
+
+ {conn.provider}{conn.name && ( -{conn.name} ++ {conn.name} + )} |