diff --git a/src/shared/components/KiroOAuthWrapper.js b/src/shared/components/KiroOAuthWrapper.js
index 93e45e3d..248f0b9c 100644
--- a/src/shared/components/KiroOAuthWrapper.js
+++ b/src/shared/components/KiroOAuthWrapper.js
@@ -13,12 +13,14 @@ import KiroSocialOAuthModal from "./KiroSocialOAuthModal";
export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onClose }) {
const [authMethod, setAuthMethod] = useState(null); // null | "builder-id" | "idc" | "social" | "import"
const [socialProvider, setSocialProvider] = useState(null); // "google" | "github"
+ const [builderIdConfig, setBuilderIdConfig] = useState(null);
const [idcConfig, setIdcConfig] = useState(null);
const handleMethodSelect = useCallback((method, config) => {
if (method === "builder-id") {
- // Use device code flow (AWS Builder ID)
+ // Use device code flow (AWS Builder ID) with config
setAuthMethod("builder-id");
+ setBuilderIdConfig(config);
} else if (method === "idc") {
// Use device code flow with IDC config
setAuthMethod("idc");
@@ -36,6 +38,7 @@ export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onCl
const handleBack = () => {
setAuthMethod(null);
setSocialProvider(null);
+ setBuilderIdConfig(null);
setIdcConfig(null);
};
@@ -48,6 +51,7 @@ export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onCl
const handleDeviceSuccess = () => {
setAuthMethod(null);
+ setBuilderIdConfig(null);
setIdcConfig(null);
onSuccess?.();
onClose?.(); // Close modal after success
@@ -66,6 +70,7 @@ export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onCl
// Show device code flow (Builder ID or IDC)
if (authMethod === "builder-id" || authMethod === "idc") {
+ const deviceConfig = authMethod === "builder-id" ? builderIdConfig : idcConfig;
return (
);
}
diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js
index c25c501e..3c8a3d56 100644
--- a/src/shared/components/OAuthModal.js
+++ b/src/shared/components/OAuthModal.js
@@ -9,8 +9,9 @@ import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
* OAuth Modal Component
* - Localhost: Auto callback via popup message
* - Remote: Manual paste callback URL
+ * - deviceConfig: Optional config for device code flow (startUrl, region)
*/
-export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, onClose }) {
+export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, onClose, deviceConfig }) {
const [step, setStep] = useState("waiting"); // waiting | input | success | error
const [authData, setAuthData] = useState(null);
const [callbackUrl, setCallbackUrl] = useState("");
@@ -21,6 +22,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
const popupRef = useRef(null);
const pollingAbortRef = useRef(false);
const { copied, copy } = useCopyToClipboard();
+ const deviceCodeGeneratedRef = useRef(false);
// State for client-only values to avoid hydration mismatch
const [isLocalhost, setIsLocalhost] = useState(false);
@@ -128,27 +130,55 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
// Start OAuth flow
const startOAuthFlow = useCallback(async () => {
if (!provider) return;
+
+ // For device code flow, don't regenerate if we already have valid device data
+ // BUT: For Kiro with deviceConfig, always regenerate to use the new config
+ const shouldReuseDeviceCode = (provider === "github" || provider === "qwen" || provider === "kiro") && deviceData && !polling && !(provider === "kiro" && deviceConfig);
+
+ if (shouldReuseDeviceCode) {
+ // Device code already exists and we're not polling, just reopen the verification URL
+ console.log("Using cached device code:", deviceData.user_code);
+ setIsDeviceCode(true);
+ setStep("waiting");
+ const verifyUrl = deviceData.verification_uri_complete || deviceData.verification_uri;
+ if (verifyUrl) window.open(verifyUrl, "_blank");
+ return;
+ }
+
try {
setError(null);
// Device code flow providers
const deviceCodeProviders = ["github", "qwen", "kiro", "kimi-coding", "kilocode"];
if (deviceCodeProviders.includes(provider)) {
+ console.log("Generating new device code. deviceData:", deviceData, "polling:", polling);
setIsDeviceCode(true);
setStep("waiting");
- const res = await fetch(`/api/oauth/${provider}/device-code`);
+ // For Kiro, pass device config (startUrl, region) if provided
+ let url = `/api/oauth/${provider}/device-code`;
+ if (provider === "kiro" && deviceConfig) {
+ const params = new URLSearchParams();
+ if (deviceConfig.startUrl) params.append("startUrl", deviceConfig.startUrl);
+ if (deviceConfig.region) params.append("region", deviceConfig.region);
+ if (params.toString()) url += `?${params.toString()}`;
+ console.log("Kiro device-code URL:", url);
+ }
+
+ const res = await fetch(url);
const data = await res.json();
if (!res.ok) throw new Error(data.error);
+ console.log("Device code response:", data.user_code, data.device_code);
+
setDeviceData(data);
// Open verification URL
const verifyUrl = data.verification_uri_complete || data.verification_uri;
if (verifyUrl) window.open(verifyUrl, "_blank");
- // Pass extraData for Kiro (contains _clientId, _clientSecret)
- const extraData = provider === "kiro" ? { _clientId: data._clientId, _clientSecret: data._clientSecret } : null;
+ // Start polling - pass extraData for Kiro (contains _clientId, _clientSecret, _region, _startUrl)
+ const extraData = provider === "kiro" ? { _clientId: data._clientId, _clientSecret: data._clientSecret, _region: data._region, _startUrl: data._startUrl } : null;
startPolling(data.device_code, data.codeVerifier, data.interval || 5, extraData);
return;
}
@@ -188,24 +218,108 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setError(err.message);
setStep("error");
}
- }, [provider, isLocalhost, startPolling]);
+ }, [provider, isLocalhost, startPolling, deviceConfig]);
// Reset state and start OAuth when modal opens
useEffect(() => {
- if (isOpen && provider) {
- setAuthData(null);
- setCallbackUrl("");
- setError(null);
- setIsDeviceCode(false);
- setDeviceData(null);
- setPolling(false);
- pollingAbortRef.current = false;
- startOAuthFlow();
- } else if (!isOpen) {
+ if (!isOpen || !provider) {
// Abort polling when modal closes
pollingAbortRef.current = true;
+ return;
+ }
+
+ // Prevent duplicate generation in React Strict Mode
+ if (deviceCodeGeneratedRef.current) {
+ console.log("Device code already generated in this cycle, skipping");
+ return;
}
- }, [isOpen, provider, startOAuthFlow]);
+ deviceCodeGeneratedRef.current = true;
+
+ setAuthData(null);
+ setCallbackUrl("");
+ setError(null);
+ setIsDeviceCode(false);
+ setPolling(false);
+
+ // Start OAuth flow inline to avoid circular dependency
+ (async () => {
+ // For device code flow, don't regenerate if we already have valid device data
+ // BUT: For Kiro with deviceConfig, always regenerate to use the new config
+ const shouldReuseDeviceCode = (provider === "github" || provider === "qwen" || provider === "kiro") && deviceData && !polling && !(provider === "kiro" && deviceConfig);
+
+ if (shouldReuseDeviceCode) {
+ setIsDeviceCode(true);
+ setStep("waiting");
+ const verifyUrl = deviceData.verification_uri_complete || deviceData.verification_uri;
+ if (verifyUrl) window.open(verifyUrl, "_blank");
+ return;
+ }
+
+ try {
+ setError(null);
+
+ // Device code flow (GitHub, Qwen, Kiro)
+ if (provider === "github" || provider === "qwen" || provider === "kiro") {
+ setIsDeviceCode(true);
+ setStep("waiting");
+
+ // For Kiro, pass device config (startUrl, region) if provided
+ let url = `/api/oauth/${provider}/device-code`;
+ if (provider === "kiro" && deviceConfig) {
+ const params = new URLSearchParams();
+ if (deviceConfig.startUrl) params.append("startUrl", deviceConfig.startUrl);
+ if (deviceConfig.region) params.append("region", deviceConfig.region);
+ if (params.toString()) url += `?${params.toString()}`;
+ }
+
+ const res = await fetch(url);
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error);
+
+ setDeviceData(data);
+
+ // Open verification URL
+ const verifyUrl = data.verification_uri_complete || data.verification_uri;
+ if (verifyUrl) window.open(verifyUrl, "_blank");
+
+ // Start polling - pass extraData for Kiro (contains _clientId, _clientSecret)
+ const extraData = provider === "kiro" ? { _clientId: data._clientId, _clientSecret: data._clientSecret } : null;
+ startPolling(data.device_code, data.codeVerifier, data.interval || 5, extraData);
+ return;
+ }
+
+ // Authorization code flow - always use localhost with current port (except Codex)
+ let redirectUri;
+ if (provider === "codex") {
+ redirectUri = "http://localhost:1455/auth/callback";
+ } else {
+ const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
+ redirectUri = `http://localhost:${port}/callback`;
+ }
+
+ const res = await fetch(`/api/oauth/${provider}/authorize?redirect_uri=${encodeURIComponent(redirectUri)}`);
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error);
+
+ setAuthData({ ...data, redirectUri });
+
+ if (provider === "codex" || !isLocalhost) {
+ setStep("input");
+ window.open(data.authUrl, "_blank");
+ } else {
+ setStep("waiting");
+ popupRef.current = window.open(data.authUrl, "oauth_popup", "width=600,height=700");
+
+ if (!popupRef.current) {
+ setStep("input");
+ }
+ }
+ } catch (err) {
+ setError(err.message);
+ setStep("error");
+ }
+ })();
+ }, [isOpen, provider, deviceConfig]);
// Listen for OAuth callback via multiple methods
useEffect(() => {
@@ -350,12 +464,12 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
Verification URL
- {deviceData.verification_uri}
+ {deviceData.verification_uri_complete || deviceData.verification_uri}
diff --git a/src/shared/components/TimeRangeModal.js b/src/shared/components/TimeRangeModal.js
new file mode 100644
index 00000000..e048f12c
--- /dev/null
+++ b/src/shared/components/TimeRangeModal.js
@@ -0,0 +1,190 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import Modal from "./Modal";
+import { cn } from "@/shared/utils/cn";
+
+export default function TimeRangeModal({ isOpen, onClose, currentRange, onRangeChange }) {
+ const [customFrom, setCustomFrom] = useState("");
+ const [customTo, setCustomTo] = useState("");
+
+ // Reset custom dates when modal opens
+ useEffect(() => {
+ if (isOpen) {
+ if (typeof currentRange === "object" && currentRange.type === "custom") {
+ setCustomFrom(currentRange.startDate || "");
+ setCustomTo(currentRange.endDate || "");
+ } else {
+ setCustomFrom("");
+ setCustomTo("");
+ }
+ }
+ }, [isOpen, currentRange]);
+
+ const quickRanges = [
+ { value: "5m", label: "5m" },
+ { value: "15m", label: "15m" },
+ { value: "30m", label: "30m" },
+ { value: "1h", label: "1h" },
+ ];
+
+ const extendedRanges = [
+ { value: "24h", label: "24h" },
+ { value: "48h", label: "48h" },
+ { value: "7d", label: "7d" },
+ { value: "30d", label: "30d" },
+ ];
+
+ const isSelected = (value) => {
+ if (typeof currentRange === "object") {
+ return false;
+ }
+ return currentRange === value;
+ };
+
+ const handleRangeSelect = (value) => {
+ onRangeChange(value);
+ onClose();
+ };
+
+ const handleCustomApply = () => {
+ if (customFrom && customTo) {
+ onRangeChange({
+ type: "custom",
+ startDate: customFrom,
+ endDate: customTo,
+ });
+ onClose();
+ }
+ };
+
+ const formatDatetimeLocal = (date) => {
+ const d = new Date(date);
+ const year = d.getFullYear();
+ const month = String(d.getMonth() + 1).padStart(2, "0");
+ const day = String(d.getDate()).padStart(2, "0");
+ const hours = String(d.getHours()).padStart(2, "0");
+ const minutes = String(d.getMinutes()).padStart(2, "0");
+ return `${year}-${month}-${day}T${hours}:${minutes}`;
+ };
+
+ return (
+
+
+ {/* Quick Ranges */}
+
+
+ Quick Ranges
+
+
+ {quickRanges.map((range) => (
+
+ ))}
+
+
+
+ {/* Extended Ranges */}
+
+
+ Extended Ranges
+
+
+ {extendedRanges.map((range) => (
+
+ ))}
+
+
+
+ {/* All Time */}
+
+
+
+
+ {/* Custom Range */}
+
+
+ Custom Range
+
+
+
+
+ setCustomFrom(e.target.value)}
+ max={formatDatetimeLocal(new Date())}
+ className="w-full px-3 py-2 rounded-lg border border-border bg-bg text-text text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
+ />
+
+
+
+ setCustomTo(e.target.value)}
+ max={formatDatetimeLocal(new Date())}
+ min={customFrom}
+ className="w-full px-3 py-2 rounded-lg border border-border bg-bg text-text text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js
index ee92194b..4f7e1be6 100644
--- a/src/shared/components/UsageStats.js
+++ b/src/shared/components/UsageStats.js
@@ -4,6 +4,7 @@ import { useState, useEffect, useMemo, useCallback } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import Badge from "./Badge";
import Card from "./Card";
+import TimeRangeModal from "./TimeRangeModal";
import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards";
import UsageTable, { fmt, fmtTime } from "@/app/(dashboard)/dashboard/usage/components/UsageTable";
import ProviderTopology from "@/app/(dashboard)/dashboard/usage/components/ProviderTopology";
@@ -20,12 +21,12 @@ function timeAgo(timestamp) {
// Auto-update time display every second without re-rendering parent
function TimeAgo({ timestamp }) {
const [, setTick] = useState(0);
-
+
useEffect(() => {
const timer = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(timer);
}, []);
-
+
return <>{timeAgo(timestamp)}>;
}
@@ -194,6 +195,11 @@ export default function UsageStats() {
const [providers, setProviders] = useState([]);
const [period, setPeriod] = useState("7d");
+ // New state for our additions
+ const [showClearConfirm, setShowClearConfirm] = useState(false);
+ const [clearing, setClearing] = useState(false);
+ const [showTimeRangeModal, setShowTimeRangeModal] = useState(false);
+
// Fetch connected providers once, deduplicate by provider type
useEffect(() => {
fetch("/api/providers")
@@ -266,6 +272,37 @@ export default function UsageStats() {
router.replace(`?${params.toString()}`, { scroll: false });
}, [searchParams, router]);
+ const handleClearMetrics = async () => {
+ setClearing(true);
+ try {
+ const res = await fetch("/api/usage/clear", { method: "POST" });
+ if (res.ok) {
+ setStats(null);
+ setLoading(true);
+ setShowClearConfirm(false);
+ // Re-fetch stats
+ fetch(`/api/usage/stats?period=${period}`)
+ .then((r) => r.ok ? r.json() : null)
+ .then((data) => { if (data) setStats(data); })
+ .catch(() => {})
+ .finally(() => setLoading(false));
+ }
+ } catch (error) {
+ console.error("Error clearing metrics:", error);
+ } finally {
+ setClearing(false);
+ }
+ };
+
+ const handleCustomRange = useCallback(({ startDate, endDate }) => {
+ setFetching(true);
+ fetch(`/api/usage/stats?startDate=${startDate}&endDate=${endDate}`)
+ .then((r) => r.ok ? r.json() : null)
+ .then((data) => { if (data) setStats((prev) => ({ ...prev, ...data })); })
+ .catch(() => {})
+ .finally(() => setFetching(false));
+ }, []);
+
// Compute active table data
const activeTableConfig = useMemo(() => {
if (!stats) return null;
@@ -393,7 +430,57 @@ export default function UsageStats() {
return (
- {/* Period selector */}
+ {/* Clear Confirmation Modal */}
+ {showClearConfirm && (
+
+
+
Clear All Usage Metrics?
+
+ This will permanently delete all usage history. This action cannot be undone.
+
+
+
+
+
+
+
+ )}
+
+ {/* Time Range Modal */}
+ {showTimeRangeModal && (
+
setShowTimeRangeModal(false)}
+ currentRange={period}
+ onRangeChange={(range) => {
+ if (typeof range === "object" && range.type === "custom") {
+ handleCustomRange(range);
+ } else {
+ setPeriod(range);
+ }
+ setShowTimeRangeModal(false);
+ }}
+ />
+ )}
+
+ {/* Period selector + Clear button */}
{PERIODS.map((p) => (
@@ -406,7 +493,22 @@ export default function UsageStats() {
{p.label}
))}
+
+
{fetching && (
progress_activity
)}
diff --git a/src/shared/constants/pricing.js b/src/shared/constants/pricing.js
index 3ca5b9c0..eb2fe16f 100644
--- a/src/shared/constants/pricing.js
+++ b/src/shared/constants/pricing.js
@@ -567,6 +567,27 @@ export const DEFAULT_PRICING = {
// Kiro AI (kr) - AWS CodeWhisperer
kr: {
+ "claude-opus-4.6": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 37.50,
+ cache_creation: 5.00
+ },
+ "claude-sonnet-4.6": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 22.50,
+ cache_creation: 3.00
+ },
+ "claude-opus-4.5": {
+ input: 5.00,
+ output: 25.00,
+ cached: 0.50,
+ reasoning: 37.50,
+ cache_creation: 5.00
+ },
"claude-sonnet-4.5": {
input: 3.00,
output: 15.00,
@@ -580,6 +601,41 @@ export const DEFAULT_PRICING = {
cached: 0.05,
reasoning: 3.75,
cache_creation: 0.50
+ },
+ "claude-sonnet-4": {
+ input: 3.00,
+ output: 15.00,
+ cached: 0.30,
+ reasoning: 22.50,
+ cache_creation: 3.00
+ },
+ "deepseek-3.2": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "deepseek-3.1": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "minimax-m2.1": {
+ input: 0.50,
+ output: 2.00,
+ cached: 0.25,
+ reasoning: 3.00,
+ cache_creation: 0.50
+ },
+ "qwen3-coder-next": {
+ input: 1.00,
+ output: 4.00,
+ cached: 0.50,
+ reasoning: 6.00,
+ cache_creation: 1.00
}
},