-
-
-
+
+
+
+ }
+ title="Active Deployment"
+ />
+
+
+
+
+ }
+ title="Domains"
+ />
+
+ {DOMAINS.map((domain) => (
+
+ ))}
+
+
+
+
+ }
+ title="Environment Variables"
+ />
+
+ }
+ title="Production"
+ projectId={projectId}
+ environment="production"
+ />
+ }
+ title="Preview"
+ projectId={projectId}
+ environment="preview"
+ />
+
+
);
}
+
+function SectionHeader({ icon, title }: { icon: ReactNode; title: string }) {
+ return (
+
+ );
+}
+
+function Section({ children }: { children: ReactNode }) {
+ return
{children}
;
+}
diff --git a/apps/dashboard/lib/trpc/routers/deploy/project/active-deployment/getBuildLogs.ts b/apps/dashboard/lib/trpc/routers/deploy/project/active-deployment/getBuildLogs.ts
new file mode 100644
index 0000000000..3df647abaa
--- /dev/null
+++ b/apps/dashboard/lib/trpc/routers/deploy/project/active-deployment/getBuildLogs.ts
@@ -0,0 +1,230 @@
+import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc";
+import { z } from "zod";
+
+const LogLevel = z.enum(["info", "warning", "error"]);
+
+const LogEntry = z.object({
+ id: z.string(),
+ timestamp: z.number(),
+ level: LogLevel.optional(),
+ message: z.string(),
+});
+
+const deploymentLogsInputSchema = z.object({
+ deploymentId: z.string(),
+});
+
+const deploymentLogsOutputSchema = z.object({
+ logs: z.array(LogEntry),
+});
+
+const MOCK_LOGS: DeploymentLog[] = [
+ {
+ id: "log_1",
+ timestamp: Date.now() - 360000,
+ message: "Running build in us-east-1 (Washington, D.C.) — iad1",
+ },
+ {
+ id: "log_2",
+ timestamp: Date.now() - 359000,
+ message: "Cloning github.com/acme/api (Branch: main, Commit: e5f6a7b)",
+ level: "error",
+ },
+ {
+ id: "log_3",
+ timestamp: Date.now() - 358000,
+ message: "Build cache not found for this project",
+ },
+ {
+ id: "log_4",
+ timestamp: Date.now() - 357000,
+ message: "Clone completed in 307ms",
+ level: "warning",
+ },
+ {
+ id: "log_5",
+ timestamp: Date.now() - 356000,
+ message: "Running `unkey build`",
+ },
+ {
+ id: "log_6",
+ timestamp: Date.now() - 355000,
+ message: "Unkey CLI 0.42.1",
+ },
+ {
+ id: "log_7",
+ timestamp: Date.now() - 354000,
+ message: "Validating config files...",
+ },
+ {
+ id: "log_8",
+ timestamp: Date.now() - 353000,
+ message: "✓ env-vars.json validated",
+ },
+ {
+ id: "log_9",
+ timestamp: Date.now() - 352000,
+ message: "✓ runtime.json validated",
+ },
+ {
+ id: "log_10",
+ timestamp: Date.now() - 351000,
+ message: "✓ secrets.json decrypted successfully",
+ },
+ {
+ id: "log_11",
+ timestamp: Date.now() - 350000,
+ message: "✓ openapi.yaml parsed — 13 endpoints detected",
+ },
+ {
+ id: "log_12",
+ timestamp: Date.now() - 349000,
+ message: '⚠️ Warning: Environment variable "STRIPE_SECRET" is not set. Using fallback value',
+ level: "warning",
+ },
+ {
+ id: "log_13",
+ timestamp: Date.now() - 348000,
+ message: "Setting up runtime environment",
+ },
+ {
+ id: "log_14",
+ timestamp: Date.now() - 347000,
+ message: "Target image: unkey:latest",
+ },
+ {
+ id: "log_15",
+ timestamp: Date.now() - 346000,
+ message: "Build environment: nodejs18.x | Linux (x64)",
+ },
+ {
+ id: "log_16",
+ timestamp: Date.now() - 345000,
+ message: "Installing dependencies...",
+ },
+ {
+ id: "log_17",
+ timestamp: Date.now() - 344000,
+ message: "✓ Dependencies installed in 1.3s",
+ },
+ {
+ id: "log_18",
+ timestamp: Date.now() - 343000,
+ message: "Compiling project...",
+ },
+ {
+ id: "log_19",
+ timestamp: Date.now() - 342000,
+ message: "✓ Build successful in 331ms",
+ },
+ {
+ id: "log_20",
+ timestamp: Date.now() - 341000,
+ message: "Registering healthcheck: GET /health every 30s",
+ },
+ {
+ id: "log_21",
+ timestamp: Date.now() - 340000,
+ message: "Checking availability in selected regions...",
+ },
+ {
+ id: "log_22",
+ timestamp: Date.now() - 339000,
+ message: "✓ us-east-1 available (2 slots)",
+ },
+ {
+ id: "log_23",
+ timestamp: Date.now() - 338000,
+ message: "✓ eu-west-1 available (1 slot)",
+ },
+ {
+ id: "log_24",
+ timestamp: Date.now() - 337000,
+ message: "✓ ap-south-1 available (1 slot)",
+ },
+ {
+ id: "log_25",
+ timestamp: Date.now() - 336000,
+ message: "Creating deployment image...",
+ },
+ {
+ id: "log_26",
+ timestamp: Date.now() - 335000,
+ message:
+ "❌ Error: Failed to optimize image layer for region eu-west-1. Using fallback strategy",
+ level: "error",
+ },
+ {
+ id: "log_27",
+ timestamp: Date.now() - 334000,
+ message: "✓ Image built: 210mb",
+ },
+ {
+ id: "log_28",
+ timestamp: Date.now() - 333000,
+ message: "Launching 4 VM instances",
+ },
+ {
+ id: "log_29",
+ timestamp: Date.now() - 332000,
+ message: "✓ Scaling enabled: 0–5 instances at 80% CPU",
+ },
+ {
+ id: "log_30",
+ timestamp: Date.now() - 331000,
+ message: "Deploying to:",
+ },
+ {
+ id: "log_31",
+ timestamp: Date.now() - 330000,
+ message: " - api.gateway.com (https)",
+ },
+ {
+ id: "log_32",
+ timestamp: Date.now() - 329000,
+ message: " - internal.api.gateway.com (http)",
+ },
+ {
+ id: "log_33",
+ timestamp: Date.now() - 328000,
+ message: " - dashboard:3000, 8080, 5792",
+ },
+ {
+ id: "log_34",
+ timestamp: Date.now() - 327000,
+ message: "Activating deployment: v_alpha001",
+ },
+ {
+ id: "log_35",
+ timestamp: Date.now() - 326000,
+ message: "✓ Deployment active",
+ },
+ {
+ id: "log_36",
+ timestamp: Date.now() - 325000,
+ message: "View logs at /dashboard/logs/alpha001",
+ },
+ {
+ id: "log_37",
+ timestamp: Date.now() - 324000,
+ message: "Deployment completed in 5.7s",
+ },
+];
+
+export type DeploymentLog = z.infer
;
+export type DeploymentLogsInput = z.infer;
+
+export const getDeploymentBuildLogs = t.procedure
+ .use(requireUser)
+ .use(requireWorkspace)
+ .use(withRatelimit(ratelimit.read))
+ .input(deploymentLogsInputSchema)
+ .output(deploymentLogsOutputSchema)
+ .query(async () => {
+ // In real implementation: fetch from database/logging service by deploymentId
+ // If not sorted, sort by timestamp asc for chronological build order
+ const sortedLogs = MOCK_LOGS.sort((a, b) => a.timestamp - b.timestamp);
+ return {
+ logs: sortedLogs,
+ };
+ });
diff --git a/apps/dashboard/lib/trpc/routers/deploy/project/active-deployment/getDetails.ts b/apps/dashboard/lib/trpc/routers/deploy/project/active-deployment/getDetails.ts
new file mode 100644
index 0000000000..970c1cc7ac
--- /dev/null
+++ b/apps/dashboard/lib/trpc/routers/deploy/project/active-deployment/getDetails.ts
@@ -0,0 +1,99 @@
+import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc";
+import { z } from "zod";
+
+const deploymentDetailsOutputSchema = z.object({
+ // Active deployment
+ repository: z.object({
+ owner: z.string(),
+ name: z.string(),
+ }),
+ branch: z.string(),
+ commit: z.string(),
+ description: z.string(),
+ image: z.string(),
+ author: z.object({
+ name: z.string(),
+ avatar: z.string(),
+ }),
+ createdAt: z.number(),
+
+ // Runtime settings
+ instances: z.number(),
+ regions: z.array(z.string()),
+ cpu: z.number(),
+ memory: z.number(),
+ storage: z.number(),
+ healthcheck: z.object({
+ method: z.string(),
+ path: z.string(),
+ interval: z.number(),
+ }),
+ scaling: z.object({
+ min: z.number(),
+ max: z.number(),
+ threshold: z.number(),
+ }),
+
+ // Build info
+ imageSize: z.number(),
+ buildTime: z.number(),
+ buildStatus: z.enum(["success", "failed", "pending"]),
+ baseImage: z.string(),
+ builtAt: z.number(),
+});
+
+type DeploymentDetailsOutputSchema = z.infer;
+export type DeploymentDetails = z.infer;
+
+export const getDeploymentDetails = t.procedure
+ .use(requireUser)
+ .use(requireWorkspace)
+ .use(withRatelimit(ratelimit.read))
+ .input(
+ z.object({
+ deploymentId: z.string(),
+ }),
+ )
+ .output(deploymentDetailsOutputSchema)
+ .query(() => {
+ //TODO: This should make a db look-up find the "active" and "latest" and "prod" deployment
+ const details: DeploymentDetailsOutputSchema = {
+ repository: {
+ owner: "acme",
+ name: "acme",
+ },
+ branch: "main",
+ commit: "e5f6a7b",
+ description: "Add auth routes + logging",
+ image: "unkey:latest",
+ author: {
+ name: "Oz",
+ avatar: "https://avatars.githubusercontent.com/u/138932600?s=48&v=4",
+ },
+ createdAt: Date.now(),
+
+ instances: 4,
+ regions: ["eu-west-2", "us-east-1", "ap-southeast-1"],
+ cpu: 32,
+ memory: 512,
+ storage: 1024,
+ healthcheck: {
+ method: "GET",
+ path: "/health",
+ interval: 30,
+ },
+ scaling: {
+ min: 0,
+ max: 5,
+ threshold: 80,
+ },
+
+ imageSize: 210,
+ buildTime: 45,
+ buildStatus: "success",
+ baseImage: "node:18-alpine",
+ builtAt: Date.now() - 300000,
+ };
+
+ return details;
+ });
diff --git a/apps/dashboard/lib/trpc/routers/deploy/project/envs/list.ts b/apps/dashboard/lib/trpc/routers/deploy/project/envs/list.ts
new file mode 100644
index 0000000000..d5fda50ad1
--- /dev/null
+++ b/apps/dashboard/lib/trpc/routers/deploy/project/envs/list.ts
@@ -0,0 +1,87 @@
+import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc";
+import { z } from "zod";
+
+const envVarSchema = z.object({
+ id: z.string(),
+ key: z.string(),
+ value: z.string(),
+ isSecret: z.boolean(),
+});
+
+const environmentVariablesOutputSchema = z.object({
+ production: z.array(envVarSchema),
+ preview: z.array(envVarSchema),
+ development: z.array(envVarSchema).optional(),
+});
+
+export type EnvironmentVariables = z.infer;
+export type EnvVar = z.infer;
+
+export const VARIABLES: EnvironmentVariables = {
+ production: [
+ {
+ id: "1",
+ key: "DATABASE_URL",
+ value: "postgresql://user:pass@prod.db.com:5432/app",
+ isSecret: true,
+ },
+ {
+ id: "2",
+ key: "API_KEY",
+ value: "sk_prod_1234567890abcdef",
+ isSecret: true,
+ },
+ {
+ id: "3",
+ key: "NODE_ENV",
+ value: "production",
+ isSecret: false,
+ },
+ {
+ id: "4",
+ key: "REDIS_URL",
+ value: "redis://prod.redis.com:6379",
+ isSecret: true,
+ },
+ {
+ id: "5",
+ key: "LOG_LEVEL",
+ value: "info",
+ isSecret: false,
+ },
+ ],
+ preview: [
+ {
+ id: "6",
+ key: "DATABASE_URL",
+ value: "postgresql://user:pass@staging.db.com:5432/app",
+ isSecret: true,
+ },
+ {
+ id: "7",
+ key: "API_KEY",
+ value: "sk_test_abcdef1234567890",
+ isSecret: true,
+ },
+ {
+ id: "8",
+ key: "NODE_ENV",
+ value: "development",
+ isSecret: false,
+ },
+ ],
+};
+
+export const getEnvs = t.procedure
+ .use(requireUser)
+ .use(requireWorkspace)
+ .use(withRatelimit(ratelimit.read))
+ .input(
+ z.object({
+ projectId: z.string(),
+ }),
+ )
+ .output(environmentVariablesOutputSchema)
+ .query(() => {
+ return VARIABLES;
+ });
diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts
index 242718239c..c6e8f0e7f3 100644
--- a/apps/dashboard/lib/trpc/routers/index.ts
+++ b/apps/dashboard/lib/trpc/routers/index.ts
@@ -37,9 +37,12 @@ import { searchRolesPermissions } from "./authorization/roles/permissions/search
import { queryRoles } from "./authorization/roles/query";
import { upsertRole } from "./authorization/roles/upsert";
import { queryUsage } from "./billing/query-usage";
+import { getDeploymentBuildLogs } from "./deploy/project/active-deployment/getBuildLogs";
+import { getDeploymentDetails } from "./deploy/project/active-deployment/getDetails";
import { createProject } from "./deploy/project/create";
import { queryDeployments } from "./deploy/project/deployment/list";
import { deploymentListLlmSearch } from "./deploy/project/deployment/llm-search";
+import { getEnvs } from "./deploy/project/envs/list";
import { queryProjects } from "./deploy/project/list";
import { deploymentRouter } from "./deployment";
import { createIdentity } from "./identity/create";
@@ -313,10 +316,17 @@ export const router = t.router({
project: t.router({
list: queryProjects,
create: createProject,
- }),
- deployment: t.router({
- list: queryDeployments,
- search: deploymentListLlmSearch,
+ activeDeployment: t.router({
+ details: getDeploymentDetails,
+ buildLogs: getDeploymentBuildLogs,
+ }),
+ envs: t.router({
+ getEnvs,
+ }),
+ deployment: t.router({
+ list: queryDeployments,
+ search: deploymentListLlmSearch,
+ }),
}),
}),
deployment: deploymentRouter,
diff --git a/internal/icons/src/icons/bolt.tsx b/internal/icons/src/icons/bolt.tsx
index 2dc0e8ede1..38c3690e0f 100644
--- a/internal/icons/src/icons/bolt.tsx
+++ b/internal/icons/src/icons/bolt.tsx
@@ -10,11 +10,18 @@
* https://nucleoapp.com/license
*/
import type React from "react";
-import type { IconProps } from "../props";
+import { type IconProps, sizeMap } from "../props";
-export const Bolt: React.FC = (props) => {
+export const Bolt: React.FC = ({ size = "xl-thin", filled, ...props }) => {
+ const { size: pixelSize, strokeWidth } = sizeMap[size];
return (
-