Skip to content

feat: New deploy settings#5073

Merged
chronark merged 37 commits intomainfrom
deploy-settings
Feb 19, 2026
Merged

feat: New deploy settings#5073
chronark merged 37 commits intomainfrom
deploy-settings

Conversation

@ogzhanolguncu
Copy link
Contributor

@ogzhanolguncu ogzhanolguncu commented Feb 18, 2026

What does this PR do?

This is a complete overhaul of deploy settings and preparation for deploy onboarding.

This PR:

  • Updates extends <SettingCard /> to have an expandable section.
  • Adds a nice utility for auto bordering <SettingCard /> called <SettingCardGroup />

Example:

      <>
        <div className="flex flex-col w-full">
          <SettingCardGroup>
            <GitHubSettings />
            <RootDirectorySettings />
            <DockerfileSettings />
            <PortSettings />
          </SettingCardGroup>
        </div>
        <SettingsGroup
          icon={<CircleHalfDottedClock iconSize="md-medium" />}
          title="Runtime settings"
        >
          <SettingCardGroup>
            <Regions />
            <Instances />
            <Cpu />
            <Memory />
            {/* Temporarily disabled */}
            {/* <Storage /> */}
            <Healthcheck />
            {/* Temporarily disabled */}
            {/* <Scaling /> */}
          </SettingCardGroup>
        </SettingsGroup>
        <SettingsGroup icon={<Gear iconSize="md-medium" />} title="Advanced configurations">
          <SettingCardGroup>
            <Command />
            <EnvVars />
            <CustomDomains />
          </SettingCardGroup>
        </SettingsGroup>
      </>

They will be automatically wrapped with correct borders so no need for border="both" mess.

  • Adds <SettingsGroup icon={<Icon />} title=".."> component to group SettingCards which later can be moved to @unkey/ui
  • Creates 3 set of configs - Build, Runtime and Advanced
  • Creates individual tRPC routes for mutations so every config can mutate one field only.
  • Adds a really nice Environment Variable form where you can drag and drop, copy paste content and env file. During this process there will be nice indicator for these operations above.

How to test

  • Play around with settings locally
  • Try to connect to Github
  • Try to add env vars
  • Try to add custom domains
  • Make a general sweep for setting pages coz this PR touches setting pages implicitly

@vercel
Copy link

vercel bot commented Feb 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview, Comment Feb 18, 2026 8:17pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
engineering Ignored Ignored Preview Feb 18, 2026 8:17pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Splits monolithic settings into many focused UI modules, adds granular TRPC build/runtime endpoints, reorganizes router exports, enhances SettingCard and UI primitives, introduces sliders/icons, and implements EnvVars import/decryption/diff utilities; several legacy composite components were removed.

Changes

