From d26b92b551788a49c0b791cb6c766dd1f8d591a5 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:36:26 -0300 Subject: [PATCH 1/3] Release/v3.7.6 (#1829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(api-keys): add rename support in permissions modal Add an editable key name field at the top of the permissions modal, allowing users to rename API keys alongside existing permission settings. The backend already supported name updates via PATCH /api/keys/:id — this wires the UI to send the name field and refreshes the key list on success. Changes: - Add keyName state and text input to PermissionsModal - Update handleUpdatePermissions to validate and send name in PATCH body - Add integration test for rename via PATCH (valid, empty, too-long names) - Update E2E mock to handle PATCH requests * chore(release): bump version to 3.7.6 * chore(release): v3.7.6 — merge API key rename feature and sync docs * chore(release): expand contributor credits to 155 PRs across full project history - Expanded acknowledgment table from 29 to 53 contributors - Added 100+ previously uncredited PRs from project inception through v3.7.5 - Moved contributor credits section to v3.7.6 (current release) - Synced llm.txt version to 3.7.6 * fix: resolve security ReDoS in codex and bugs #1797 #1789 * feat(dashboard): implement remaining v3.7.6 dashboard features and fixes * fix(xiaomi-mimo): update models to V2.5, fix Token Plan validation and default region (#1823) Integrated into release/v3.7.6 * fix(dashboard): correct loadPresets ReferenceError in CostOverviewTab * fix(codex): omit compact client metadata (#1822) Integrated into release/v3.7.6 * feat(chatgpt-web): support thinking_effort (Standard/Extended) for thinking-capable models (#1821) Integrated into release/v3.7.6 * Fix endpoint visibility, A2A status, and API catalog (#1806) Integrated into release/v3.7.6 * fix(analytics): use pure SQL aggregations — no history rows loaded (#1802) Integrated into release/v3.7.6 * fix(stability): resolve codex input validation, enable combo circuit breaker, and fix broken unit tests * docs(changelog): update for stability bug fixes #1804 #1805 * fix: clear active requests and recover providers (#1824) Integrated into release/v3.7.6 * feat: inject fallback tool names to prevent upstream 400 errors (#1775) * feat: auto-restore probe-failed database to prevent data loss (#1810) * fix: safely cast inputs to strings before calling trim() to avoid crashes on numeric fields in proxy modal (#1825) * chore(release): v3.7.6 — final stability patches for production * test: update expected db probe-failure error message for auto-restore feature * chore(workflow): mandate implementation plan generation in resolve-issues * docs(changelog): rewrite v3.7.6 with complete commit-accurate entries * feat(analytics): add cost-based usage insights and activity streaks Expand usage analytics to report total cost, per-series cost totals, API key counts, and current activity streaks using pricing-aware token calculations. Also make probe-failed database recovery choose the newest backup by its embedded timestamp instead of filesystem mtime so auto-restore selects the intended snapshot reliably. * fix(mitm): enforce transparent interception on port 443 only Reject non-443 MITM port updates in the settings API and normalize stored configuration back to the required transparent interception port. Lock the dashboard port field to 443, update the validation copy, and add integration coverage to prevent stale custom ports from being accepted or surfaced. * docs(changelog): update for analytics and mitm features * chore(release): attribute community contributors Co-authored-by: rdself Co-authored-by: oyi77 Co-authored-by: clousky2020 Co-authored-by: benzntech Co-authored-by: kang-heewon Co-authored-by: herjarsa Co-authored-by: backryun Co-authored-by: tombii Co-authored-by: christopher-s Co-authored-by: zen0bit Co-authored-by: k0valik Co-authored-by: zhangqiang8vip Co-authored-by: wlfonseca Co-authored-by: RaviTharuma Co-authored-by: prakersh Co-authored-by: payne0420 Co-authored-by: only4copilot Co-authored-by: jay77721 Co-authored-by: hijak Co-authored-by: hartmark Co-authored-by: defhouse Co-authored-by: xiaoge1688 Co-authored-by: xandr0s Co-authored-by: willbnu Co-authored-by: slewis3600 Co-authored-by: sergey-v9 Co-authored-by: razllivan Co-authored-by: nmime Co-authored-by: Moutia-Ben-Yahia Co-authored-by: Mind-Dragon Co-authored-by: mercs2910 Co-authored-by: MAINER4IK Co-authored-by: luandiasrj Co-authored-by: knopki Co-authored-by: kfiramar Co-authored-by: ken2190 Co-authored-by: keith8496 Co-authored-by: jonesfernandess Co-authored-by: JasonLandbridge Co-authored-by: i1hwan Co-authored-by: Gorchakov-Pressure Co-authored-by: foxy1402 Co-authored-by: dt418 Co-authored-by: dhaern Co-authored-by: DavyMassoneto Co-authored-by: dail45 Co-authored-by: congvc-dev Co-authored-by: be0hhh Co-authored-by: andruwa13 Co-authored-by: AndrewDragonIV Co-authored-by: AndersonFirmino Co-authored-by: alexsvdk Co-authored-by: abhinavjnu --------- Co-authored-by: Andrew Munsell Co-authored-by: Antigravity Assistant Co-authored-by: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Co-authored-by: Sergey Morozov Co-authored-by: payne Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: ipanghu Co-authored-by: rdself Co-authored-by: oyi77 Co-authored-by: clousky2020 Co-authored-by: benzntech Co-authored-by: kang-heewon Co-authored-by: herjarsa Co-authored-by: backryun Co-authored-by: tombii Co-authored-by: christopher-s Co-authored-by: zen0bit Co-authored-by: k0valik Co-authored-by: zhangqiang8vip Co-authored-by: wlfonseca Co-authored-by: RaviTharuma Co-authored-by: prakersh Co-authored-by: payne0420 Co-authored-by: only4copilot Co-authored-by: jay77721 Co-authored-by: hijak Co-authored-by: hartmark Co-authored-by: defhouse Co-authored-by: xiaoge1688 Co-authored-by: xandr0s Co-authored-by: willbnu Co-authored-by: slewis3600 Co-authored-by: sergey-v9 Co-authored-by: razllivan Co-authored-by: nmime Co-authored-by: Moutia-Ben-Yahia Co-authored-by: Mind-Dragon Co-authored-by: mercs2910 Co-authored-by: MAINER4IK Co-authored-by: luandiasrj Co-authored-by: knopki Co-authored-by: kfiramar Co-authored-by: ken2190 Co-authored-by: keith8496 Co-authored-by: jonesfernandess Co-authored-by: JasonLandbridge Co-authored-by: i1hwan Co-authored-by: Gorchakov-Pressure Co-authored-by: foxy1402 Co-authored-by: dt418 Co-authored-by: dhaern Co-authored-by: DavyMassoneto Co-authored-by: dail45 Co-authored-by: congvc-dev Co-authored-by: be0hhh Co-authored-by: andruwa13 Co-authored-by: AndrewDragonIV Co-authored-by: AndersonFirmino Co-authored-by: alexsvdk Co-authored-by: abhinavjnu From cc460b36b9ef0ba5b486ffb2eba81c1f461d27b8 Mon Sep 17 00:00:00 2001 From: Antigravity Assistant Date: Thu, 30 Apr 2026 14:43:16 -0300 Subject: [PATCH 2/3] fix(ci): fix typecheck implicit any errors and CodeQL regex alert --- src/app/api/usage/analytics/route.ts | 2 +- src/shared/utils/formatting.ts | 31 ++++++++++++++++------------ tests/unit/chatgpt-web.test.ts | 6 +++--- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/app/api/usage/analytics/route.ts b/src/app/api/usage/analytics/route.ts index e52a4141f4..2c34e593a1 100644 --- a/src/app/api/usage/analytics/route.ts +++ b/src/app/api/usage/analytics/route.ts @@ -122,7 +122,7 @@ function computeActivityStreak(activityMap: Record): number { return streak; } -export async function GET(request) { +export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const range = searchParams.get("range") || "30d"; diff --git a/src/shared/utils/formatting.ts b/src/shared/utils/formatting.ts index 5a42c87ecf..6dd941cf2a 100644 --- a/src/shared/utils/formatting.ts +++ b/src/shared/utils/formatting.ts @@ -10,8 +10,9 @@ * @param {string} isoString - ISO 8601 date string * @returns {string} */ -export function formatTime(isoString) { +export function formatTime(isoString: string | null | undefined) { try { + if (!isoString) return "-"; const d = new Date(isoString); return d.toLocaleTimeString("en-US", { hour12: false, @@ -29,7 +30,7 @@ export function formatTime(isoString) { * @param {number} ms - Duration in milliseconds * @returns {string} e.g., "42ms", "1.2s", "-" */ -export function formatDuration(ms) { +export function formatDuration(ms: number | null | undefined) { if (!ms) return "-"; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(1)}s`; @@ -40,8 +41,9 @@ export function formatDuration(ms) { * @param {string} iso - ISO 8601 date string * @returns {string} */ -export function formatDateTime(iso) { +export function formatDateTime(iso: string | null | undefined) { try { + if (!iso) return "-"; const d = new Date(iso); return d.toLocaleDateString("pt-BR") + ", " + d.toLocaleTimeString("en-US", { hour12: false }); } catch { @@ -56,7 +58,7 @@ export function formatDateTime(iso) { * @param {number} end - Number of characters to show at end (default: 2) * @returns {string} */ -export function maskSegment(value, start = 2, end = 2) { +export function maskSegment(value: string | null | undefined, start = 2, end = 2) { if (!value) return ""; if (value.length <= start + end) return `${value.slice(0, 1)}***`; return `${value.slice(0, start)}***${value.slice(-end)}`; @@ -68,7 +70,7 @@ export function maskSegment(value, start = 2, end = 2) { * @param {boolean} emailsVisible - Whether to show full email (true) or mask it (false) * @returns {string} */ -export function maskAccount(account, emailsVisible) { +export function maskAccount(account: string | null | undefined, emailsVisible: boolean) { if (!account || account === "-") return "-"; const atIdx = account.indexOf("@"); if (atIdx > 3) { @@ -87,7 +89,10 @@ export function maskAccount(account, emailsVisible) { * @param {string} apiKeyId - Unique ID of the key * @returns {string} */ -export function formatApiKeyLabel(apiKeyName, apiKeyId) { +export function formatApiKeyLabel( + apiKeyName: string | null | undefined, + apiKeyId: string | null | undefined +) { if (!apiKeyName && !apiKeyId) return "—"; const displayName = apiKeyName || "key"; if (!apiKeyId) return displayName; @@ -99,7 +104,7 @@ export function formatApiKeyLabel(apiKeyName, apiKeyId) { * @param {string} key - API key or token to mask * @returns {string} */ -export function maskKey(key) { +export function maskKey(key: string | null | undefined) { if (!key || key.length < 8) return "***"; return `${key.slice(0, 4)}...${key.slice(-4)}`; } @@ -109,10 +114,10 @@ export function maskKey(key) { * @param {number} n - Number to format * @returns {string} */ -export function fmtCompact(n) { - if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`; - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; +export function fmtCompact(n: number | null | undefined) { + if (n && n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`; + if (n && n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n && n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return new Intl.NumberFormat().format(n || 0); } @@ -121,7 +126,7 @@ export function fmtCompact(n) { * @param {number} n - Number to format * @returns {string} */ -export function fmtFull(n) { +export function fmtFull(n: number | null | undefined) { return new Intl.NumberFormat().format(n || 0); } @@ -147,7 +152,7 @@ export const fmtCost = formatCost; * @param {number} max - Maximum characters (default: 50) * @returns {string} */ -export function truncateUrl(url, max = 50) { +export function truncateUrl(url: string | null | undefined, max = 50) { if (!url) return "-"; try { const parsed = new URL(url); diff --git a/tests/unit/chatgpt-web.test.ts b/tests/unit/chatgpt-web.test.ts index e899e2306d..dd49da1c32 100644 --- a/tests/unit/chatgpt-web.test.ts +++ b/tests/unit/chatgpt-web.test.ts @@ -1231,9 +1231,9 @@ test("thinking_effort: bare chatgpt.com slug (e.g. gpt-5-4-t-mini) passed as mod 1, `bare slug ${bareSlug} must trigger thinking_effort PATCH` ); - assert.match( - m.calls.userConfigUrls[0], - new RegExp(`model_slug=${bareSlug.replace(/\./g, "\\.")}`) + assert.ok( + m.calls.userConfigUrls[0].includes(`model_slug=${bareSlug}`), + `URL should contain model_slug=${bareSlug}` ); } finally { m.restore(); From cd27c5cf334e657c4d6d1f855e87f5edcb9bccde Mon Sep 17 00:00:00 2001 From: Antigravity Assistant Date: Thu, 30 Apr 2026 15:39:51 -0300 Subject: [PATCH 3/3] chore(workflow): fix changelog extraction logic for github releases --- .agents/workflows/generate-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.agents/workflows/generate-release.md b/.agents/workflows/generate-release.md index 7e05c01305..c6107339bb 100644 --- a/.agents/workflows/generate-release.md +++ b/.agents/workflows/generate-release.md @@ -259,7 +259,7 @@ git pull origin main VERSION=$(node -p "require('./package.json').version") # Extracts the changelog section for this version -NOTES=$(awk '/^## \['"$VERSION"'\]/{flag=1; next} /^## \[[0-9]+/{if(flag) exit} flag' CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') +NOTES=$(awk "/^## \\[$VERSION\\]/{flag=1; next} /^---/{if(flag) {flag=0; exit}} flag" CHANGELOG.md | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') if [ -z "$NOTES" ]; then NOTES="OmniRoute v$VERSION Release"; fi git tag -a "v$VERSION" -m "Release v$VERSION"