Cohort / File(s) Summary
Border styling (visual only)
web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx, web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/delete-protection.tsx, web/apps/dashboard/app/(app)/[workspaceSlug]/ratelimits/[namespaceId]/settings/components/settings-client.tsx, web/apps/dashboard/app/(app)/[workspaceSlug]/settings/general/update-workspace-name.tsx
Minor CSS changes adding border-grayA-4 to existing border classes; no logic changes.
Removed legacy composite UI modules
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings.tsx, .../runtime-application-settings.tsx, .../runtime-scaling-settings.tsx, .../github-app-card.tsx, .../github-settings-client.tsx, .../_components/repository-card.tsx
Deleted large, composite settings components (UI and wiring) — functionality redistributed to smaller modules.
New build settings UI
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx, .../root-directory-settings.tsx, .../port-settings.tsx, .../github-settings/**
Added focused components for Dockerfile, docker context, root directory, port, and GitHub integration with forms, zod validation, TRPC mutations and toasts.
New runtime settings UI
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/*
Introduces Cpu, Memory, Instances, Regions, Scaling, Healthcheck, Storage and related forms/sliders, validation and TRPC update flows.
Advanced settings & EnvVars
.../advanced-settings/command.tsx, .../custom-domains/**, .../env-vars/index.tsx, .../env-vars/schema.ts, .../env-vars/use-drop-zone.ts, .../env-vars/use-decrypted-values.ts, .../env-vars/utils.ts, .../env-var-row.tsx
New command editor, custom domains UI changes, full EnvVars feature: form, schema, drag/paste import hook, decrypted-values hook, diffing utilities, row component, TRPC mutations integration.
Shared settings UI & utilities
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/form-setting-card.tsx, .../selected-config.tsx, .../setting-description.tsx, .../settings-group.tsx, .../slider-utils.ts
Adds reusable FormSettingCard, SelectedConfig, SettingDescription, SettingsGroup, and slider helpers for consistent settings UX.
Settings page layout
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/page.tsx
Reworks page to a static multi-section layout (Core, Runtime, Advanced); removes prior dynamic export and environment-driven conditional rendering.
TRPC: removed batch endpoints
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/update-build.ts, .../update-runtime.ts
Removed previous composite updateEnvironmentBuildSettings and updateEnvironmentRuntimeSettings procedures.
TRPC: new granular endpoints
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/build/update-dockerfile.ts, .../build/update-docker-context.ts, .../runtime/{update-cpu,update-memory,update-port,update-command,update-healthcheck,update-regions,update-instances}.ts, .../get-available-regions.ts
Introduces discrete runtime/build mutations and a query for available regions; each updates specific fields in environment tables.
TRPC router reorganization
web/apps/dashboard/lib/trpc/routers/index.ts
Replaces flat updateBuild/updateRuntime with nested environmentSettings.runtime and environmentSettings.build routes and adds getAvailableRegions.
Custom domains types/imports
web/apps/dashboard/.../custom-domains-section/add-custom-domain.tsx, .../custom-domains-section/index.tsx, .../custom-domains-section/types.ts
Switches type imports to central collections module, moves/exports a local skeleton component, removes redundant type re-exports.
Icons added
web/internal/icons/src/icons/connections3.tsx, file-settings.tsx, folder-link.tsx, heart-pulse.tsx, nodes-2.tsx, scan-code.tsx, square-terminal.tsx, web/internal/icons/src/index.ts
Adds seven SVG icon components and re-exports them from the icons barrel.
UI library changes
web/internal/ui/src/components/settings-card.tsx, .../slider.tsx, .../form/form-checkbox.tsx, .../form/index.tsx, web/internal/ui/package.json, web/internal/ui/src/index.ts
Converts SettingCard to a client component with expandability, adds new types and SettingCardGroup, introduces a Slider wrapper (Radix UI) and dependency, conditionally renders FormCheckbox label, re-exports form-helpers.
Build TRPC helpers
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/build/update-dockerfile.ts, .../update-docker-context.ts
New mutations to update dockerfile and docker context independently.
Env-vars helpers & utilities
.../env-vars/schema.ts, .../env-vars/use-drop-zone.ts, .../env-vars/use-decrypted-values.ts, .../env-vars/utils.ts
Adds zod schemas, drop/paste import hook, decryption hook, diffing/grouping helpers used by EnvVars UI.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant Browser as Settings UI (client)
  participant TRPC as tRPC Router
  participant DB as Database

  User->>Browser: Edit/submit setting (form, slider, env import)
  Browser->>TRPC: call environmentSettings.build.* or environmentSettings.runtime.*
  TRPC->>DB: UPDATE/INSERT environmentBuildSettings / environmentRuntimeSettings
  DB-->>TRPC: acknowledgement
  TRPC-->>Browser: mutation success
  Browser->>TRPC: invalidate & refetch queries
  TRPC->>DB: SELECT refreshed settings
  DB-->>TRPC: return data
  TRPC-->>Browser: updated data
  Browser-->>User: show toast, update UI
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: New deploy settings' directly summarizes the main change—a complete overhaul of deploy settings UI and configuration. It is concise, clear, and relates to the primary purpose of the PR.
Description check ✅ Passed The PR description adequately covers what the PR does, includes specific examples of the new component usage, lists key features added (SettingCard expandability, SettingCardGroup, SettingsGroup, three config sets, individual tRPC routes, enhanced env var editor), and provides testing guidance. While the template's 'Type of change' and 'Checklist' sections are not explicitly filled out, the core narrative content is comprehensive and directly addresses the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch deploy-settings

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel vercel bot temporarily deployed to Preview – dashboard February 18, 2026 15:24 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 18, 2026 15:42 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 18, 2026 17:16 Inactive
@ogzhanolguncu ogzhanolguncu marked this pull request as ready for review February 18, 2026 17:30
@ogzhanolguncu
Copy link
Contributor Author

There will be second iteration where I move these all the tanstack db then mutation will be more organized

@vercel vercel bot temporarily deployed to Preview – engineering February 18, 2026 17:33 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 18, 2026 17:33 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx (1)

40-41: ⚠️ Potential issue | 🔴 Critical

Replace as any with proper Zod input/output typing

The as any cast bypasses type checking. Since keyBytesSchema uses z.coerce.number() (which has different input/output types), you need explicit typing. With @hookform/resolvers@5.2.2 installed, use:

useForm<z.input<typeof formSchema>, any, z.output<typeof formSchema>>({
  ...createApiFormConfig(formSchema),
  resolver: zodResolver(formSchema),
  // ... rest of config
})

Alternatively, let React Hook Form infer types from the resolver by removing the explicit generic and letting it match the resolver output. Remove the as any cast entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx
around lines 40 - 41, The current cast "as any" on the resolver bypasses type
safety; update the useForm call that references resolver, formSchema,
keyBytesSchema and createApiFormConfig to use correct Zod input/output generics
instead of "as any" — either call useForm with explicit generics
useForm<z.input<typeof formSchema>, any, z.output<typeof formSchema>> and pass
resolver: zodResolver(formSchema) (removing the cast), or drop the explicit
useForm generic so React Hook Form infers types from zodResolver(formSchema);
ensure the resolver: zodResolver(formSchema) line no longer uses "as any".
🟡 Minor comments (11)
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/build/update-dockerfile.ts-13-22 (1)

13-22: ⚠️ Potential issue | 🟡 Minor

Silent failure if record doesn't exist; should validate update success.

The mutation updates the database without verifying a matching record exists. If no row matches the workspaceId/environmentId combination, the update silently succeeds with 0 rows affected and returns nothing. Compare with similar mutations in the codebase (e.g., env-vars/delete.ts) which check rowsAffected === 0 and throw a NOT_FOUND error. Either verify the record exists before updating using findFirst, or check the result and throw an error when no rows are affected.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/build/update-dockerfile.ts`
around lines 13 - 22, The mutation currently calls
db.update(environmentBuildSettings).set(...) without verifying a row was
changed, so it can silently no-op; modify the handler in this mutation to either
first lookup the record via a findFirst on environmentBuildSettings filtered by
environmentBuildSettings.workspaceId === ctx.workspace.id and
environmentBuildSettings.environmentId === input.environmentId, or inspect the
update result's rowsAffected and if rowsAffected === 0 throw a TRPCError with
code 'NOT_FOUND'; ensure you reference the same identifiers
(db.update(environmentBuildSettings).set, environmentBuildSettings.workspaceId,
environmentBuildSettings.environmentId, ctx.workspace.id, input.environmentId)
and return/throw consistently as other mutations (e.g., env-vars/delete.ts).
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/instances.tsx-51-55 (1)

51-55: ⚠️ Potential issue | 🟡 Minor

Remove React.FC and use explicit function type annotation.

The file doesn't import React, so using React.FC relies on the implicit global namespace. While this works with the current "jsx": "react-jsx" tsconfig setting, it's unnecessary and creates an undeclared dependency. Use a direct function type instead.

🛠️ Suggested change
-const InstancesForm: React.FC<InstancesFormProps> = ({
+const InstancesForm = ({
   environmentId,
   defaultInstances,
   selectedRegions,
-}) => {
+}: InstancesFormProps) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/instances.tsx
around lines 51 - 55, Replace the React.FC usage on the InstancesForm
declaration with a plain function typed with the props interface: change "const
InstancesForm: React.FC<InstancesFormProps> = ({ ... }) => { ... }" to "function
InstancesForm({ environmentId, defaultInstances, selectedRegions }:
InstancesFormProps) { ... }" (or an equivalent const with explicit function type
like "const InstancesForm = ({...}: InstancesFormProps) => { ... }"); remove any
reliance on the React global type and do not add a React import.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx-39-39 (1)

39-39: ⚠️ Potential issue | 🟡 Minor

Remove React.FC namespace reference and implement save handler.

Line 39 uses React.FC<StorageFormProps> without importing React. While modern JSX doesn't require the import, React.FC is a direct namespace reference that will fail. Additionally, line 69's onSubmit handler only prevents default without persisting changes—the Save button can become enabled but does nothing.

🛠️ Suggested changes
-const StorageForm: React.FC<StorageFormProps> = ({ defaultStorage }) => {
+const StorageForm = ({ defaultStorage }: StorageFormProps) => {
-      onSubmit={(e) => e.preventDefault()}
+      onSubmit={async (e) => {
+        e.preventDefault();
+        // TODO: Implement storage persistence logic
+      }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx
at line 39, The component uses a direct React namespace
(React.FC<StorageFormProps>) without importing React and its onSubmit only
preventsDefault without persisting changes; change the declaration to a plain
typed function (e.g., const StorageForm = ({ defaultStorage }: StorageFormProps)
=> { ... }) to remove the React.FC reference, and implement a save handler
(e.g., handleSave or onSubmit) that actually persists the form state (call your
save API, context updater, or an onSave prop), manage a saving boolean to
disable the Save button while awaiting the request, and handle success/error
feedback so the enabled Save button performs the intended persist operation;
update any references to onSubmit and the Save button to call this new handler.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/form-setting-card.tsx-6-70 (1)

6-70: ⚠️ Potential issue | 🟡 Minor

Wrap with forwardRef to properly forward the ref prop.

The ref prop won't be forwarded to the form element without forwardRef. React treats ref as a special prop and does not pass it through as a normal prop to functional components, making the current API misleading.

🛠️ Suggested change (forwardRef)
-import { Button, SettingCard, type SettingCardBorder } from "@unkey/ui";
-import type React from "react";
+import { Button, SettingCard, type SettingCardBorder } from "@unkey/ui";
+import { forwardRef } from "react";
+import type React from "react";
@@
 type EditableSettingCardProps = {
   icon: React.ReactNode;
@@
   canSave: boolean;
   isSaving: boolean;
 
-  ref?: React.Ref<HTMLFormElement>;
   className?: string;
 };
 
-export const FormSettingCard = ({
+export const FormSettingCard = forwardRef<HTMLFormElement, EditableSettingCardProps>(
+  ({
     icon,
     title,
     description,
     border,
     displayValue,
     onSubmit,
     children,
     canSave,
     isSaving,
-    ref,
     className,
-}: EditableSettingCardProps) => {
+  }, ref) => {
   return (
@@
-  );
-};
+  );
+});
+
+FormSettingCard.displayName = "FormSettingCard";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/form-setting-card.tsx
around lines 6 - 70, The component FormSettingCard currently declares a ref prop
in EditableSettingCardProps but doesn't actually forward React refs; wrap the
component with React.forwardRef to accept a forwardedRef parameter (e.g.,
forwardRef<HTMLFormElement, EditableSettingCardProps>(function
FormSettingCard(props, forwardedRef) { ... })), remove the explicit ref field
from EditableSettingCardProps (or mark it optional and not used), and pass the
forwardedRef to the form element's ref attribute instead of the props.ref;
ensure the exported FormSettingCard is the forwardRef-wrapped function so
consumers can attach refs to the underlying <form>.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx-44-50 (1)

44-50: ⚠️ Potential issue | 🟡 Minor

Type mismatch: environmentId may be undefined but is typed as string.

Same issue as in memory.tsx - environments[0]?.id returns string | undefined, but CpuFormProps declares environmentId: string.

🛡️ Proposed fix
 type CpuFormProps = {
-  environmentId: string;
+  environmentId: string | undefined;
   defaultCpu: number;
 };

As per coding guidelines: "Never compromise type safety"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx
around lines 44 - 50, The CpuFormProps declaration currently types environmentId
as string but the caller passes environments[0]?.id which is string | undefined;
update the prop typing to accept undefined (change CpuFormProps.environmentId to
string | undefined) or ensure the parent guarantees a string before rendering
CpuForm (e.g., only render when environments[0]?.id is defined). Locate CpuForm,
CpuFormProps and the caller that passes environments[0]?.id in cpu.tsx and apply
one of these fixes so the types align without using unsafe assertions.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx-44-50 (1)

44-50: ⚠️ Potential issue | 🟡 Minor

Type mismatch: environmentId may be undefined but is typed as string.

environments[0]?.id on Line 35 returns string | undefined, but MemoryFormProps on Line 48 declares environmentId: string. This violates type safety since the component could receive undefined.

🛡️ Proposed fix
 type MemoryFormProps = {
-  environmentId: string;
+  environmentId: string | undefined;
   defaultMemory: number;
 };

Then handle the undefined case in onSubmit:

 const onSubmit = async (values: MemoryFormValues) => {
+  if (!environmentId) {
+    return;
+  }
   await updateMemory.mutateAsync({
     environmentId,
     memoryMib: values.memory,
   });
 };

As per coding guidelines: "Never compromise type safety: No any, no ! (non-null assertion), no as Type"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx
around lines 44 - 50, The prop environmentId passed into MemoryForm is coming
from environments[0]?.id which can be undefined, so update the
MemoryFormProps/environmentId type to string | undefined (or make it optional)
and then update MemoryForm's onSubmit and any callers to handle the undefined
case safely (e.g., no-op or show validation/error and prevent API calls when
environmentId is undefined); do not use non-null assertions or casts—explicitly
check environmentId inside MemoryForm (and in submit handlers) before
proceeding.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/port-settings.tsx-76-78 (1)

76-78: ⚠️ Potential issue | 🟡 Minor

Empty string fallback for environmentId may cause server errors.

When environmentId is undefined, passing "" to the mutation could result in a failed database query or unexpected behavior. Consider early-returning or disabling submission when environmentId is unavailable.

🛡️ Proposed fix
 const onSubmit = async (values: z.infer<typeof portSchema>) => {
+  if (!environmentId) {
+    return;
+  }
-  await updatePort.mutateAsync({ environmentId: environmentId ?? "", port: values.port });
+  await updatePort.mutateAsync({ environmentId, port: values.port });
 };

Also disable the form when environmentId is missing:

-      canSave={isValid && !isSubmitting && currentPort !== defaultValue}
+      canSave={isValid && !isSubmitting && currentPort !== defaultValue && Boolean(environmentId)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/port-settings.tsx
around lines 76 - 78, The handler onSubmit currently passes environmentId ?? ""
into updatePort.mutateAsync which can send an empty string to the server; change
onSubmit to early-return when environmentId is undefined (or throw/notify) so
updatePort.mutateAsync is only called with a valid environmentId, and also
disable the form submission UI when environmentId is missing (tie the
form/button disabled state to the presence of environmentId) so portSchema
validation and updatePort.mutateAsync are never invoked with an empty string.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/shared.tsx-37-46 (1)

37-46: ⚠️ Potential issue | 🟡 Minor

Handle edge case when fullName lacks a separator

If fullName doesn't contain /, the destructuring yields undefined for repoName, which renders as the literal text "undefined" in the UI.

🛡️ Proposed defensive fix
 export const RepoNameLabel = ({ fullName }: { fullName: string }) => {
-  const [handle, repoName] = fullName.split("/");
+  const parts = fullName.split("/");
+  const handle = parts[0] ?? fullName;
+  const repoName = parts[1];
   return (
     // This max-w-[185px] and w-[185px] in ComboboxSkeleton should match
     <div className="max-w-[185px] truncate">
       <span className="text-[13px] text-gray-12 font-medium">{handle}</span>
-      <span className="text-[13px] text-gray-11">/{repoName}</span>
+      {repoName && <span className="text-[13px] text-gray-11">/{repoName}</span>}
     </div>
   );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/shared.tsx
around lines 37 - 46, The RepoNameLabel component currently destructures
fullName.split("/") so if fullName lacks "/" repoName becomes undefined and
"undefined" shows in the UI; change the logic in RepoNameLabel to defensively
split and handle missing separator (e.g., const parts = fullName.split("/", 2);
const handle = parts[0]; const repoName = parts[1] ?? ""; and only render the
"/" and repoName when repoName is non-empty) so the component displays the
fullName or handle cleanly instead of "undefined".
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-regions.ts-21-21 (1)

21-21: ⚠️ Potential issue | 🟡 Minor

Type assertion as Record<string, number> violates type safety guideline

This cast bypasses type checking. The schema defines regionConfig with .$type<Record<string, number>>(), so Drizzle should infer the correct type. If the query result typing is incomplete, consider creating a typed helper or using Zod to parse the result.

As per coding guidelines: "Never compromise type safety: No any, no ! (non-null assertion), no as Type".

🛡️ Proposed fix using nullish coalescing with type guard
-    const currentConfig = (existing?.regionConfig as Record<string, number>) ?? {};
+    const currentConfig = existing?.regionConfig ?? {};

If the type error persists, ensure the Drizzle schema's .$type<>() is properly propagated to query results, or parse with Zod:

const regionConfigSchema = z.record(z.string(), z.number());
const currentConfig = regionConfigSchema.parse(existing?.regionConfig ?? {});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-regions.ts`
at line 21, Replace the unsafe type assertion on regionConfig—remove "as
Record<string, number>" used when creating currentConfig—and instead
validate/derive a properly typed object: either ensure the Drizzle schema type
propagates to the query so existing.regionConfig is already typed, or
parse/validate existing?.regionConfig with a Zod record schema (e.g.,
regionConfigSchema.parse(existing?.regionConfig ?? {})) or a small type-guard
that returns a Record<string, number> before assigning to currentConfig; update
any references to currentConfig to use the resulting typed value.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/use-drop-zone.ts-96-101 (1)

96-101: ⚠️ Potential issue | 🟡 Minor

Avoid as Node type assertion in drag-leave handling.

DragEvent.relatedTarget is typed as EventTarget | null, not Node. The cast bypasses type safety and would throw a runtime error if relatedTarget is null. Use an instanceof Node check instead, which handles null and provides proper type narrowing.

🔧 Suggested fix
     const handleDragLeave = (e: DragEvent) => {
       e.preventDefault();
       e.stopPropagation();
-      if (e.currentTarget === dropZone && !dropZone.contains(e.relatedTarget as Node)) {
+      const relatedTarget = e.relatedTarget;
+      const leftDropZone =
+        !(relatedTarget instanceof Node) || !dropZone.contains(relatedTarget);
+      if (e.currentTarget === dropZone && leftDropZone) {
         setIsDragging(false);
       }
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/use-drop-zone.ts
around lines 96 - 101, In handleDragLeave, avoid the unsafe cast of
e.relatedTarget as Node; instead check that e.relatedTarget is a Node (e.g.
using "instanceof Node") before calling dropZone.contains, and handle the null
case so you only call dropZone.contains when relatedTarget is non-null and a
Node; update the conditional that references dropZone.contains(e.relatedTarget
as Node) to first guard via an instanceof Node check and then call
setIsDragging(false) accordingly.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/regions.tsx-184-188 (1)

184-188: ⚠️ Potential issue | 🟡 Minor

optional label conflicts with required validation.

The schema enforces at least one region, but the combobox is marked optional. Either remove optional or relax the validation to avoid UX confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/regions.tsx
around lines 184 - 188, The FormCombobox for Regions is marked optional but your
validation schema requires at least one region; either remove the optional prop
from the FormCombobox component (named FormCombobox) so the UI reflects the
required validation, or update the validation rule that enforces at least one
region (the schema that validates selected regions) to allow an empty selection;
pick one consistent approach and update the corresponding code (remove the
optional prop on the FormCombobox or relax the “min items”/required constraint
in the regions validation schema) so the UI and validation match.
🧹 Nitpick comments (9)
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/setting-description.tsx (1)

10-13: Semantic mismatch: <output> element used for static description.

The <output> element is semantically intended for "the result of a calculation or user action" (e.g., form calculation results). For static descriptive text, a <div> or <span> is more appropriate and avoids potential screen reader confusion.

Proposed fix
-      <output className="text-gray-9 flex gap-2 items-start">
+      <div className="text-gray-9 flex gap-2 items-start">
         <CircleInfo iconSize="md-medium" className="flex-shrink-0 mt-[3px]" aria-hidden="true" />
         <span className="flex-1 text-gray-10">{children}</span>
-      </output>
+      </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/setting-description.tsx
around lines 10 - 13, The component is using an <output> element for static
descriptive text which is semantically incorrect; replace the <output
className="text-gray-9 flex gap-2 items-start">...</output> wrapper with a
non-semantic container like a <div> (or <span> if inline) preserving the inner
structure (CircleInfo with aria-hidden and the <span className="flex-1
text-gray-10">{children}</span>) and keep existing className values and layout
so styling and accessibility remain unchanged.
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/build/update-docker-context.ts (1)

8-11: Input validation inconsistency with sibling endpoint.

dockerContext accepts empty strings (z.string()) while updateDockerfile requires non-empty input (z.string().min(1)). The database schema defaults dockerContext to ".", suggesting empty values are likely unintended.

Proposed fix for consistent validation
     z.object({
       environmentId: z.string(),
-      dockerContext: z.string(),
+      dockerContext: z.string().min(1),
     }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/build/update-docker-context.ts`
around lines 8 - 11, The zod input schema here accepts empty dockerContext
values (dockerContext: z.string()) which is inconsistent with the sibling
updateDockerfile endpoint and the DB default of "."; update the input validation
in this file to require a non-empty string (use z.string().min(1) for
dockerContext) so the schema enforces a meaningful value, leaving environmentId
as z.string() unchanged and keeping behavior consistent with updateDockerfile.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx (1)

164-169: Consider reusing formatMemory from deployment-formatters.

parseMemoryDisplay duplicates logic already present in formatMemory (imported on Line 4). The only difference is the return type (tuple vs string).

♻️ Suggested approach

Either extend formatMemory to support tuple output, or extract a shared helper:

function parseMemoryDisplay(mib: number): [string, string] {
  const formatted = formatMemory(mib);
  // Parse the formatted string, or refactor formatMemory to expose parts
  if (mib >= 1024) {
    return [`${(mib / 1024).toFixed(mib % 1024 === 0 ? 0 : 1)}`, "GiB"];
  }
  return [`${mib}`, "MiB"];
}

Alternatively, add a formatMemoryParts utility to deployment-formatters.ts that both components can use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx
around lines 164 - 169, parseMemoryDisplay duplicates formatting logic already
implemented by formatMemory from deployment-formatters; refactor by either
extending formatMemory to return a tuple or by adding a shared helper (e.g.,
formatMemoryParts) in deployment-formatters and update parseMemoryDisplay to
call that helper instead of reimplementing logic; change parseMemoryDisplay to
use the new helper/extended formatMemory so it returns [string, string] based on
the shared implementation.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/scaling.tsx (1)

66-68: Form submission is disabled - component appears incomplete.

The onSubmit handler only calls e.preventDefault() and isSaving is hardcoded to false. The Save button is rendered but does nothing. If this is intentional (as noted in the AI summary as "temporarily disabled"), consider:

  1. Adding a comment explaining the WIP state
  2. Hiding the Save button entirely, or
  3. Disabling the expandable form section
📝 Suggested documentation
+// TODO: Wire up TRPC mutation when scaling backend is ready
 export const Scaling = () => {

Or disable saving entirely:

-      canSave={isValid && hasChanges}
+      canSave={false} // Scaling mutation not yet implemented
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/scaling.tsx
around lines 66 - 68, The form currently prevents submit and hardcodes isSaving
to false, leaving the Save button non-functional; update the component that
renders the form (the props onSubmit, canSave, isSaving) so submission is
handled or clearly disabled: implement a submit handler (replace onSubmit={(e)
=> e.preventDefault()} with a function that calls the save routine or dispatches
saveProjectScaling), wire a real isSaving state variable (e.g.,
useState/isSaving or reading from the relevant async mutation) instead of false,
and either hide/disable the Save control when saving is intentionally WIP or add
a clear inline comment explaining it’s intentionally disabled; reference the
props named onSubmit, canSave (uses isValid && hasChanges), and isSaving when
making changes.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/github-no-repo.tsx (1)

71-71: Unconventional JSX attribute syntax

Using placeholder=<span> without curly braces is valid but unconventional. Most style guides prefer wrapping JSX in curly braces for clarity.

♻️ Suggested change for consistency
-          placeholder=<span className="text-left w-full">Select a repository...</span>
+          placeholder={<span className="text-left w-full">Select a repository...</span>}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/github-no-repo.tsx
at line 71, The placeholder prop in the GitHubNoRepo component
(github-no-repo.tsx) uses JSX without curly braces (placeholder=<span
className="...">…), which is unconventional; change it to use an expression by
wrapping the JSX in curly braces (placeholder={<span className="text-left
w-full">Select a repository...</span>}) so the placeholder prop contains a
proper JSX expression for consistency with style guides.
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/shared.tsx (1)

58-63: Semantic issue: <a> nested inside <Button>

Nesting an <a> inside a <Button> creates invalid HTML (interactive element inside interactive element) and can cause accessibility issues. The button's click handler and the anchor's navigation may conflict.

Consider using the Button's asChild pattern (if supported) or rendering the anchor directly with button-like styling.

♻️ Proposed fix using anchor with button styling
-<Button variant={variant} className={className}>
-  <a href={installUrl} className="text-sm text-gray-12" target="_blank" rel="noopener noreferrer">
-    {text}
-  </a>
-</Button>
+<a
+  href={installUrl}
+  target="_blank"
+  rel="noopener noreferrer"
+  className={cn(
+    "inline-flex items-center justify-center text-sm text-gray-12",
+    className,
+  )}
+>
+  {text}
+</a>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/shared.tsx
around lines 58 - 63, The current JSX nests an <a> inside <Button> which is
invalid; update the rendering so the interactive element isn't nested: either
use the Button's asChild prop (e.g., <Button asChild> and pass the <a
href={installUrl} target="_blank" rel="noopener noreferrer"
className=...>{text}</a>) or render the <a> directly with the same button-like
styling and props (use variant/className values) and keep installUrl, target and
rel intact; ensure you remove the nested structure around Button and preserve
accessibility attributes.
web/internal/ui/src/components/settings-card.tsx (1)

148-165: Potential height calculation issue on initial expansion

When isExpanded becomes true, contentRef.current?.scrollHeight may return 0 or an incorrect value on the first render cycle because the content hasn't been measured yet. This could cause a visual glitch where the panel animates from 0px to 0px, then jumps to the correct height on subsequent renders.

Consider using a useLayoutEffect to measure and set the height, or defaulting to scrollHeight || 'auto' for the initial expansion.

♻️ Optional fix using fallback
          style={{
-           maxHeight: isExpanded ? `${contentRef.current?.scrollHeight}px` : "0px",
+           maxHeight: isExpanded ? `${contentRef.current?.scrollHeight || 9999}px` : "0px",
          }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/internal/ui/src/components/settings-card.tsx` around lines 148 - 165, The
expandable panel can measure scrollHeight too late causing a 0px-to-0px
animation; update the component that uses contentRef, isExpanded and expandable
to measure and set the correct height before paint (use useLayoutEffect) and/or
fall back to "auto" on first expansion; specifically, when isExpanded toggles to
true run a useLayoutEffect that reads contentRef.current.scrollHeight and sets a
local measuredHeight state (or directly sets the inline maxHeight) so the inline
style uses the measured value (or "auto" if measurement is not yet available) to
avoid the initial jump.
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-command.ts (1)

13-22: Silent failure when environment settings don't exist

The mutation updates without verifying the row exists. If environmentId is invalid or the settings record is missing, the update silently affects 0 rows with no error returned to the client. This could mask bugs or provide confusing UX.

Consider checking the affected row count or querying first to confirm the record exists.

♻️ Proposed fix with existence check
   .mutation(async ({ ctx, input }) => {
-    await db
+    const result = await db
       .update(environmentRuntimeSettings)
       .set({ command: input.command })
       .where(
         and(
           eq(environmentRuntimeSettings.workspaceId, ctx.workspace.id),
           eq(environmentRuntimeSettings.environmentId, input.environmentId),
         ),
       );
+
+    if (result.rowsAffected === 0) {
+      throw new Error("Environment settings not found");
+    }
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-command.ts`
around lines 13 - 22, The mutation currently performs
db.update(environmentRuntimeSettings).set({ command: input.command }) with the
where clause on environmentRuntimeSettings.workspaceId and
environmentRuntimeSettings.environmentId but does not verify a row was actually
updated; if the record is missing the call silently affects 0 rows. Change the
handler in the mutation to either first select the row (e.g., select from
environmentRuntimeSettings where workspaceId = ctx.workspace.id and
environmentId = input.environmentId) and throw a not-found error if absent, or
capture the update result/rowCount from the db.update call and throw a
not-found/validation error when zero rows were affected so the client receives
an explicit error instead of a silent no-op.
web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-regions.ts (1)

27-35: Silent failure when environment settings don't exist

Same concern as other update mutations: if existing is undefined (no settings row exists), the update proceeds and silently affects 0 rows. The client receives no indication of failure.

♻️ Proposed fix
+    if (!existing) {
+      throw new Error("Environment settings not found");
+    }
+
     await db
       .update(environmentRuntimeSettings)
       .set({ regionConfig })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-regions.ts`
around lines 27 - 35, The update currently runs even when no environment runtime
settings row exists, silently affecting 0 rows; before calling
db.update(...).set({ regionConfig }).where(...), query
environmentRuntimeSettings for the row matching ctx.workspace.id and
input.environmentId (e.g.,
db.select().from(environmentRuntimeSettings).where(...)) and if the result is
undefined throw an appropriate error (e.g., TRPCError with NOT_FOUND) so the
client gets a clear failure instead of a silent no-op; update the code paths in
update-regions.ts to perform this existence check and only run the db.update
when the row is present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/command.tsx:
- Around line 31-32: The component currently passes environmentId ?? "" into
CommandForm which allows submitting while the real environmentId is missing;
update CommandForm usage and its submit path to require a truthy environmentId:
disable the Save button and prevent form submission when environmentId is falsy,
show a loading/disabled state until environmentId is available, and
short-circuit the mutation call in the CommandForm submit handler (or the parent
submit proxy) to avoid calling the mutation with an empty string; touch the
CommandForm component and any submit handler/mutation invocation that reads
environmentId (references: CommandForm, environmentId, the form submit
function/mutation) and apply the same guard for the other occurrences around
lines 73–97.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/index.tsx:
- Around line 138-191: The code uses unsafe `as string` casts for env var ids;
replace them by narrowing with a type predicate (e.g. function hasId(v: EnvVar):
v is EnvVar & { id: string }) and use it everywhere: filter
originalVars/currentIds/toDelete/toCreate/toUpdate by hasId so maps and new
Map(originalVars.map((v)=>[v.id, v])) and uses of v.id (in
deleteMutation/updateMutation) no longer require `as string`; ensure toCreate
still filters out empty key/value and use toTrpcType(v.secret) unchanged.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx:
- Around line 36-47: The form's defaultValues are only applied on mount so when
defaultValue changes the UI and canSave state can become stale; inside the
Dockerfile settings component call the reset function returned by useForm
whenever defaultValue changes (e.g., useEffect watching defaultValue and
invoking reset({ dockerfile: defaultValue })) so useWatch/currentDockerfile and
form validity reflect the loaded settings; reference the existing useForm hook
(resolver: zodResolver(dockerfileSchema), defaultValues) and the
currentDockerfile/useWatch to locate where to add the effect.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/root-directory-settings.tsx:
- Around line 36-47: The form is initialized with useForm({... defaultValues: {
dockerContext: defaultValue }}) but defaultValues only applies on first render
so when the query-provided defaultValue changes you must call the form API to
update state; add a useEffect that watches defaultValue and calls reset({
dockerContext: defaultValue }) (imported from useForm result) to update the
controlled value returned by useWatch and prevent saving stale values —
reference the useForm call, rootDirectorySchema, defaultValue,
useWatch/currentDockerContext, and reset.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx:
- Around line 16-25: Fix the typo in the CPU_OPTIONS constant: the "32 vCPU"
entry in CPU_OPTIONS currently has value 32786 but should be 32768 (32 * 1024).
Update the object in CPU_OPTIONS where label is "32 vCPU" to use value 32768 so
valueToIndex and CPU allocation match server values.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/healthcheck/index.tsx:
- Around line 32-37: The form seeds default field values when
runtimeSettings.healthcheck is null but never marks the form as changed, so
users cannot "enable" a healthcheck; fix by adding an existence flag into the
seeded/initial values and include it in the hasChanges comparison: compute const
initialHealthcheckExists = Boolean(healthcheck), include exists:
initialHealthcheckExists in the initial/default HealthcheckFormValues (alongside
method/path/interval using secondsToInterval), ensure the live form state sets
exists: true when the user toggles/enables the healthcheck, and update the
hasChanges check to compare this exists flag as well as method/path/interval so
creating a new healthcheck is detected as a change.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/instances.tsx:
- Around line 31-32: Remove the redundant type assertion on regionConfig: drop
"as Record<string, number>" from the expression that reads
settingsData?.runtimeSettings?.regionConfig and either rely on the declared DB
type inference or add an explicit variable annotation (e.g., declare
regionConfig: Record<string, number> = ...). Update the same pattern where
regionConfig is set in regions.tsx (the
settingsData?.runtimeSettings?.regionConfig usage) so both locations use a
direct typed variable or inference rather than the "as" assertion.
- Around line 23-41: Guard against a possibly undefined environmentId by making
the prop optional or early-returning before rendering InstancesForm: update
InstancesFormProps/environmentId to accept string | undefined or return null
when environments[0]?.id is undefined and ensure
trpc.deploy.environmentSettings.get.useQuery and any downstream mutations are
only called when environmentId is present; replace the unsafe cast on
settingsData?.runtimeSettings?.regionConfig by narrowing/parsing it at runtime
(e.g., check settingsData?.runtimeSettings?.regionConfig is an object, map its
entries and coerce values to numbers) and derive
selectedRegions/defaultInstances from that safe mapping; finally remove or
correctly import React for any React.FC usage (or prefer explicit function
component types) so the component type is valid.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/regions.tsx:
- Around line 210-219: The remove-region control uses a plain <span> with only
onClick, making it inaccessible to keyboard users; update the span wrapping the
XMark in regions.tsx to include role="button", tabIndex={0}, and an onKeyDown
handler that listens for Enter and Space and calls e.stopPropagation();
removeRegion(r); (also preventDefault for Space to avoid scrolling) so keyboard
activation mirrors the onClick behavior; keep the existing onClick and className
intact so removeRegion, XMark, and the stopPropagation logic remain central and
consistent.
- Around line 37-39: The code unsafely casts
settingsData?.runtimeSettings?.regionConfig with "as Record<string, number>";
remove that assertion and instead validate/parse
settingsData.runtimeSettings.regionConfig using a zod schema (e.g.,
z.record(z.number()).optional()) before using it — create a schema, run
schema.parse or safeParse on settingsData?.runtimeSettings, fallback to {} on
failure, assign the validated object to regionConfig, and then compute
defaultRegions = Object.keys(regionConfig); update any error handling or logging
around the validation to surface malformed API shapes.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx:
- Around line 69-71: The form's onSubmit is a no-op so the Save button
(controlled by canSave/isValid/hasChanges) does nothing; replace the noop with a
real submit handler (e.g., implement handleSubmit or wire onSubmit to
submitStorage) that calls the backend mutation (e.g.,
updateProjectStorage/updateStorageMutation), toggles isSaving while awaiting the
request, handles success by updating local state and clearing hasChanges, and
surfaces errors to the UI/logging; if backend work isn't ready, instead disable
the Save action by setting canSave to false or change the button to a "coming
soon" state so enabled Save cannot be clicked.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-cpu.ts`:
- Around line 8-11: The schema for the incoming payload accepts floats and
negatives; update the z.object validation for cpuMillicores in update-cpu.ts to
enforce integer and non-negative constraints (e.g., replace z.number() with a
validator that requires integers and a minimum of 0 such as
z.number().int().min(0)) so only valid millicore integers are accepted before
persisting to the integer-backed column.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-instances.ts`:
- Around line 31-34: The loop that builds regionConfig uses
process.env.AVAILABLE_REGIONS and splits on commas but can insert an
empty-string key when AVAILABLE_REGIONS is "" or contains extra commas; update
the logic that iterates regions so you trim each entry and skip empty/whitespace
entries (e.g., use regionsEnv.split(",").map(r=>r.trim()).filter(Boolean) or
equivalent) before assigning regionConfig[region] = input.replicasPerRegion;
ensure the change is applied where regionConfig, process.env.AVAILABLE_REGIONS,
and input.replicasPerRegion are referenced.
- Line 21: Remove the unsafe assertion on regionConfig and give currentConfig an
explicit type instead: replace the `as Record<string, number>` cast on the
expression that uses `existing?.regionConfig` with a type annotation on
`currentConfig` (e.g., declare `currentConfig: Record<string, number> =
existing?.regionConfig ?? {}`) so you keep type-safety while preserving the
fallback behavior; update the assignment that references
`existing`/`regionConfig` and ensure no other `as` or non-null assertions remain
in this statement.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-memory.ts`:
- Around line 8-11: The zod schema for the update-memory mutation currently
accepts any number for memoryMib; change the schema entry so memoryMib is
validated as a positive integer (e.g., z.number().int().positive().min(1) or
z.number().int().positive()) to prevent negatives and decimals from passing;
update the z.object that contains { environmentId: z.string(), memoryMib:
z.number() } in update-memory.ts to use the stricter zod chain for memoryMib so
server-side validation matches the DB int constraint and mirrors the checks used
in updatePort/updateInstances.

---

Outside diff comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-bytes.tsx:
- Around line 40-41: The current cast "as any" on the resolver bypasses type
safety; update the useForm call that references resolver, formSchema,
keyBytesSchema and createApiFormConfig to use correct Zod input/output generics
instead of "as any" — either call useForm with explicit generics
useForm<z.input<typeof formSchema>, any, z.output<typeof formSchema>> and pass
resolver: zodResolver(formSchema) (removing the cast), or drop the explicit
useForm generic so React Hook Form infers types from zodResolver(formSchema);
ensure the resolver: zodResolver(formSchema) line no longer uses "as any".

---

Nitpick comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/github-no-repo.tsx:
- Line 71: The placeholder prop in the GitHubNoRepo component
(github-no-repo.tsx) uses JSX without curly braces (placeholder=<span
className="...">…), which is unconventional; change it to use an expression by
wrapping the JSX in curly braces (placeholder={<span className="text-left
w-full">Select a repository...</span>}) so the placeholder prop contains a
proper JSX expression for consistency with style guides.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/github-settings/shared.tsx:
- Around line 58-63: The current JSX nests an <a> inside <Button> which is
invalid; update the rendering so the interactive element isn't nested: either
use the Button's asChild prop (e.g., <Button asChild> and pass the <a
href={installUrl} target="_blank" rel="noopener noreferrer"
className=...>{text}</a>) or render the <a> directly with the same button-like
styling and props (use variant/className values) and keep installUrl, target and
rel intact; ensure you remove the nested structure around Button and preserve
accessibility attributes.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx:
- Around line 164-169: parseMemoryDisplay duplicates formatting logic already
implemented by formatMemory from deployment-formatters; refactor by either
extending formatMemory to return a tuple or by adding a shared helper (e.g.,
formatMemoryParts) in deployment-formatters and update parseMemoryDisplay to
call that helper instead of reimplementing logic; change parseMemoryDisplay to
use the new helper/extended formatMemory so it returns [string, string] based on
the shared implementation.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/scaling.tsx:
- Around line 66-68: The form currently prevents submit and hardcodes isSaving
to false, leaving the Save button non-functional; update the component that
renders the form (the props onSubmit, canSave, isSaving) so submission is
handled or clearly disabled: implement a submit handler (replace onSubmit={(e)
=> e.preventDefault()} with a function that calls the save routine or dispatches
saveProjectScaling), wire a real isSaving state variable (e.g.,
useState/isSaving or reading from the relevant async mutation) instead of false,
and either hide/disable the Save control when saving is intentionally WIP or add
a clear inline comment explaining it’s intentionally disabled; reference the
props named onSubmit, canSave (uses isValid && hasChanges), and isSaving when
making changes.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/shared/setting-description.tsx:
- Around line 10-13: The component is using an <output> element for static
descriptive text which is semantically incorrect; replace the <output
className="text-gray-9 flex gap-2 items-start">...</output> wrapper with a
non-semantic container like a <div> (or <span> if inline) preserving the inner
structure (CircleInfo with aria-hidden and the <span className="flex-1
text-gray-10">{children}</span>) and keep existing className values and layout
so styling and accessibility remain unchanged.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/build/update-docker-context.ts`:
- Around line 8-11: The zod input schema here accepts empty dockerContext values
(dockerContext: z.string()) which is inconsistent with the sibling
updateDockerfile endpoint and the DB default of "."; update the input validation
in this file to require a non-empty string (use z.string().min(1) for
dockerContext) so the schema enforces a meaningful value, leaving environmentId
as z.string() unchanged and keeping behavior consistent with updateDockerfile.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-command.ts`:
- Around line 13-22: The mutation currently performs
db.update(environmentRuntimeSettings).set({ command: input.command }) with the
where clause on environmentRuntimeSettings.workspaceId and
environmentRuntimeSettings.environmentId but does not verify a row was actually
updated; if the record is missing the call silently affects 0 rows. Change the
handler in the mutation to either first select the row (e.g., select from
environmentRuntimeSettings where workspaceId = ctx.workspace.id and
environmentId = input.environmentId) and throw a not-found error if absent, or
capture the update result/rowCount from the db.update call and throw a
not-found/validation error when zero rows were affected so the client receives
an explicit error instead of a silent no-op.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-regions.ts`:
- Around line 27-35: The update currently runs even when no environment runtime
settings row exists, silently affecting 0 rows; before calling
db.update(...).set({ regionConfig }).where(...), query
environmentRuntimeSettings for the row matching ctx.workspace.id and
input.environmentId (e.g.,
db.select().from(environmentRuntimeSettings).where(...)) and if the result is
undefined throw an appropriate error (e.g., TRPCError with NOT_FOUND) so the
client gets a clear failure instead of a silent no-op; update the code paths in
update-regions.ts to perform this existence check and only run the db.update
when the row is present.

In `@web/internal/ui/src/components/settings-card.tsx`:
- Around line 148-165: The expandable panel can measure scrollHeight too late
causing a 0px-to-0px animation; update the component that uses contentRef,
isExpanded and expandable to measure and set the correct height before paint
(use useLayoutEffect) and/or fall back to "auto" on first expansion;
specifically, when isExpanded toggles to true run a useLayoutEffect that reads
contentRef.current.scrollHeight and sets a local measuredHeight state (or
directly sets the inline maxHeight) so the inline style uses the measured value
(or "auto" if measurement is not yet available) to avoid the initial jump.

Comment on lines +31 to +32
return <CommandForm environmentId={environmentId ?? ""} defaultCommand={defaultCommand} />;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard submissions when environmentId is missing.

environmentId ?? "" means a user can edit and submit before the ID loads, sending an empty string to the mutation (likely a no-op with a misleading success). Gate the submit and disable save when environmentId is falsy.

🔒 Suggested guard
-  const onSubmit = async (values: CommandFormValues) => {
+  const onSubmit = async (values: CommandFormValues) => {
+    if (!environmentId) return;
     const trimmed = values.command.trim();
     const command = trimmed === "" ? [] : trimmed.split(/\s+/).filter(Boolean);
     await updateCommand.mutateAsync({ environmentId, command });
   };
@@
-      canSave={isValid && !isSubmitting && hasChanges}
+      canSave={isValid && !isSubmitting && hasChanges && Boolean(environmentId)}

Also applies to: 73-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/command.tsx
around lines 31 - 32, The component currently passes environmentId ?? "" into
CommandForm which allows submitting while the real environmentId is missing;
update CommandForm usage and its submit path to require a truthy environmentId:
disable the Save button and prevent form submission when environmentId is falsy,
show a loading/disabled state until environmentId is available, and
short-circuit the mutation call in the CommandForm submit handler (or the parent
submit proxy) to avoid calling the mutation with an empty string; touch the
CommandForm component and any submit handler/mutation invocation that reads
environmentId (references: CommandForm, environmentId, the form submit
function/mutation) and apply the same guard for the other occurrences around
lines 73–97.

Comment on lines +36 to +47
const {
register,
handleSubmit,
formState: { isValid, isSubmitting, errors },
control,
} = useForm<z.infer<typeof dockerfileSchema>>({
resolver: zodResolver(dockerfileSchema),
mode: "onChange",
defaultValues: { dockerfile: defaultValue },
});

const currentDockerfile = useWatch({ control, name: "dockerfile" });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset the form when defaultValue changes.

react-hook-form applies defaultValues only on mount, so when settings load the input can stay stale and canSave flips true without user edits, risking an unintended overwrite. Add a reset effect tied to defaultValue.

🐛 Proposed fix
@@
-import { useForm, useWatch } from "react-hook-form";
+import { useEffect } from "react";
+import { useForm, useWatch } from "react-hook-form";
@@
-  const {
-    register,
-    handleSubmit,
-    formState: { isValid, isSubmitting, errors },
-    control,
-  } = useForm<z.infer<typeof dockerfileSchema>>({
+  const {
+    register,
+    handleSubmit,
+    formState: { isValid, isSubmitting, errors },
+    control,
+    reset,
+  } = useForm<z.infer<typeof dockerfileSchema>>({
     resolver: zodResolver(dockerfileSchema),
     mode: "onChange",
     defaultValues: { dockerfile: defaultValue },
   });
 
+  useEffect(() => {
+    reset({ dockerfile: defaultValue });
+  }, [defaultValue, reset]);
+
   const currentDockerfile = useWatch({ control, name: "dockerfile" });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/dockerfile-settings.tsx
around lines 36 - 47, The form's defaultValues are only applied on mount so when
defaultValue changes the UI and canSave state can become stale; inside the
Dockerfile settings component call the reset function returned by useForm
whenever defaultValue changes (e.g., useEffect watching defaultValue and
invoking reset({ dockerfile: defaultValue })) so useWatch/currentDockerfile and
form validity reflect the loaded settings; reference the existing useForm hook
(resolver: zodResolver(dockerfileSchema), defaultValues) and the
currentDockerfile/useWatch to locate where to add the effect.

Comment on lines +36 to +47
const {
register,
handleSubmit,
formState: { isValid, isSubmitting, errors },
control,
} = useForm<z.infer<typeof rootDirectorySchema>>({
resolver: zodResolver(rootDirectorySchema),
mode: "onChange",
defaultValues: { dockerContext: defaultValue },
});

const currentDockerContext = useWatch({ control, name: "dockerContext" });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the file exists and examine its content
fd "root-directory-settings.tsx" web/apps/dashboard

# Check the file size before reading
wc -l web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/build-settings/root-directory-settings.tsx 2>/dev/null || find web/apps/dashboard -name "root-directory-settings.tsx" -type f

Repository: unkeyed/unkey

Length of output: 340


🏁 Script executed:

#!/bin/bash
# Find the file more reliably
find web/apps/dashboard -name "root-directory-settings.tsx" -type f | head -1

Repository: unkeyed/unkey

Length of output: 196


🏁 Script executed:

#!/bin/bash
# Check project structure
ls -la web/apps/dashboard/app/ 2>/dev/null || echo "Path not found, checking alternatives"

Repository: unkeyed/unkey

Length of output: 1021


🏁 Script executed:

#!/bin/bash
# Read the file to understand the implementation
cat -n "web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/root-directory-settings.tsx"

Repository: unkeyed/unkey

Length of output: 4342


🏁 Script executed:

#!/bin/bash
# Also check the referenced default-prefix.tsx from the learning
find web/apps/dashboard -name "default-prefix.tsx" -type f

Repository: unkeyed/unkey

Length of output: 153


🏁 Script executed:

#!/bin/bash
# Check the referenced default-prefix.tsx from the learning
cat -n "web/apps/dashboard/app/(app)/[workspaceSlug]/apis/[apiId]/settings/components/default-prefix.tsx" | head -100

Repository: unkeyed/unkey

Length of output: 3649


🏁 Script executed:

#!/bin/bash
# Check if there's a useEffect dependency on defaultValue anywhere in the codebase
rg -n "useEffect.*defaultValue|reset.*defaultValue" web/apps/dashboard -g "*.tsx" -A 3

Repository: unkeyed/unkey

Length of output: 2297


Reset form state when defaultValue changes to prevent saving stale values.

useForm only applies defaultValues on first render. When the query resolves and defaultValue updates, the form field stays at its initial value, allowing unintended saves. This pattern is already used elsewhere in the codebase for query-driven form values.

🔧 Minimal fix
+import { useEffect } from "react";
 import { useForm, useWatch } from "react-hook-form";
 
 const RootDirectoryForm = ({
   environmentId,
   defaultValue,
 }: {
   environmentId: string;
   defaultValue: string;
 }) => {
   const utils = trpc.useUtils();
 
   const {
     register,
     handleSubmit,
     formState: { isValid, isSubmitting, errors },
     control,
+    reset,
   } = useForm<z.infer<typeof rootDirectorySchema>>({
     resolver: zodResolver(rootDirectorySchema),
     mode: "onChange",
     defaultValues: { dockerContext: defaultValue },
   });
 
+  useEffect(() => {
+    reset({ dockerContext: defaultValue });
+  }, [defaultValue, reset]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const {
register,
handleSubmit,
formState: { isValid, isSubmitting, errors },
control,
} = useForm<z.infer<typeof rootDirectorySchema>>({
resolver: zodResolver(rootDirectorySchema),
mode: "onChange",
defaultValues: { dockerContext: defaultValue },
});
const currentDockerContext = useWatch({ control, name: "dockerContext" });
const {
register,
handleSubmit,
formState: { isValid, isSubmitting, errors },
control,
reset,
} = useForm<z.infer<typeof rootDirectorySchema>>({
resolver: zodResolver(rootDirectorySchema),
mode: "onChange",
defaultValues: { dockerContext: defaultValue },
});
useEffect(() => {
reset({ dockerContext: defaultValue });
}, [defaultValue, reset]);
const currentDockerContext = useWatch({ control, name: "dockerContext" });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/build-settings/root-directory-settings.tsx
around lines 36 - 47, The form is initialized with useForm({... defaultValues: {
dockerContext: defaultValue }}) but defaultValues only applies on first render
so when the query-provided defaultValue changes you must call the form API to
update state; add a useEffect that watches defaultValue and calls reset({
dockerContext: defaultValue }) (imported from useForm result) to update the
controlled value returned by useWatch and prevent saving stale values —
reference the useForm call, rootDirectorySchema, defaultValue,
useWatch/currentDockerContext, and reset.

Comment on lines +32 to +37
const healthcheck = settingsData?.runtimeSettings?.healthcheck;
const defaultValues: HealthcheckFormValues = {
method: healthcheck?.method ?? "GET",
path: healthcheck?.path ?? "/health",
interval: healthcheck ? secondsToInterval(healthcheck.intervalSeconds) : "30s",
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Allow enabling a healthcheck when none exists.

When runtimeSettings.healthcheck is null, the form seeds defaults but hasChanges is false, so Save is disabled and users can’t enable a healthcheck with the default values.

🔧 Minimal fix
   const healthcheck = settingsData?.runtimeSettings?.healthcheck;
+  const hasExistingHealthcheck = Boolean(healthcheck);
   const defaultValues: HealthcheckFormValues = {
     method: healthcheck?.method ?? "GET",
     path: healthcheck?.path ?? "/health",
     interval: healthcheck ? secondsToInterval(healthcheck.intervalSeconds) : "30s",
   };
 
-  return <HealthcheckForm environmentId={environmentId ?? ""} defaultValues={defaultValues} />;
+  return (
+    <HealthcheckForm
+      environmentId={environmentId ?? ""}
+      defaultValues={defaultValues}
+      hasExistingHealthcheck={hasExistingHealthcheck}
+    />
+  );
 };
 
 type HealthcheckFormProps = {
   environmentId: string;
   defaultValues: HealthcheckFormValues;
+  hasExistingHealthcheck: boolean;
 };
 
-const HealthcheckForm: React.FC<HealthcheckFormProps> = ({ environmentId, defaultValues }) => {
+const HealthcheckForm: React.FC<HealthcheckFormProps> = ({
+  environmentId,
+  defaultValues,
+  hasExistingHealthcheck,
+}) => {
@@
   const hasChanges =
     currentMethod !== defaultValues.method ||
     currentPath !== defaultValues.path ||
     currentInterval !== defaultValues.interval;
+  const canSave = isValid && !isSubmitting && (hasChanges || !hasExistingHealthcheck);
@@
-      canSave={isValid && !isSubmitting && hasChanges}
+      canSave={canSave}

Also applies to: 112-131

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/healthcheck/index.tsx
around lines 32 - 37, The form seeds default field values when
runtimeSettings.healthcheck is null but never marks the form as changed, so
users cannot "enable" a healthcheck; fix by adding an existence flag into the
seeded/initial values and include it in the hasChanges comparison: compute const
initialHealthcheckExists = Boolean(healthcheck), include exists:
initialHealthcheckExists in the initial/default HealthcheckFormValues (alongside
method/path/interval using secondsToInterval), ensure the live form state sets
exists: true when the user toggles/enables the healthcheck, and update the
hasChanges check to compare this exists flag as well as method/path/interval so
creating a new healthcheck is detected as a change.

Comment on lines +69 to +71
onSubmit={(e) => e.preventDefault()}
canSave={isValid && hasChanges}
isSaving={false}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Save is currently a no-op.

Line 69 prevents default submission but doesn’t persist the updated storage value. That means the Save button can be enabled and still do nothing.

Consider wiring this to a backend mutation (or disabling Save until it’s supported).

If you want, I can help wire this to a new storage mutation or add a temporary disabled/“coming soon” state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/storage.tsx
around lines 69 - 71, The form's onSubmit is a no-op so the Save button
(controlled by canSave/isValid/hasChanges) does nothing; replace the noop with a
real submit handler (e.g., implement handleSubmit or wire onSubmit to
submitStorage) that calls the backend mutation (e.g.,
updateProjectStorage/updateStorageMutation), toggles isSaving while awaiting the
request, handles success by updating local state and clearing hasChanges, and
surfaces errors to the UI/logging; if backend work isn't ready, instead disable
the Save action by setting canSave to false or change the button to a "coming
soon" state so enabled Save cannot be clicked.

Comment on lines +8 to +11
z.object({
environmentId: z.string(),
cpuMillicores: z.number(),
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check its content
fd "update-cpu.ts" --type f

Repository: unkeyed/unkey

Length of output: 142


🏁 Script executed:

# Check the actual content of the file to confirm the current schema
cat -n "web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-cpu.ts"

Repository: unkeyed/unkey

Length of output: 900


🏁 Script executed:

# Search for cpuMillicores usage patterns across the codebase
rg "cpuMillicores" --type ts --type tsx -B 2 -A 2 | head -100

Repository: unkeyed/unkey

Length of output: 84


🏁 Script executed:

# Check for database schema or migration files related to cpu_millicores
find . -type f \( -name "*.sql" -o -name "*migration*" -o -name "*schema*" \) | xargs rg "cpu_millicores" 2>/dev/null | head -50

Repository: unkeyed/unkey

Length of output: 1061


🏁 Script executed:

# Check zod version in package.json
rg '"zod"' package.json

Repository: unkeyed/unkey

Length of output: 109


🏁 Script executed:

# Search for other cpuMillicores usages with validation
rg "cpuMillicores" --type ts -B 5 -A 5 | head -200

Repository: unkeyed/unkey

Length of output: 17837


🏁 Script executed:

# Look for environment runtime settings schema definition
fd "schema" --type f | xargs rg "environmentRuntimeSettings\|cpuMillicores" -B 2 -A 2 | head -100

Repository: unkeyed/unkey

Length of output: 39


🏁 Script executed:

# Check if there are any constants or documentation about valid CPU values
rg "cpu\|CPU\|millicores\|millicor" --type ts --type js -i -B 1 -A 1 | grep -i "min\|max\|valid\|range\|limit" | head -50

Repository: unkeyed/unkey

Length of output: 39


Add integer and minimum constraints to cpuMillicores schema validation.

The schema currently accepts floats and negative values, which can persist invalid data into an integer-backed column (floats truncate, negatives are invalid for resource allocation). Per coding guidelines, enforce constraints at the boundary.

Suggested change
-      cpuMillicores: z.number(),
+      cpuMillicores: z.number().int().min(1),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
z.object({
environmentId: z.string(),
cpuMillicores: z.number(),
}),
z.object({
environmentId: z.string(),
cpuMillicores: z.number().int().min(1),
}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-cpu.ts`
around lines 8 - 11, The schema for the incoming payload accepts floats and
negatives; update the z.object validation for cpuMillicores in update-cpu.ts to
enforce integer and non-negative constraints (e.g., replace z.number() with a
validator that requires integers and a minimum of 0 such as
z.number().int().min(0)) so only valid millicore integers are accepted before
persisting to the integer-backed column.

),
});

const currentConfig = (existing?.regionConfig as Record<string, number>) ?? {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the schema definition for regionConfig
find . -type f -name "*.ts" -path "*schema*" | head -20

Repository: unkeyed/unkey

Length of output: 2341


🏁 Script executed:

# Check the specific file mentioned in the request
rg -n "regionConfig" --type ts -B3 -A3

Repository: unkeyed/unkey

Length of output: 8744


🏁 Script executed:

# Examine the update-instances.ts file to understand context
head -60 web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-instances.ts

Repository: unkeyed/unkey

Length of output: 1536


Remove the as Record<string, number> assertion to maintain type safety.

The regionConfig is already properly typed in the schema as Record<string, number> with .notNull(), so the assertion is redundant and violates the type safety guideline. Replace it with an explicit type annotation instead.

🛠️ Suggested change
-    const currentConfig = (existing?.regionConfig as Record<string, number>) ?? {};
+    const currentConfig: Record<string, number> = existing?.regionConfig ?? {};

Per coding guidelines: "Never compromise type safety: No any, no ! (non-null assertion), no as Type."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const currentConfig = (existing?.regionConfig as Record<string, number>) ?? {};
const currentConfig: Record<string, number> = existing?.regionConfig ?? {};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-instances.ts`
at line 21, Remove the unsafe assertion on regionConfig and give currentConfig
an explicit type instead: replace the `as Record<string, number>` cast on the
expression that uses `existing?.regionConfig` with a type annotation on
`currentConfig` (e.g., declare `currentConfig: Record<string, number> =
existing?.regionConfig ?? {}`) so you keep type-safety while preserving the
fallback behavior; update the assignment that references
`existing`/`regionConfig` and ensure no other `as` or non-null assertions remain
in this statement.

Comment on lines +31 to +34
const regionsEnv = process.env.AVAILABLE_REGIONS ?? "";
for (const region of regionsEnv.split(",")) {
regionConfig[region] = input.replicasPerRegion;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Filter empty/whitespace regions when AVAILABLE_REGIONS is unset.

Line 31-34 currently writes an empty string key when AVAILABLE_REGIONS is "" (or has trailing commas). That persists invalid region keys.

🛠️ Suggested change
-      const regionsEnv = process.env.AVAILABLE_REGIONS ?? "";
-      for (const region of regionsEnv.split(",")) {
+      const regionsEnv = process.env.AVAILABLE_REGIONS ?? "";
+      for (const region of regionsEnv.split(",").map((r) => r.trim()).filter(Boolean)) {
         regionConfig[region] = input.replicasPerRegion;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const regionsEnv = process.env.AVAILABLE_REGIONS ?? "";
for (const region of regionsEnv.split(",")) {
regionConfig[region] = input.replicasPerRegion;
}
const regionsEnv = process.env.AVAILABLE_REGIONS ?? "";
for (const region of regionsEnv.split(",").map((r) => r.trim()).filter(Boolean)) {
regionConfig[region] = input.replicasPerRegion;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-instances.ts`
around lines 31 - 34, The loop that builds regionConfig uses
process.env.AVAILABLE_REGIONS and splits on commas but can insert an
empty-string key when AVAILABLE_REGIONS is "" or contains extra commas; update
the logic that iterates regions so you trim each entry and skip empty/whitespace
entries (e.g., use regionsEnv.split(",").map(r=>r.trim()).filter(Boolean) or
equivalent) before assigning regionConfig[region] = input.replicasPerRegion;
ensure the change is applied where regionConfig, process.env.AVAILABLE_REGIONS,
and input.replicasPerRegion are referenced.

Comment on lines +8 to +11
z.object({
environmentId: z.string(),
memoryMib: z.number(),
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find existing memory bounds/options in dashboard
echo "=== Searching for memory configuration in dashboard ==="
rg -n "memory" web/apps/dashboard/app -g '*.tsx' -g '*.ts' | head -30

echo -e "\n=== Searching for memoryMib usage ==="
rg -rn "memoryMib" web/apps/dashboard/lib -g '*.ts' -g '*.tsx'

echo -e "\n=== Searching for memoryMib in the entire apps/dashboard ==="
rg -rn "memoryMib" web/apps/dashboard -g '*.ts' -g '*.tsx'

Repository: unkeyed/unkey

Length of output: 9357


🏁 Script executed:

#!/bin/bash
# Check the actual file under review
echo "=== Checking update-memory.ts ==="
if [ -f "web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-memory.ts" ]; then
  cat -n "web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-memory.ts"
else
  echo "File not found at specified path"
  find web -name "update-memory.ts" -type f
fi

Repository: unkeyed/unkey

Length of output: 925


🏁 Script executed:

#!/bin/bash
# Search for validation patterns with z.number() in similar contexts
echo "=== Searching for similar z.number() validation patterns ==="
rg -rn "z\.number\(\)" web/apps/dashboard/lib/trpc -g '*.ts' | head -20

echo -e "\n=== Searching for .positive() usage pattern ==="
rg -rn "\.positive\(\)" web/apps/dashboard -g '*.ts' -g '*.tsx' | head -20

Repository: unkeyed/unkey

Length of output: 2060


🏁 Script executed:

#!/bin/bash
# Look for any database schema that defines memoryMib column
echo "=== Searching for memoryMib in schema/database definitions ==="
rg -rn "memoryMib" web -g '*.ts' -g '*.prisma' -g '*.sql'

Repository: unkeyed/unkey

Length of output: 1705


🏁 Script executed:

#!/bin/bash
# Check the client-side memory form validation
echo "=== Full memory.tsx validation schema ==="
if [ -f "web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx" ]; then
  head -50 "web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx"
else
  echo "File not found"
fi

echo -e "\n=== Check for any memory bounds configuration ==="
rg -rn "MEMORY_OPTIONS\|memory.*min\|memory.*max" web/apps/dashboard -g '*.ts' -g '*.tsx' -A 3

Repository: unkeyed/unkey

Length of output: 1752


🏁 Script executed:

#!/bin/bash
# Check the complete memory.tsx file to see the slider and how it enforces bounds
echo "=== Complete memorySchema and related validation ==="
sed -n '27,35p' "web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx"

echo -e "\n=== Check slider implementation and any client-side bounds ==="
rg -rn "Slider.*memory\|valueToIndex\|indexToValue" "web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx" -A 5 -B 2

echo -e "\n=== Check if there's server-side validation elsewhere ==="
rg -rn "environmentRuntimeSettings\|memoryMib" web/apps/dashboard/lib/trpc/routers/deploy/environment-settings -g '*.ts' -A 3

Repository: unkeyed/unkey

Length of output: 466


🏁 Script executed:

#!/bin/bash
# Get the complete memory.tsx file to understand the full validation picture
echo "=== Full memory.tsx file ==="
cat "web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/memory.tsx"

Repository: unkeyed/unkey

Length of output: 5665


🏁 Script executed:

#!/bin/bash
# Check the update mutation call in environment-settings router
echo "=== Environment settings runtime folder content ==="
ls -la web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/

echo -e "\n=== Check all mutations in environment-settings ==="
find web/apps/dashboard/lib/trpc/routers/deploy/environment-settings -name "*.ts" -exec echo "=== {} ===" \; -exec head -20 {} \;

Repository: unkeyed/unkey

Length of output: 8843


🏁 Script executed:

#!/bin/bash
# Check the slider-utils to understand how memory bounds are managed
echo "=== slider-utils implementation ==="
cat web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/shared/slider-utils.ts 2>/dev/null || \
find web/apps/dashboard -name "slider-utils.ts" -exec cat {} \;

Repository: unkeyed/unkey

Length of output: 531


Validate memoryMib as a positive integer.

The DB column is an int, but the schema accepts any number (including negatives and decimals). This allows invalid runtime settings to bypass server-side validation despite UI constraints. Similar mutations in the same file (updatePort, updateInstances) enforce proper boundary validation.

🛠️ Suggested change
-      memoryMib: z.number(),
+      memoryMib: z.number().int().min(1),

Per coding guidelines, input validation at boundaries should make illegal states unrepresentable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
z.object({
environmentId: z.string(),
memoryMib: z.number(),
}),
z.object({
environmentId: z.string(),
memoryMib: z.number().int().min(1),
}),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/lib/trpc/routers/deploy/environment-settings/runtime/update-memory.ts`
around lines 8 - 11, The zod schema for the update-memory mutation currently
accepts any number for memoryMib; change the schema entry so memoryMib is
validated as a positive integer (e.g., z.number().int().positive().min(1) or
z.number().int().positive()) to prevent negatives and decimals from passing;
update the z.object that contains { environmentId: z.string(), memoryMib:
z.number() } in update-memory.ts to use the stricter zod chain for memoryMib so
server-side validation matches the DB int constraint and mirrors the checks used
in updatePort/updateInstances.

@vercel vercel bot temporarily deployed to Preview – engineering February 18, 2026 19:37 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 18, 2026 19:38 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/custom-domains/index.tsx (1)

66-70: Duplicate domain check is case-sensitive and doesn't trim stored values.

The check at line 68 compares the trimmed input against d.domain which may not be trimmed or normalized. Domain names are case-insensitive by DNS standards, so API.example.com and api.example.com would both be allowed but resolve identically.

♻️ Proposed fix for case-insensitive comparison
 const onSubmit = (values: CustomDomainFormValues) => {
   const trimmedDomain = values.domain.trim();
-  if (customDomains.some((d) => d.domain === trimmedDomain)) {
+  const normalizedDomain = trimmedDomain.toLowerCase();
+  if (customDomains.some((d) => d.domain.toLowerCase() === normalizedDomain)) {
     setError("domain", { message: "Domain already registered" });
     return;
   }
   collection.customDomains.insert({
     id: crypto.randomUUID(),
-    domain: trimmedDomain,
+    domain: normalizedDomain,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/custom-domains/index.tsx
around lines 66 - 70, The duplicate-domain check in onSubmit uses trimmedDomain
but compares against raw stored values in customDomains, causing case- and
whitespace-sensitive misses; update the check to normalize both sides by
trimming and lowercasing (e.g., compare trimmedDomain.toLowerCase() against each
d.domain.trim().toLowerCase()) and ensure wherever you add a new domain you
store the normalized form (trimmed/lowercased) so future checks use the same
canonical representation; reference onSubmit, customDomains, trimmedDomain, and
setError when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/custom-domains/index.tsx:
- Around line 24-25: The current assignment of defaultEnvironmentId uses an
empty string fallback which violates the schema (z.string().min(1)) when
environments is empty; change the logic so you do not produce "" — instead
detect environments.length === 0 and short-circuit the component to show a "no
environments" state or disable the domain form, and set defaultEnvironmentId to
undefined/null (or omit its use) when there are no environments; update any
consumers of defaultEnvironmentId (the variable named defaultEnvironmentId and
the environments array) to handle the absent value so the form and zod
validation are never invoked with an empty string.
- Around line 72-89: Replace the hardcoded workspaceId="" when inserting a
custom domain by retrieving workspace.id from the existing
useWorkspaceNavigation() hook in this component (use the same pattern in
add-custom-domain.tsx where applicable) and pass that value into
collection.customDomains.insert; also wrap the insert call
(collection.customDomains.insert) in a try-catch so you only call reset({
environmentId: values.environmentId, domain: "" }) on success and surface an
error to the user on failure (e.g., via an existing toast/error handler or form
setError) so failures don't silently reset the form.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/use-decrypted-values.ts:
- Around line 19-41: Create a stable ref for the mutation function and add
per-request error handling: initialize a useRef (e.g. decryptMutationRef) and
update decryptMutationRef.current = decryptMutation.mutateAsync whenever
decryptMutation changes; inside the useEffect that watches envData, call
decryptMutationRef.current instead of decryptMutation.mutateAsync so you don't
add the unstable mutate function to the dependency array; wrap each decrypt call
in try/catch (or map to an async function that returns undefined on error) so
failed decryptions are logged/ignored and only successful [envVarId, value]
tuples are collected, then call
setDecryptedValues(Object.fromEntries(successfulEntries)) and ensure
setIsDecrypting(true)/finally setIsDecrypting(false) remains.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/utils.ts:
- Around line 7-27: The computeEnvVarsDiff function uses multiple `as string`
casts and has lint errors from single-line returns; replace casts by introducing
a narrow type (e.g., EnvVarWithId) and a type predicate function (e.g., hasId)
that filters EnvVarItem[] into EnvVarWithId[], then use that predicate when
building originalVars, originalIds, originalMap and currentIds; also change the
single-line arrow-return expressions in the current filter callbacks (the ones
computing toCreate, toUpdate and any early returns in the v.id checks) into
block statements with explicit return statements to satisfy the linter, and
ensure toUpdate is typed as EnvVarWithId[] so downstream code no longer needs
casts.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx:
- Around line 16-25: CPU_OPTIONS is missing the 768 (3/4 vCPU) entry which
causes valueToIndex to fallback to index 0 when the backend returns 768; add the
option { label: "3/4 vCPU", value: 768 } into the CPU_OPTIONS array between the
entries for 512 and 1024 so parseCpuDisplay/formatCpu values are represented and
the slider maps correctly via valueToIndex.
- Around line 27-29: The cpuSchema currently allows any number; change it to
only accept the allowed CPU values by validating against the set
{256,512,1024,2048,4096,8192,16384,32768}—for example replace z.number() in
cpuSchema with a validator that enforces membership (either a z.union of
z.literal(...) for each allowed numeric value or z.number().refine(val =>
allowedSet.has(val), { message: "..."})); update the error message to clearly
state allowed CPU values so invalid backend data is rejected when parsing in
cpuSchema.
- Around line 33-45: The Cpu component must early-return when no environmentId
is available to avoid passing undefined to CpuForm and prevent downstream
mutations from running; update Cpu (which uses useProjectData -> environments
and trpc.deploy.environmentSettings.get.useQuery) to check if
environments[0]?.id is falsy and return null (or a placeholder) before calling
trpc or rendering CpuForm, and ensure you only call
trpc.deploy.environmentSettings.get.useQuery with a defined environmentId (keep
the enabled flag) and pass a string to CpuForm (defaultCpu can remain as
computed).

---

Duplicate comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/index.tsx:
- Around line 129-142: Remove the remaining "as string" cast on v.id inside the
toUpdate.map call and instead narrow the type safely: ensure the array
`toUpdate` (produced by `computeEnvVarsDiff`) is typed to include only items
with an `id` (e.g., change `computeEnvVarsDiff.toUpdate` return type to
`EnvVarWithId[]`) or filter/narrow with a user-defined type guard (e.g.,
`hasId`) before calling `updateMutation.mutateAsync`; then call
`updateMutation.mutateAsync({ envVarId: v.id, key: v.key, value: v.value, type:
toTrpcType(v.secret) })` without any `as` cast so TypeScript knows `v.id` is a
string.

---

Nitpick comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/custom-domains/index.tsx:
- Around line 66-70: The duplicate-domain check in onSubmit uses trimmedDomain
but compares against raw stored values in customDomains, causing case- and
whitespace-sensitive misses; update the check to normalize both sides by
trimming and lowercasing (e.g., compare trimmedDomain.toLowerCase() against each
d.domain.trim().toLowerCase()) and ensure wherever you add a new domain you
store the normalized form (trimmed/lowercased) so future checks use the same
canonical representation; reference onSubmit, customDomains, trimmedDomain, and
setError when making the change.

Comment on lines +24 to +25
const defaultEnvironmentId =
environments.find((e) => e.slug === "production")?.id ?? environments[0]?.id ?? "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Empty string fallback when no environments exist could cause issues.

If environments is empty, defaultEnvironmentId becomes "", which will fail schema validation (z.string().min(1)). Consider handling the empty environments case explicitly in the UI rather than silently defaulting to an invalid state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/custom-domains/index.tsx
around lines 24 - 25, The current assignment of defaultEnvironmentId uses an
empty string fallback which violates the schema (z.string().min(1)) when
environments is empty; change the logic so you do not produce "" — instead
detect environments.length === 0 and short-circuit the component to show a "no
environments" state or disable the domain form, and set defaultEnvironmentId to
undefined/null (or omit its use) when there are no environments; update any
consumers of defaultEnvironmentId (the variable named defaultEnvironmentId and
the environments array) to handle the absent value so the form and zod
validation are never invoked with an empty string.

Comment on lines +72 to +89
collection.customDomains.insert({
id: crypto.randomUUID(),
domain: trimmedDomain,
workspaceId: "",
projectId,
environmentId: values.environmentId,
verificationStatus: "pending",
verificationToken: "",
ownershipVerified: false,
cnameVerified: false,
targetCname: "",
checkAttempts: 0,
lastCheckedAt: null,
verificationError: null,
createdAt: Date.now(),
updatedAt: null,
});
reset({ environmentId: values.environmentId, domain: "" });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, check the actual content around lines 72-89
echo "=== File content at lines 72-89 ==="
cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/index.tsx | sed -n '70,92p'

# Check file size to determine if we can read the whole file
echo -e "\n=== File line count ==="
wc -l web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/index.tsx

# Check imports and initial context
echo -e "\n=== First 50 lines for context ==="
head -50 web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/index.tsx

Repository: unkeyed/unkey

Length of output: 2485


🏁 Script executed:

#!/bin/bash

# Check data-provider.tsx to see what context is available
echo "=== Data provider structure ==="
cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx | head -80

# Check for ProjectDataContext definition
echo -e "\n=== Search for ProjectDataContext ==="
rg -n "ProjectDataContext\|export const useProjectData\|workspaceId" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx | head -30

Repository: unkeyed/unkey

Length of output: 3289


🏁 Script executed:

#!/bin/bash

# Search for useProjectData hook implementation
echo "=== Search for useProjectData implementation ==="
rg -n "const useProjectData|export.*useProjectData" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/ -A 5

# Search for how workspaceId is typically accessed in similar files
echo -e "\n=== Search workspaceId usage in workspace context ==="
rg -n "useWorkspace\|workspaceId.*useParams\|useParams.*workspace" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/ -t tsx | head -20

Repository: unkeyed/unkey

Length of output: 1006


🏁 Script executed:

#!/bin/bash

# Check Project type definition to see if it has workspaceId
echo "=== Check Project type ==="
rg -n "type Project|interface Project" web/apps/dashboard/lib/collections/deploy/ -A 15 | head -40

# Check if useWorkspace hook is available
echo -e "\n=== Search for useWorkspace hook ==="
rg -n "export.*useWorkspace|const useWorkspace" web/apps/dashboard/ -t ts --max-count=10

# Check if there's workspace context/provider
echo -e "\n=== Search for workspace provider or context ==="
rg -n "WorkspaceContext\|useWorkspace" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/data-provider.tsx | head -20

# Check the full project query to see all fields
echo -e "\n=== Check what project object contains ==="
rg -n "from.*project\|project:" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx | head -20

Repository: unkeyed/unkey

Length of output: 2122


🏁 Script executed:

#!/bin/bash

# Check if workspaceId is passed through params
echo "=== Check params and workspaceSlug handling ==="
sed -n '42,50p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx

# Check if error handling is present with fault library
echo -e "\n=== Check for fault library import and error handling ==="
grep -n "fault\|try\|catch\|error" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/index.tsx | head -20

# Check how other insert operations are done nearby
echo -e "\n=== Check collection.customDomains usage pattern ==="
rg -n "collection\.customDomains" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/ -B 3 -A 3

Repository: unkeyed/unkey

Length of output: 7235


🏁 Script executed:

#!/bin/bash

# Check the Project schema definition
echo "=== Check Project schema ==="
sed -n '1,40p' web/apps/dashboard/lib/collections/deploy/projects.ts

# Check useWorkspace hook
echo -e "\n=== Check useWorkspace hook ==="
head -50 web/apps/dashboard/providers/workspace-provider.tsx

# Check CustomDomain type to verify workspaceId field exists
echo -e "\n=== Check CustomDomain type ==="
rg -n "type CustomDomain|interface CustomDomain" web/apps/dashboard/lib/collections/deploy/ -A 20

Repository: unkeyed/unkey

Length of output: 5022


🏁 Script executed:

#!/bin/bash

# Check if workspaceSlug is extracted from params in the data provider
echo "=== Check if workspaceSlug is extracted ==="
cat web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx | head -60 | tail -20

# Check what happens in handleSubmit - look at full implementation
echo -e "\n=== Check full handleSubmit implementation ==="
sed -n '50,95p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/index.tsx

Repository: unkeyed/unkey

Length of output: 2023


🏁 Script executed:

#!/bin/bash

# Get the CustomDomain schema definition
echo "=== Check CustomDomain schema ==="
sed -n '1,30p' web/apps/dashboard/lib/collections/deploy/custom-domains.ts

# See if there's a separate workspace data provider
echo -e "\n=== Check if workspace is available in ProjectDataProvider ==="
sed -n '42,52p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx

# Check useWorkspace availability in the component or its parent
echo -e "\n=== Check file structure to see where useWorkspace could be used ==="
rg -n "useWorkspace|useParams" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/index.tsx

Repository: unkeyed/unkey

Length of output: 1612


🏁 Script executed:

#!/bin/bash

# Check the add-custom-domain.tsx to see if it has the same issue
echo "=== Check add-custom-domain.tsx for comparison ==="
sed -n '85,100p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/details/custom-domains-section/add-custom-domain.tsx

# Check if that file uses useWorkspace
echo -e "\n=== Check add-custom-domain.tsx imports and hooks ==="
head -30 web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/details/custom-domains-section/add-custom-domain.tsx

Repository: unkeyed/unkey

Length of output: 1296


🏁 Script executed:

#!/bin/bash

# Check what the actual file imports
echo "=== Check all imports in custom-domains/index.tsx ==="
sed -n '1,25p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/index.tsx

# Check if workspace is available through any parent context
echo -e "\n=== Check parent settings layout ==="
ls -la web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/

# Check if there's a workspace available through layout.tsx or root provider
echo -e "\n=== Check layout.tsx in settings folder ==="
cat web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/layout.tsx 2>/dev/null | head -30

# Look for any other component that accesses workspaceId correctly
echo -e "\n=== Search for correct workspaceId usage in similar contexts ==="
rg -n "workspaceId.*useWorkspace\|useWorkspace.*workspaceId" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/ --type tsx -B 2 -A 2 | head -30

Repository: unkeyed/unkey

Length of output: 1439


🏁 Script executed:

#!/bin/bash

# Check if the workspace can be extracted from useParams
echo "=== Check if useParams extracts workspaceSlug ==="
rg -n "useParams\(\)" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/ --type tsx -A 3 | head -40

Repository: unkeyed/unkey

Length of output: 151


🏁 Script executed:

#!/bin/bash

# Search for workspaceId usage without type flag
echo "=== Search for workspaceId access patterns ==="
rg -n "workspaceId" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/ -A 2 -B 2 | head -50

# Check if useWorkspace is used in the settings area
echo -e "\n=== Check useWorkspace usage in settings components ==="
rg -n "useWorkspace" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/ | head -20

# Check data provider to see all params being used
echo -e "\n=== Check all params in ProjectDataProvider ==="
sed -n '42,48p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx

Repository: unkeyed/unkey

Length of output: 2122


🏁 Script executed:

#!/bin/bash

# Try to find how workspace is provided to project components
echo "=== Check root workspace provider/context in this area ==="
find web/apps/dashboard/app/\(app\)/\[workspaceSlug\] -name "layout.tsx" -o -name "data-provider.tsx" | head -10

# Check what's available in the parent layout
echo -e "\n=== Check workspace slug parent ==="
ls -la web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/

# Look for workspace context usage in similar insert operations
echo -e "\n=== Check other insert operations ==="
rg -n "collection\..+\.insert" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/ -B 5 | head -60

Repository: unkeyed/unkey

Length of output: 4939


🏁 Script executed:

#!/bin/bash

# Check the workspace layout to see if workspace is provided there
echo "=== Check workspace layout.tsx ==="
cat web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/layout.tsx

# Check if workspace can be obtained from useWorkspace hook in components
echo -e "\n=== Check how workspace is typically accessed ==="
rg -n "const.*workspace.*useWorkspace\|useWorkspace.*workspace" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/ -B 2 -A 2 | head -40

# Check the ratelimit override insert to see if it has workspaceId
echo -e "\n=== Check ratelimit override insert pattern ==="
sed -n '76,95p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/ratelimits/\[namespaceId\]/_components/identifier-dialog.tsx

Repository: unkeyed/unkey

Length of output: 1636


🏁 Script executed:

#!/bin/bash

# Check if there's an existing pattern for getting workspaceId in this component tree
echo "=== Check if other components in projects access workspace ==="
rg -n "useWorkspace" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/ -B 2 -A 5 | head -50

# Check CustomDomainRow to see if it accesses workspace
echo -e "\n=== Check CustomDomainRow implementation ==="
head -40 web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/advanced-settings/custom-domains/custom-domain-row.tsx

Repository: unkeyed/unkey

Length of output: 9095


🏁 Script executed:

#!/bin/bash

# Check the useWorkspaceNavigation hook definition
echo "=== Check useWorkspaceNavigation hook ==="
cat web/apps/dashboard/hooks/use-workspace-navigation.tsx

# Verify that this hook is safe to use in these components
echo -e "\n=== Check if hook returns workspace with id ==="
rg -n "workspace\.id\|workspace\.slug" web/apps/dashboard/hooks/use-workspace-navigation.tsx -A 2 -B 2

Repository: unkeyed/unkey

Length of output: 1568


🏁 Script executed:

#!/bin/bash

# Check the Workspace type to confirm it has an id field
echo "=== Check Workspace type definition ==="
rg -n "type Workspace|interface Workspace" web/apps/dashboard/ -B 2 -A 15 | head -40

# Confirm the workspace object structure
echo -e "\n=== Check what fields workspace has ==="
rg -n "workspace\." web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/navigation.tsx | head -10

Repository: unkeyed/unkey

Length of output: 3947


🏁 Script executed:

#!/bin/bash

# Verify that Workspace from `@unkey/db` has an id field
echo "=== Confirm Workspace type has id field ==="
rg -n "export.*type Workspace\|export.*interface Workspace" web/apps/dashboard/ -A 30 | grep -A 30 "@unkey/db" | head -40

Repository: unkeyed/unkey

Length of output: 100


Retrieve workspaceId from the workspace context instead of hardcoding an empty string.

The workspaceId is hardcoded to an empty string on line 75, which will create malformed records. Use the useWorkspaceNavigation() hook (already available in this component tree) to access workspace.id. This same issue also exists in add-custom-domain.tsx line 95.

Additionally, the insert operation lacks error handling. If the insert fails, the form resets anyway, leaving the user unaware of the failure. Wrap the insert in a try-catch block and provide user feedback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/custom-domains/index.tsx
around lines 72 - 89, Replace the hardcoded workspaceId="" when inserting a
custom domain by retrieving workspace.id from the existing
useWorkspaceNavigation() hook in this component (use the same pattern in
add-custom-domain.tsx where applicable) and pass that value into
collection.customDomains.insert; also wrap the insert call
(collection.customDomains.insert) in a try-catch so you only call reset({
environmentId: values.environmentId, domain: "" }) on success and surface an
error to the user on failure (e.g., via an existing toast/error handler or form
setError) so failures don't silently reset the form.

Comment on lines +19 to +41
useEffect(() => {
if (!envData) {
return;
}

const recoverableVars = envData.variables.filter((v) => v.type === "recoverable");
if (recoverableVars.length === 0) {
return;
}

setIsDecrypting(true);
Promise.all(
recoverableVars.map((v) =>
decryptMutation.mutateAsync({ envVarId: v.id }).then((r) => [v.id, r.value] as const)
)
)
.then((entries) => {
setDecryptedValues(Object.fromEntries(entries));
})
.finally(() => {
setIsDecrypting(false);
});
}, [envData]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix lint error and add error handling for decrypt failures.

The pipeline reports a missing dependency (decryptMutation.mutateAsync). Adding it directly would cause infinite re-renders since useMutation returns unstable references. Additionally, failed decryptions are silently ignored, leaving decryptedValues incomplete.

🛠️ Proposed fix using a stable ref
 export function useDecryptedValues(envData: EnvData | undefined) {
   const decryptMutation = trpc.deploy.envVar.decrypt.useMutation();
+  const decryptRef = React.useRef(decryptMutation.mutateAsync);
+  decryptRef.current = decryptMutation.mutateAsync;
+
   const [decryptedValues, setDecryptedValues] = useState<Record<string, string>>({});
   const [isDecrypting, setIsDecrypting] = useState(false);

   useEffect(() => {
     if (!envData) {
       return;
     }

     const recoverableVars = envData.variables.filter((v) => v.type === "recoverable");
     if (recoverableVars.length === 0) {
       return;
     }

     setIsDecrypting(true);
     Promise.all(
       recoverableVars.map((v) =>
-        decryptMutation.mutateAsync({ envVarId: v.id }).then((r) => [v.id, r.value] as const)
+        decryptRef.current({ envVarId: v.id })
+          .then((r) => [v.id, r.value] as const)
+          .catch(() => null)
       )
     )
       .then((entries) => {
-        setDecryptedValues(Object.fromEntries(entries));
+        const validEntries = entries.filter((e): e is [string, string] => e !== null);
+        setDecryptedValues(Object.fromEntries(validEntries));
       })
       .finally(() => {
         setIsDecrypting(false);
       });
   }, [envData]);
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 19-19: lint/correctness/useExhaustiveDependencies: useEffect dependency array is missing decryptMutation.mutateAsync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/use-decrypted-values.ts
around lines 19 - 41, Create a stable ref for the mutation function and add
per-request error handling: initialize a useRef (e.g. decryptMutationRef) and
update decryptMutationRef.current = decryptMutation.mutateAsync whenever
decryptMutation changes; inside the useEffect that watches envData, call
decryptMutationRef.current instead of decryptMutation.mutateAsync so you don't
add the unstable mutate function to the dependency array; wrap each decrypt call
in try/catch (or map to an async function that returns undefined on error) so
failed decryptions are logged/ignored and only successful [envVarId, value]
tuples are collected, then call
setDecryptedValues(Object.fromEntries(successfulEntries)) and ensure
setIsDecrypting(true)/finally setIsDecrypting(false) remains.

Comment on lines +7 to +27
export function computeEnvVarsDiff(original: EnvVarItem[], current: EnvVarItem[]) {
const originalVars = original.filter((v) => v.id);
const originalIds = new Set(originalVars.map((v) => v.id as string));
const originalMap = new Map(originalVars.map((v) => [v.id as string, v]));

const currentIds = new Set(current.filter((v) => v.id).map((v) => v.id as string));

const toDelete = [...originalIds].filter((id) => !currentIds.has(id));

const toCreate = current.filter((v) => !v.id && v.key !== "" && v.value !== "");

const toUpdate = current.filter((v) => {
if (!v.id) return false;
const orig = originalMap.get(v.id);
if (!orig) return false;
if (v.value === "") return false;
return v.key !== orig.key || v.value !== orig.value || v.secret !== orig.secret;
});

return { toDelete, toCreate, toUpdate, originalMap };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove as string casts and fix block statement lint errors.

Multiple as string casts violate coding guidelines. Additionally, the pipeline reports lint errors for missing block statements on lines 19, 21, and 22. Use a type predicate to narrow types properly.

🛡️ Proposed fix with type predicate and block statements
 import type { EnvVarsFormValues } from "./schema";

 export type EnvVarItem = EnvVarsFormValues["envVars"][number];
+export type EnvVarWithId = EnvVarItem & { id: string };
+
+const hasId = (v: EnvVarItem): v is EnvVarWithId => Boolean(v.id);

 export const toTrpcType = (secret: boolean) => (secret ? "writeonly" : "recoverable");

 export function computeEnvVarsDiff(original: EnvVarItem[], current: EnvVarItem[]) {
-  const originalVars = original.filter((v) => v.id);
-  const originalIds = new Set(originalVars.map((v) => v.id as string));
-  const originalMap = new Map(originalVars.map((v) => [v.id as string, v]));
+  const originalVars = original.filter(hasId);
+  const originalIds = new Set(originalVars.map((v) => v.id));
+  const originalMap = new Map<string, EnvVarWithId>(originalVars.map((v) => [v.id, v]));

-  const currentIds = new Set(current.filter((v) => v.id).map((v) => v.id as string));
+  const currentIds = new Set(current.filter(hasId).map((v) => v.id));

   const toDelete = [...originalIds].filter((id) => !currentIds.has(id));

   const toCreate = current.filter((v) => !v.id && v.key !== "" && v.value !== "");

-  const toUpdate = current.filter((v) => {
-    if (!v.id) return false;
+  const toUpdate = current.filter((v): v is EnvVarWithId => {
+    if (!v.id) {
+      return false;
+    }
     const orig = originalMap.get(v.id);
-    if (!orig) return false;
-    if (v.value === "") return false;
+    if (!orig) {
+      return false;
+    }
+    if (v.value === "") {
+      return false;
+    }
     return v.key !== orig.key || v.value !== orig.value || v.secret !== orig.secret;
   });

   return { toDelete, toCreate, toUpdate, originalMap };
 }

This fix:

  1. Introduces EnvVarWithId type and hasId type predicate to eliminate all as string casts
  2. Wraps single-line returns in block statements to satisfy the linter
  3. Returns toUpdate with proper EnvVarWithId[] type, which will also fix the downstream cast in index.tsx

As per coding guidelines: "Never compromise type safety: No any, no ! (non-null assertion), no as Type".

🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 19-19: lint/style/useBlockStatements: Block statements are preferred here. (Unsafe fix suggested: wrap in a JsBlockStatement)


[error] 21-21: lint/style/useBlockStatements: Block statements are preferred here. (Unsafe fix suggested: wrap in a JsBlockStatement)


[error] 22-22: lint/style/useBlockStatements: Block statements are preferred here. (Unsafe fix suggested: wrap in a JsBlockStatement)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/utils.ts
around lines 7 - 27, The computeEnvVarsDiff function uses multiple `as string`
casts and has lint errors from single-line returns; replace casts by introducing
a narrow type (e.g., EnvVarWithId) and a type predicate function (e.g., hasId)
that filters EnvVarItem[] into EnvVarWithId[], then use that predicate when
building originalVars, originalIds, originalMap and currentIds; also change the
single-line arrow-return expressions in the current filter callbacks (the ones
computing toCreate, toUpdate and any early returns in the v.id checks) into
block statements with explicit return statements to satisfy the linter, and
ensure toUpdate is typed as EnvVarWithId[] so downstream code no longer needs
casts.

Comment on lines +16 to +25
const CPU_OPTIONS = [
{ label: "1/4 vCPU", value: 256 },
{ label: "1/2 vCPU", value: 512 },
{ label: "1 vCPU", value: 1024 },
{ label: "2 vCPU", value: 2048 },
{ label: "4 vCPU", value: 4096 },
{ label: "8 vCPU", value: 8192 },
{ label: "16 vCPU", value: 16384 },
{ label: "32 vCPU", value: 32768 },
] as const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx | head -50

Repository: unkeyed/unkey

Length of output: 1978


🏁 Script executed:

rg -n "parseCpuDisplay" -g'*.ts' -g'*.tsx' --max-count=10

Repository: unkeyed/unkey

Length of output: 432


🏁 Script executed:

rg -n "768" -g'*.ts' -g'*.tsx' --max-count=20

Repository: unkeyed/unkey

Length of output: 1252


🏁 Script executed:

rg -n "valueToIndex" -g'*.ts' -g'*.tsx' --max-count=10

Repository: unkeyed/unkey

Length of output: 1452


🏁 Script executed:

sed -n '155,180p' web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx

Repository: unkeyed/unkey

Length of output: 489


🏁 Script executed:

cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/shared/slider-utils.ts

Repository: unkeyed/unkey

Length of output: 593


🏁 Script executed:

sed -n '1,30p' web/apps/dashboard/lib/utils/deployment-formatters.ts

Repository: unkeyed/unkey

Length of output: 662


🏁 Script executed:

rg -n "cpuMillicores" -g'*.ts' -g'*.tsx' -A 3 -B 3 | head -60

Repository: unkeyed/unkey

Length of output: 5052


🏁 Script executed:

rg -n "3/4.*vCPU|768" web/apps/dashboard -g'*.ts' -g'*.tsx'

Repository: unkeyed/unkey

Length of output: 857


Add 768 to CPU_OPTIONS to match parseCpuDisplay and prevent slider position fallback.

Both parseCpuDisplay and formatCpu (in deployment-formatters.ts) explicitly handle 768 (3/4 vCPU), indicating backend support. However, CPU_OPTIONS omits this value. When the backend returns 768, valueToIndex finds no match and falls back to index 0, incorrectly mapping to 256 vCPU on the slider. Add { label: "3/4 vCPU", value: 768 } to CPU_OPTIONS between 512 and 1024 to maintain consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx
around lines 16 - 25, CPU_OPTIONS is missing the 768 (3/4 vCPU) entry which
causes valueToIndex to fallback to index 0 when the backend returns 768; add the
option { label: "3/4 vCPU", value: 768 } into the CPU_OPTIONS array between the
entries for 512 and 1024 so parseCpuDisplay/formatCpu values are represented and
the slider maps correctly via valueToIndex.

Comment on lines +27 to +29
const cpuSchema = z.object({
cpu: z.number(),
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the file and examine its contents
find . -type f -name "cpu.tsx" | grep -E "(runtime-settings|settings)" | head -5

Repository: unkeyed/unkey

Length of output: 180


🏁 Script executed:

# Search for CPU_OPTIONS definition
rg "CPU_OPTIONS" -g '*.ts' -g '*.tsx' -B 2 -A 2

Repository: unkeyed/unkey

Length of output: 4052


🏁 Script executed:

# Look for the specific file path
fd "cpu.tsx" web/apps/dashboard

Repository: unkeyed/unkey

Length of output: 178


🏁 Script executed:

# Read the full cpu.tsx file
cat -n web/apps/dashboard/app/(app)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx

Repository: unkeyed/unkey

Length of output: 278


🏁 Script executed:

# Use find to locate and read the file
find . -name "cpu.tsx" -path "*runtime-settings*" -exec cat -n {} \;

Repository: unkeyed/unkey

Length of output: 6816


🏁 Script executed:

# Look for similar validation patterns with refine() or enum validation
rg "\.refine\(" -A 3 -B 1 -g '*.tsx' -g '*.ts' | head -40

Repository: unkeyed/unkey

Length of output: 3357


🏁 Script executed:

# Search for memory settings to see if they have similar validation
rg -l "memory|Memory" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/ | head -5

Repository: unkeyed/unkey

Length of output: 307


🏁 Script executed:

# Check memory.tsx to see how it handles validation
find . -name "memory.tsx" -path "*runtime-settings*" -exec cat -n {} \;

Repository: unkeyed/unkey

Length of output: 6819


🏁 Script executed:

# Check storage.tsx to see the pattern there
find . -name "storage.tsx" -path "*runtime-settings*" -exec cat -n {} \;

Repository: unkeyed/unkey

Length of output: 4544


Constrain cpu to valid options to make illegal states unrepresentable.

The schema accepts any number but only specific CPU values are valid (256, 512, 1024, 2048, 4096, 8192, 16384, 32768). While the UI uses a controlled slider to prevent direct user input of invalid values, the schema should validate the actual domain constraints to handle potentially invalid backend data.

const cpuSchema = z.object({
-  cpu: z.number(),
+  cpu: z
+    .number()
+    .int()
+    .refine((v) => CPU_OPTIONS.some((o) => o.value === v), {
+      message: "Invalid CPU option",
+    }),
});

This aligns with the coding guideline to make illegal states unrepresentable by validating inputs at boundaries into properly typed structures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx
around lines 27 - 29, The cpuSchema currently allows any number; change it to
only accept the allowed CPU values by validating against the set
{256,512,1024,2048,4096,8192,16384,32768}—for example replace z.number() in
cpuSchema with a validator that enforces membership (either a z.union of
z.literal(...) for each allowed numeric value or z.number().refine(val =>
allowedSet.has(val), { message: "..."})); update the error message to clearly
state allowed CPU values so invalid backend data is rejected when parsing in
cpuSchema.

Comment on lines +33 to +45
export const Cpu = () => {
const { environments } = useProjectData();
const environmentId = environments[0]?.id;

const { data: settingsData } = trpc.deploy.environmentSettings.get.useQuery(
{ environmentId: environmentId ?? "" },
{ enabled: Boolean(environmentId) },
);

const defaultCpu = settingsData?.runtimeSettings?.cpuMillicores ?? 256;

return <CpuForm environmentId={environmentId} defaultCpu={defaultCpu} />;
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "cpu.tsx" -x cat -n {}

Repository: unkeyed/unkey

Length of output: 6816


🏁 Script executed:

rg -n "CpuForm" --type ts --type tsx -A 3 -B 3

Repository: unkeyed/unkey

Length of output: 84


🏁 Script executed:

rg -n "interface CpuFormProps|type CpuFormProps" --type ts --type tsx

Repository: unkeyed/unkey

Length of output: 84


🏁 Script executed:

rg -n "useProjectData" --type ts --type tsx -A 10

Repository: unkeyed/unkey

Length of output: 84


🏁 Script executed:

rg -n "useProjectData" -A 15 -B 2

Repository: unkeyed/unkey

Length of output: 50370


🏁 Script executed:

fd -type f "data-provider" -path "*/dashboard/*"

Repository: unkeyed/unkey

Length of output: 227


🏁 Script executed:

rg -n "export.*useProjectData|const useProjectData" -A 20

Repository: unkeyed/unkey

Length of output: 959


🏁 Script executed:

cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx | head -160

Repository: unkeyed/unkey

Length of output: 6258


🏁 Script executed:

rg -n "ProjectDataContext|environments" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/data-provider.tsx -A 2 -B 2

Repository: unkeyed/unkey

Length of output: 2221


🏁 Script executed:

fd -name "tsconfig.json" | head -5 | xargs cat

Repository: unkeyed/unkey

Length of output: 287


🏁 Script executed:

rg -n "environmentId: undefined" --type js --type ts -A 2 -B 2

Repository: unkeyed/unkey

Length of output: 39


🏁 Script executed:

cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/memory.tsx | sed -n '30,50p'

Repository: unkeyed/unkey

Length of output: 791


🏁 Script executed:

find web/apps/dashboard -name "tsconfig.json" -o -name "tsconfig*.json" | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Repository: unkeyed/unkey

Length of output: 814


🏁 Script executed:

rg "as const" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/ -l

Repository: unkeyed/unkey

Length of output: 565


🏁 Script executed:

rg "environmentId\?" web/apps/dashboard -A 1 -B 1 --max-count=10

Repository: unkeyed/unkey

Length of output: 483


🏁 Script executed:

rg "noUncheckedIndexedAccess" web/apps/dashboard/tsconfig.json

Repository: unkeyed/unkey

Length of output: 39


🏁 Script executed:

cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx | sed -n '33,50p'

Repository: unkeyed/unkey

Length of output: 697


🏁 Script executed:

rg "// `@ts-ignore`|// `@ts-expect-error`" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\)/\(overview\)/settings/components/runtime-settings/ -B 3 -A 1

Repository: unkeyed/unkey

Length of output: 211


🏁 Script executed:

rg "CpuForm|MemoryForm|RegionsForm" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/ -A 3 | grep -E "environmentId|return <" | head -20

Repository: unkeyed/unkey

Length of output: 2063


🏁 Script executed:

rg -A 5 "type CpuFormProps" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx

Repository: unkeyed/unkey

Length of output: 206


🏁 Script executed:

rg -B 2 -A 10 "const CpuForm.*React.FC" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx

Repository: unkeyed/unkey

Length of output: 348


🏁 Script executed:

rg "environmentId" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx | grep -E "mutate|mutation|invalidate"

Repository: unkeyed/unkey

Length of output: 130


🏁 Script executed:

cat -n web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx | sed -n '73,105p'

Repository: unkeyed/unkey

Length of output: 1396


🏁 Script executed:

rg "environmentId\s*\?" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx

Repository: unkeyed/unkey

Length of output: 100


🏁 Script executed:

rg "@ts-ignore|@ts-expect-error" web/apps/dashboard/app/\(app\)/\[workspaceSlug\]/projects/\[projectId\]/\(overview\)/settings/components/runtime-settings/cpu.tsx

Repository: unkeyed/unkey

Length of output: 39


🏁 Script executed:

git -C web/apps/dashboard log --oneline --all -- "**/cpu.tsx" 2>/dev/null | head -5

Repository: unkeyed/unkey

Length of output: 89


Guard rendering when environmentId is missing.

environmentId is typed as string | undefined (from environments[0]?.id), but CpuFormProps.environmentId expects string. With strict: true in TypeScript, passing an undefined value violates type safety. Additionally, mutations at lines 79 and 102 depend on environmentId being a valid string.

Add an early return to prevent rendering when no environment is available:

Guard example
 export const Cpu = () => {
   const { environments } = useProjectData();
   const environmentId = environments[0]?.id;
 
+  if (!environmentId) {
+    return null;
+  }
+
   const { data: settingsData } = trpc.deploy.environmentSettings.get.useQuery(
     { environmentId: environmentId ?? "" },
     { enabled: Boolean(environmentId) },
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/runtime-settings/cpu.tsx
around lines 33 - 45, The Cpu component must early-return when no environmentId
is available to avoid passing undefined to CpuForm and prevent downstream
mutations from running; update Cpu (which uses useProjectData -> environments
and trpc.deploy.environmentSettings.get.useQuery) to check if
environments[0]?.id is falsy and return null (or a placeholder) before calling
trpc or rendering CpuForm, and ensure you only call
trpc.deploy.environmentSettings.get.useQuery with a defined environmentId (keep
the enabled flag) and pass a string to CpuForm (defaultCpu can remain as
computed).

@vercel vercel bot temporarily deployed to Preview – dashboard February 18, 2026 20:14 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web/apps/dashboard/app/(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/env-var-row.tsx (1)

56-66: Consider accessibility for the visibility toggle button.

The eye button lacks an accessible label. Screen readers won't convey the button's purpose.

♿ Proposed accessibility improvement
     <button
       type="button"
       className="text-gray-9 hover:text-gray-11 transition-colors"
       onClick={() => setIsVisible((v) => !v)}
       tabIndex={-1}
+      aria-label={isVisible ? "Hide value" : "Show value"}
     >
       {isVisible ? <EyeSlash iconSize="sm-regular" /> : <Eye iconSize="sm-regular" />}
     </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/env-var-row.tsx
around lines 56 - 66, The visibility toggle button (eyeButton) is missing an
accessible label; update the button rendered in eyeButton to include a dynamic
aria-label (e.g., aria-label={isVisible ? 'Hide value' : 'Show value'}) and/or
title so screen readers and hover users know its purpose, and add
aria-pressed={isVisible} if treating it as a toggle; ensure the change is
applied where setIsVisible is used and the Eye/EyeSlash icons remain decorative
(aria-hidden="true") so only the label is announced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/utils.ts:
- Around line 7-32: computeEnvVarsDiff uses unsafe "as string" casts for v.id;
replace them by introducing a type predicate (e.g., isItemWithId(item): item is
EnvVarItem & { id: string }) and use it in the filters that produce
originalVars, originalIds/originalMap and the currentIds computation so
TypeScript knows id is present and string-typed without casts; update uses in
originalVars = original.filter(isItemWithId), originalMap = new
Map(originalVars.map(v => [v.id, v])), and currentIds = new
Set(current.filter(isItemWithId).map(v => v.id)) while leaving other checks
(like !v.id) intact where appropriate.

---

Nitpick comments:
In
`@web/apps/dashboard/app/`(app)/[workspaceSlug]/projects/[projectId]/(overview)/settings/components/advanced-settings/env-vars/env-var-row.tsx:
- Around line 56-66: The visibility toggle button (eyeButton) is missing an
accessible label; update the button rendered in eyeButton to include a dynamic
aria-label (e.g., aria-label={isVisible ? 'Hide value' : 'Show value'}) and/or
title so screen readers and hover users know its purpose, and add
aria-pressed={isVisible} if treating it as a toggle; ensure the change is
applied where setIsVisible is used and the Eye/EyeSlash icons remain decorative
(aria-hidden="true") so only the label is announced.

@vercel vercel bot temporarily deployed to Preview – dashboard February 18, 2026 20:17 Inactive
@chronark chronark merged commit 87e34bf into main Feb 19, 2026
16 checks passed
@chronark chronark deleted the deploy-settings branch February 19, 2026 06:42
MichaelUnkey pushed a commit that referenced this pull request Feb 26, 2026
* feat: add github section

* feat: Add icons

* feat: add new sections

* feat: add settingsgroup

* feat: add region selection

* feat: add instances

* feat: add memory and cpu section

* feat: add sections

* feat: add health check

* feat: add scaling

* fix: get rid of redundant prop

* refactor: Add toasts to mutations

* refactor: rename component

* feat: add port section

* feat: fix overlapping borders

* refactor: fix healthcheck tRPC

* feat: add command section

* feat: add env section

* fix: finalize env-vars

* refactor: finalize

* feat: Add custom domains

* fix: overflwo

* feat: make tRPC route for each mutation

* fix: displayValue styles

* refactor: tidy

* fix: revert accidental changes

* feat: add cname table

* fix: github styling issues

* refactor: tidy

* refactor: rename

* fix: linter

* fix: dynamic form issue

* feat: allow env selection

* chore: tidy

* fix: use same chevron
github-merge-queue bot pushed a commit that referenced this pull request Mar 17, 2026
* test keys table

* re org and exports

* error fix

* Apos

* chore: remove deployment breadcrumbs (#5019)

* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* chore: clean up nav

* fix(clickhouse): improve latest keys used queries for high volume (150M +)  (#4959)

* fix(clickhouse): improve clickhouse query for key logs and add  new table and mv for latest keys used

* fix valid/error count = 0 scenario

* remove identity_id from order by

* wrap identity_id with aggregating function since its removed from the order key

---------

Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>

* fix: domain refetch and promotion disable rule (#5013)

* fix: domain refetch and promotion disable rule

* fix: regression

---------

Co-authored-by: Andreas Thomas <dev@chronark.com>

* refactor: move custom domains to tanstack db (#5017)

* refactor: move custom domains to tanstack db

* fix: comment

* fix: delete mutation

* remove: unnecessary query

* remove agent (#5021)

* remove agent

* remove agent

* chore: vault in dashboard (#5023)

* remove agent

* remove agent

* use vault in dashboard

* remove

* project domain (#5022)

* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* chore: clean up nav

* feat: add per-project sticky domain and only display that

* chore: use vault in api (#5024)

* chore: use vault in api

* chore: use vault in api

* fix harness

* use memory test

* vault container go start

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: Make GH callback dynamic (#5029)

* dunno

* nextjs should allow a setting that says dynamic

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* allow longer timeouts (#5032)

* docs: add ratelimit.unkey.com benchmark links to ratelimiting docs

Add references to real-time performance benchmarks in:
- introduction.mdx: new 'Performance at scale' accordion
- modes.mdx: link after latency claim

Presents benchmarks as capability demonstration rather than comparison.

* docs: add description to cache store interface page (#5037)

Add missing SEO description to frontmatter

Generated-By: mintlify-agent

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Andreas Thomas <dev@chronark.com>

* docs: remove orphaned SDK documentation (#5033)

Remove Spring Boot Java, Rust, and Elixir SDK docs that are not linked in navigation and appear to be outdated/unmaintained.

Generated-By: mintlify-agent

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Andreas Thomas <dev@chronark.com>

* No data

* add bg

* rework release (#5044)

* rework release

* rework release

* feat: generate rpc wrappers (#5028)

* feat: generate rpc wrappers

* bazel happyier

* more changes

* more changes

* move path

* delete old files (#5043)

* fix: rabbit comments

---------

Co-authored-by: Oz <21091016+ogzhanolguncu@users.noreply.github.com>

* feat/gossip (#5015)

* add a gossip implementation

* add gossip to sentinel/frontline

* add message muxing

* sentinel fun

* cleansings

* cleansings

* cleansings

* cleansings

* use oneof

* fix bazel happiness

* do some changies

* exportoneof

* more cool fancy thingx

* change gateway choosing

* add label

* adjjust some more

* adjjust some more

* fixa test

* goodbye kafka

* fix: bazel

* rename gateway -> ambassador

* add docs

* fix: rabbit comments

* [autofix.ci] apply automated fixes

* idfk

* more changes

* more changes

* fix ordering

* fix missing files

* fix test

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: retry hubble ui (#5056)

* fix: wait for cillium policy until CRDs are ready (#5059)

* fix: retry cillium policy until CRDs are ready

* fix: blocks until all system pods are ready

* deployment build screen v1 (#5042)

* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* feat: new build screen for ongoing deployments

* fix: table column typo

* fix: update copy to remove mention of analytics deletion (#5067)

* fix typo (#5039)

* rfc: sentinel middlewares (#5041)

* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* feat: middleware rfc

* Update svc/sentinel/proto/buf.gen.ts.yaml

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>

* feat: config files (#5045)

* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* feat: add pkg/config for struct-tag-driven TOML/YAML/JSON configuration

Introduces a new configuration package that replaces environment variable
based configuration with file-based config. Features:

- Load and validate config from TOML, YAML, or JSON files
- Struct tag driven: required, default, min/max, oneof, nonempty
- Environment variable expansion (${VAR} and ${VAR:-default})
- JSON Schema generation for editor autocompletion
- Collects all validation errors instead of failing on first
- Custom Validator interface for cross-field checks

Also adds cmd/generate-config-docs for generating MDX documentation
from Go struct tags, and a Makefile target 'config-docs'.

Amp-Thread-ID: https://ampcode.com/threads/T-019c672a-0e8e-7138-b0ab-27cdbeaca7ba
Co-authored-by: Amp <amp@ampcode.com>

* remove gen

* clean up

* feat(api): migrate API service to file-based TOML config (#5046)

* feat(api): migrate API service to file-based config

Migrate the API service from environment variables to TOML file-based
configuration using pkg/config. Replaces all UNKEY_* env vars with a
structured api.toml config file.

Changes:
- Rewrite svc/api/config.go with tagged Config struct
- Update svc/api/run.go to use new config fields
- Update cmd/api/main.go to accept --config flag
- Add dev/config/api.toml for docker-compose
- Update dev/k8s/manifests/api.yaml with ConfigMap
- Regenerate config docs from struct tags

Amp-Thread-ID: https://ampcode.com/threads/T-019c672a-0e8e-7138-b0ab-27cdbeaca7ba
Co-authored-by: Amp <amp@ampcode.com>

* feat(vault): migrate Vault service to file-based TOML config (#5047)

* feat(vault): migrate Vault service to file-based config

Migrate the Vault service from environment variables to TOML file-based
configuration using pkg/config.

Changes:
- Rewrite svc/vault/config.go with tagged Config struct
- Update svc/vault/run.go to use new config fields
- Update cmd/vault/main.go to accept --config flag
- Add dev/config/vault.toml for docker-compose
- Update dev/k8s/manifests/vault.yaml with ConfigMap
- Remove UNKEY_* env vars from docker-compose and k8s

Amp-Thread-ID: https://ampcode.com/threads/T-019c672a-0e8e-7138-b0ab-27cdbeaca7ba
Co-authored-by: Amp <amp@ampcode.com>

* feat(ctrl): migrate Ctrl API and Worker to file-based TOML config (#5048)

* feat(ctrl): migrate Ctrl API and Worker services to file-based config

Migrate both ctrl-api and ctrl-worker from environment variables to TOML
file-based configuration using pkg/config.

Changes:
- Rewrite svc/ctrl/api/config.go and svc/ctrl/worker/config.go
- Update run.go files to use new config fields
- Update cmd/ctrl/api.go and worker.go to accept --config flag
- Add dev/config/ctrl-api.toml and ctrl-worker.toml
- Update dev/k8s/manifests/ctrl-api.yaml and ctrl-worker.yaml with ConfigMaps
- Remove UNKEY_* env vars from docker-compose and k8s manifests

* feat(krane): migrate Krane service to file-based TOML config (#5049)

* feat(krane): migrate Krane service to file-based config

Migrate the Krane container orchestrator from environment variables to
TOML file-based configuration using pkg/config.

Changes:
- Rewrite svc/krane/config.go with tagged Config struct
- Update svc/krane/run.go to use new config fields
- Update cmd/krane/main.go to accept --config flag
- Add dev/config/krane.toml for docker-compose
- Update dev/k8s/manifests/krane.yaml with ConfigMap
- Remove UNKEY_* env vars from docker-compose and k8s

Amp-Thread-ID: https://ampcode.com/threads/T-019c672a-0e8e-7138-b0ab-27cdbeaca7ba
Co-authored-by: Amp <amp@ampcode.com>

* feat(frontline): migrate Frontline service to file-based TOML config (#5050)

* feat(frontline): migrate Frontline service to file-based config

Migrate the Frontline reverse proxy from environment variables to TOML
file-based configuration using pkg/config.

Changes:
- Rewrite svc/frontline/config.go with tagged Config struct
- Update svc/frontline/run.go to use new config fields
- Update cmd/frontline/main.go to accept --config flag
- Update dev/k8s/manifests/frontline.yaml with ConfigMap
- Remove UNKEY_* env vars from k8s manifest

Amp-Thread-ID: https://ampcode.com/threads/T-019c672a-0e8e-7138-b0ab-27cdbeaca7ba
Co-authored-by: Amp <amp@ampcode.com>

* feat(preflight): migrate Preflight service to file-based TOML config (#5051)

* feat(preflight): migrate Preflight service to file-based config

Migrate the Preflight webhook admission controller from environment
variables to TOML file-based configuration using pkg/config.

Changes:
- Rewrite svc/preflight/config.go with tagged Config struct
- Update svc/preflight/run.go to use new config fields
- Update cmd/preflight/main.go to accept --config flag
- Update dev/k8s/manifests/preflight.yaml with ConfigMap
- Remove UNKEY_* env vars from k8s manifest

Amp-Thread-ID: https://ampcode.com/threads/T-019c672a-0e8e-7138-b0ab-27cdbeaca7ba
Co-authored-by: Amp <amp@ampcode.com>

* feat(sentinel): migrate Sentinel service to file-based config (#5052)

Migrate the Sentinel sidecar from environment variables to TOML
file-based configuration using pkg/config. This is the final service
migration in the config stack.

Changes:
- Rewrite svc/sentinel/config.go with tagged Config struct
- Update svc/sentinel/run.go to use new config fields
- Update cmd/sentinel/main.go to accept --config flag
- Update dev/docker-compose.yaml: replace env vars with TOML volume
  mounts for all migrated services (api, vault, krane, ctrl-api,
  ctrl-worker)
- Minor formatting fix in pkg/db generated code

---------

Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>

---------

Co-authored-by: Amp <amp@ampcode.com>

* fix: bad config

* remove unnecessary tls config for ctrl api

* fix: error

* fix: do not log config content

* ix: remove kafka

* fix: replica

* fix: return err

* fix: only overwrite frontline id if missing

* fix: observability

* fix: otel

* fix: redundant config

* fix: reuse tls

* fix: consolidate

* fix: use shared configs

* fix: config

* fix: something

* Update pkg/config/common.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: vault startup

* fix: instanceid

* fix: vault config

* fix: make configs required

* fix: everything works again

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* clean deployment url label (#4976)

* clean deployment url

* fix conversion error and maintain single source of truth

---------

Co-authored-by: Andreas Thomas <dev@chronark.com>
Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>

* feat: New deploy settings (#5073)

* feat: add github section

* feat: Add icons

* feat: add new sections

* feat: add settingsgroup

* feat: add region selection

* feat: add instances

* feat: add memory and cpu section

* feat: add sections

* feat: add health check

* feat: add scaling

* fix: get rid of redundant prop

* refactor: Add toasts to mutations

* refactor: rename component

* feat: add port section

* feat: fix overlapping borders

* refactor: fix healthcheck tRPC

* feat: add command section

* feat: add env section

* fix: finalize env-vars

* refactor: finalize

* feat: Add custom domains

* fix: overflwo

* feat: make tRPC route for each mutation

* fix: displayValue styles

* refactor: tidy

* fix: revert accidental changes

* feat: add cname table

* fix: github styling issues

* refactor: tidy

* refactor: rename

* fix: linter

* fix: dynamic form issue

* feat: allow env selection

* chore: tidy

* fix: use same chevron

* fix: use certmanager if availiable otherwise certfile (#5076)

* fix: use certmanager if availiable otherwise certfile

* feat: make tls enabled by default

now you need to explicitely pass tls.disabled=true
if not, we fail during startup.

also renamed some port vars to make it obvious what they are used for

* chore: log candidates for easier debugging

* fix: use static certs first

---------

Co-authored-by: chronark <dev@chronark.com>

* feat: sentinel key verification middleware (#5079)

* feat: key-sentinel-middleware

* fix error pages (#5083)

* fix error pages

* remove test

* move some files

* Update svc/frontline/internal/errorpage/error.go.tmpl

Co-authored-by: Andreas Thomas <dev@chronark.com>

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Andreas Thomas <dev@chronark.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* add rl headers.

* feat: new ui and fixed a bunch of stuff

* Update svc/sentinel/engine/match.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: coderabbit

---------

Co-authored-by: Andreas Thomas <dev@chronark.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* clean up after sentinel middleware (#5088)

* feat: key-sentinel-middleware

* fix error pages (#5083)

* fix error pages

* remove test

* move some files

* Update svc/frontline/internal/errorpage/error.go.tmpl

Co-authored-by: Andreas Thomas <dev@chronark.com>

* [autofix.ci] apply automated fixes

---------

Co-authored-by: Andreas Thomas <dev@chronark.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* add rl headers.

* feat: new ui and fixed a bunch of stuff

* Update svc/sentinel/engine/match.go

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: coderabbit

* chore: clean up old columns

* fix: db

---------

Co-authored-by: Flo <flo@unkey.com>
Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix proto type (#5093)

* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* fix: runtime exception due to gaslighting type

* fix: Modals with combo box work again  (#5002)

* chore: remove chproxy routes (#5101)

* chore: remove chproxy routes

* refactor: move prometheus metrics to scoped packages (#5102)

* remove the hand holding (#5108)

* feat: gossip metrics (#5107)

* fix: Make identity slugs copyable (#5100)

* fix: make me copy

* Update web/apps/dashboard/app/(app)/[workspaceSlug]/authorization/permissions/components/table/components/assigned-items-cell.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor: useDeployment hook usage (#5086)

* refactor: useDeployment hook usage

* fix: remove redundant check

* feat: i need more metrics (#5115)

* docs: Python and Go examples (#5058)

* Add Python and Go SDK documentation

- Add Go quickstart guide with stdlib, Gin, and Echo examples
- Add Python quickstart guide with FastAPI, Flask, Django
- Add Go cookbook: stdlib, Gin, Echo middleware recipes
- Add Python cookbook: FastAPI rate limiting recipe

All content uses Unkey v2 API only.

* Add final Python cookbook recipe and 5-minutes guide

* Update docs.json sidebar navigation

- Add Go and Python quickstart guides to Framework Guides
- Add Go and Python cookbook recipes to Recipes section
- Remove duplicate 5-minutes quickstart file

* Add Go examples to quickstart and reorganize cookbook by language

- Add Go code examples to /quickstart/quickstart.mdx for key creation and verification
- Reorganize cookbook recipes into subsections: TypeScript, Go, Python, General
- Keep existing TypeScript and Python examples in quickstart

* Update cookbook index with new Go and Python recipes

* Fix code issues in Go and Python documentation

- Fix int to string conversion in go-gin-middleware (use strconv)
- Fix middleware composition in go-stdlib-middleware
- Fix wait calculation in python-fastapi-ratelimit (use total_seconds)
- Fix headers attachment in python-fastapi-ratelimit (use JSONResponse)
- Fix nil pointer dereference in quickstart/go
- Fix unsafe type assertion in quickstart/go

* Fix async/sync issue and nil pointer in quickstart docs

- Use verify_key_async in Python async route
- Add nil check for result.Code in Go quickstart

* Fix more code issues in documentation

- Fix GetUnkeyResult type assertion in go-gin-middleware
- Fix imports in python-fastapi-ratelimit (add JSONResponse, remove unused timedelta)
- Update basic rate limit example to use async API with context manager
- Add missing os import in Django settings snippet

* Fix missing os import in python-flask-auth.mdx

* Fix unsafe type assertions in Go middleware docs

- Fix RequirePermission in go-echo-middleware with safe type assertion
- Fix GetUnkeyResult in go-echo-middleware with safe type assertion
- Fix RequirePermission in go-gin-middleware with safe type assertion

* Fix error handling in Python docs - replace ApiError with UnkeyError

* Update legacy analytics documentation

- Replace outdated /apis/features/analytics.mdx with minimal reference page
- Remove analytics from API Keys sidebar in docs.json
- Add redirect from /apis/features/analytics to /analytics/overview

* fix

* Update to mint

* Fix critical type assertion issues in go-gin-middleware

- Store pointer to struct in context (not value) for type assertion compatibility
- Add checked type assertion in RequireRole with proper error handling

* Add it back

* fix the comma

* revert

* Update go examples

* cookbook update

* update quickstart

* remove analytics page that is redirected

* Update web/apps/docs/cookbook/go-echo-middleware.mdx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update web/apps/docs/cookbook/go-echo-middleware.mdx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: deploy quick fixes (#5085)

* fix: fetch correct deployment+sentinel

* fix: add missing team switcher hover indicator

* refactor: use the same empty text

* fix: lock network view and fix generate dummy network

* fix: safari rendering issue of network

* chore: fmt

* fix: build

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>
Co-authored-by: James P <james@unkey.com>

* docs: remove duplicate onboarding page (#5035)

Remove quickstart/onboarding/onboarding-api.mdx which duplicates content from the new quickstart. Redirects already exist in docs.json.

Generated-By: mintlify-agent

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: Andreas Thomas <dev@chronark.com>
Co-authored-by: James P <james@unkey.com>

* docs: remove deprecated Vercel integration page (#5034)

The Vercel integration is currently not supported. Remove the page to avoid confusing users.

Generated-By: mintlify-agent

Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: James P <james@unkey.com>

* fix: schema cache (#5116)

* fix: do background gossip connect (#5119)

* fix: do background gossip connect

* bazel happy

* chore: debug wan failures (#5124)

* chore: debug wan failures

* add log writer

* bazel ..........

* bazel ..........

* fix: a user cannot click outside of the org selection modal (#5031)

* fix: a user cannot click outside of the org selection modal

* use errorMessage instead of hard coding messages

* restore x closing functionality

* fix rabbit, fix flash of empty state

* clear last used workspace when auto-selection fails

* remove unused conditional

---------

Co-authored-by: James P <james@unkey.com>

* sentinel prewarm cache (#5071)

* fix: cleanup project side nav

* feat: simplify deployment overview page

only show build logs until it's built, then show domains and network

* feat: sentinels prewarm their cache

it's not optmized, but pretty neat

---------

Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>

* fix: ignore empty wan (#5122)

Co-authored-by: Andreas Thomas <dev@chronark.com>
Co-authored-by: James P <james@unkey.com>

* docs: move docs (#5125)

* refactor: deploy settings tanstack (#5104)

* refactor: move them to tanstack

* refactor: tidy up

* feat: add env provider to decide what env we are on

* refactor: tidy

* feat: add scroll into view for settingcard

* fix: bg

* refactor: remove toasts from env-vars

* chore: tidy

* fix: build

* feat: vault bulk en/decrypt (#5127)

* feat: vault bulk en/decrypt

* oops wrong file

* cleanup proto

* [autofix.ci] apply automated fixes

* cleanup

* cleanup

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* feat: trace generated rpc clients (#5128)

* feat: trace generated rpc clients

* ignore not found

* fix: docs generator paths (#5136)

* fix: retry memberlist creation (#5134)

* fix: retry memberlist creation

* remove comments

* move to const

* fix: Restore filtering on logs (#5138)

* Restore filtering on logs

Restores filtering on the logs.

* [autofix.ci] apply automated fixes

* fmt

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: resolve dns to ip (#5139)

* fix: resolve dns to ip

* rabbit comments

* Fix/issue 5132 billing section widths (#5140)

* fix: Billing page has inconsistent section widths (#5132)

Standardized all SettingCard components to use consistent width classes:
- Updated Usage component: contentWidth changed from 'w-full lg:w-[320px]' to 'w-full'
- Updated CancelAlert component: contentWidth changed from 'w-full lg:w-[320px]' to 'w-full'
- Updated Billing Portal in client.tsx: contentWidth changed from 'w-full lg:w-[320px]' to 'w-full'
- Updated CurrentPlanCard component: removed min-w-[200px] from className for consistency

All billing sections now use contentWidth='w-full' for consistent layout.

Fixes #5132

* Fix billing and setting cards

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* pnpm i

* No virtualization

* pagination footer

* sorting and pagination changes

* refactor

* exports

* move data-table to ui

* install

* fmt

* Footer style changes

* sorting and pagination changes

* sorting and footer loading fix

* cleanup

* prefetch pages

* changes for review comments from rabbit and meg

* Ref fix and removed not needed import

* [autofix.ci] apply automated fixes

* sorting fix and key navigation

* review changes mess

* minor rabbit changes

* Update loading-indicator animation delay

* style change on pageination footer

* ref type change

---------

Co-authored-by: Andreas Thomas <dev@chronark.com>
Co-authored-by: Meg Stepp <mcstepp@users.noreply.github.com>
Co-authored-by: Flo <53355483+Flo4604@users.noreply.github.com>
Co-authored-by: Oz <21091016+ogzhanolguncu@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: James P <james@unkey.com>
Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Co-authored-by: gui martins <guilhermev2huehue@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Vansh Malhotra <vansh.malhotra439@gmail.com>
Co-authored-by: Flo <flo@unkey.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants