From 4ad060e08c9552d656ee45e4d9b9b2290cca2037 Mon Sep 17 00:00:00 2001
From: Arjun Komath <arjunkomath@gmail.com>
Date: Thu, 25 Jun 2026 08:43:18 +1000
Subject: [PATCH] Fix: clean up rollout logs

---
 web/.env.example                              | 11 ++--
 web/README.md                                 |  3 +
 web/lib/inngest/functions/rollout-helpers.ts  | 59 ++++++++++++++-----
 web/lib/inngest/functions/rollout-workflow.ts | 50 +++++++++-------
 web/lib/s3.ts                                 | 16 ++++-
 web/next.config.ts                            |  6 --
 web/package.json                              |  3 +-
 web/pnpm-lock.yaml                            | 11 ----
 8 files changed, 95 insertions(+), 64 deletions(-)

diff --git a/web/.env.example b/web/.env.example
index 67fac890..74dbcc30 100644
--- a/web/.env.example
+++ b/web/.env.example
@@ -9,7 +9,7 @@ BETTER_AUTH_SECRET=your-secret-key-here
 ENCRYPTION_KEY=your-64-character-hex-string
 
 # Public URL
-APP_URL=https://your-domain.com
+APP_URL=http://localhost:3000
 
 # Logging (optional)
 VICTORIA_LOGS_URL=http://username:password@victoria-logs:9428
@@ -25,10 +25,11 @@ VM_RETENTION=30d
 # Docker Registry for builds (optional)
 REGISTRY_HOST=registry.example.com
 
-# Inngest (required for production)
-INNGEST_BASE_URL=http://inngest:8288
-INNGEST_SIGNING_KEY=signkey-xxx
-INNGEST_EVENT_KEY=xxx
+# Inngest (local dev via ../compose.dev.yml)
+INNGEST_BASE_URL=http://localhost:8288
+INNGEST_DEV=1
+INNGEST_SIGNING_KEY=
+INNGEST_EVENT_KEY=
 
 # GitHub App Integration (optional)
 GITHUB_APP_ID=your-github-app-id
diff --git a/web/README.md b/web/README.md
index 5c32c69f..7bcb0a37 100644
--- a/web/README.md
+++ b/web/README.md
@@ -6,10 +6,13 @@ Next.js-based control plane for Techulus Cloud container deployment platform.
 
 ```bash
 pnpm install
+cp .env.example .env
+docker compose -f ../compose.dev.yml up -d
 pnpm dev
 ```
 
 Open [http://localhost:3000](http://localhost:3000) to access the control plane.
+Open [http://localhost:8288](http://localhost:8288) to access the Inngest dev server.
 
 ## Stack
 
diff --git a/web/lib/inngest/functions/rollout-helpers.ts b/web/lib/inngest/functions/rollout-helpers.ts
index 7aeb1408..055c7dc9 100644
--- a/web/lib/inngest/functions/rollout-helpers.ts
+++ b/web/lib/inngest/functions/rollout-helpers.ts
@@ -202,7 +202,7 @@ export async function cleanupTerminalDeployments(
 
 export async function cleanupExistingDeployments(
 	serviceId: string,
-): Promise<void> {
+): Promise<{ deletedCount: number }> {
 	const existingDeployments = await db
 		.select()
 		.from(deployments)
@@ -214,35 +214,55 @@ export async function cleanupExistingDeployments(
 			.where(eq(deploymentPorts.deploymentId, dep.id));
 		await db.delete(deployments).where(eq(deployments.id, dep.id));
 	}
+
+	return { deletedCount: existingDeployments.length };
 }
 
+export type CertificateProvisioningResult = {
+	domains: string[];
+	existingDomains: string[];
+	issuedDomains: string[];
+	failedDomains: string[];
+};
+
 export async function issueCertificatesForService(
 	serviceId: string,
-): Promise<void> {
+): Promise<CertificateProvisioningResult> {
 	const servicePortsList = await db
 		.select()
 		.from(servicePorts)
 		.where(eq(servicePorts.serviceId, serviceId));
 
-	const domainsNeedingCerts = servicePortsList
-		.filter((p) => p.isPublic && p.domain)
-		.map((p) => p.domain as string);
+	const domainsNeedingCerts = Array.from(
+		new Set(
+			servicePortsList
+				.filter((p) => p.isPublic && p.domain)
+				.map((p) => (p.domain as string).trim())
+				.filter(Boolean),
+		),
+	);
 
+	const existingDomains: string[] = [];
+	const issuedDomains: string[] = [];
 	const failedDomains: string[] = [];
 
 	for (const domain of domainsNeedingCerts) {
 		const existingCert = await getCertificate(domain);
-		if (!existingCert) {
-			try {
-				await issueCertificate(domain);
-				console.log(`[deploy] issued certificate for ${domain}`);
-			} catch (error) {
-				console.error(
-					`[deploy] failed to issue certificate for ${domain}:`,
-					error,
-				);
-				failedDomains.push(domain);
-			}
+		if (existingCert) {
+			existingDomains.push(domain);
+			continue;
+		}
+
+		try {
+			await issueCertificate(domain);
+			console.log(`[deploy] issued certificate for ${domain}`);
+			issuedDomains.push(domain);
+		} catch (error) {
+			console.error(
+				`[deploy] failed to issue certificate for ${domain}:`,
+				error,
+			);
+			failedDomains.push(domain);
 		}
 	}
 
@@ -251,6 +271,13 @@ export async function issueCertificatesForService(
 			`Certificate provisioning failed for: ${failedDomains.join(", ")}`,
 		);
 	}
+
+	return {
+		domains: domainsNeedingCerts,
+		existingDomains,
+		issuedDomains,
+		failedDomains,
+	};
 }
 
 export async function createDeploymentRecords(
diff --git a/web/lib/inngest/functions/rollout-workflow.ts b/web/lib/inngest/functions/rollout-workflow.ts
index 07ae61b1..3747e1be 100644
--- a/web/lib/inngest/functions/rollout-workflow.ts
+++ b/web/lib/inngest/functions/rollout-workflow.ts
@@ -178,13 +178,15 @@ export const rolloutWorkflow = inngest.createFunction(
 			});
 		} else {
 			await step.run("cleanup-existing", async () => {
-				await cleanupExistingDeployments(serviceId);
-				await ingestRolloutLog(
-					rolloutId,
-					serviceId,
-					"preparing",
-					"Cleaned up existing deployments",
-				);
+				const { deletedCount } = await cleanupExistingDeployments(serviceId);
+				if (deletedCount > 0) {
+					await ingestRolloutLog(
+						rolloutId,
+						serviceId,
+						"preparing",
+						`Cleaned up ${deletedCount} existing deployment(s)`,
+					);
+				}
 			});
 		}
 
@@ -194,13 +196,15 @@ export const rolloutWorkflow = inngest.createFunction(
 				.set({ currentStage: "certificates" })
 				.where(eq(rollouts.id, rolloutId));
 			try {
-				await issueCertificatesForService(serviceId);
-				await ingestRolloutLog(
-					rolloutId,
-					serviceId,
-					"certificates",
-					"Certificates issued",
-				);
+				const result = await issueCertificatesForService(serviceId);
+				if (result.issuedDomains.length > 0) {
+					await ingestRolloutLog(
+						rolloutId,
+						serviceId,
+						"certificates",
+						`Certificates issued for ${result.issuedDomains.length} domain(s)`,
+					);
+				}
 				return { success: true as const };
 			} catch (error) {
 				const message =
@@ -452,7 +456,7 @@ export const rolloutWorkflow = inngest.createFunction(
 
 		if (isRollingUpdate) {
 			await step.run("stop-old-deployments", async () => {
-				await db
+				const stoppedDeployments = await db
 					.update(deployments)
 					.set({ status: "stopping", desired: false })
 					.where(
@@ -460,13 +464,17 @@ export const rolloutWorkflow = inngest.createFunction(
 							eq(deployments.serviceId, serviceId),
 							eq(deployments.status, "draining"),
 						),
+					)
+					.returning({ id: deployments.id });
+
+				if (stoppedDeployments.length > 0) {
+					await ingestRolloutLog(
+						rolloutId,
+						serviceId,
+						"dns_sync",
+						`Stopping ${stoppedDeployments.length} old deployment(s) after DNS sync`,
 					);
-				await ingestRolloutLog(
-					rolloutId,
-					serviceId,
-					"dns_sync",
-					"Stopping old deployments after DNS sync",
-				);
+				}
 			});
 		}
 
diff --git a/web/lib/s3.ts b/web/lib/s3.ts
index bf0ba75f..eba65023 100644
--- a/web/lib/s3.ts
+++ b/web/lib/s3.ts
@@ -1,7 +1,7 @@
 import { DeleteObjectCommand, S3Client } from "@aws-sdk/client-s3";
 import { getBackupStorageConfig } from "@/db/queries";
 
-const DEFAULT_S3_DELETE_TIMEOUT_MS = 15000;
+const DEFAULT_S3_DELETE_TIMEOUT_MS = 60000;
 
 let cachedClient: S3Client | null = null;
 let cachedConfigHash: string | null = null;
@@ -10,8 +10,18 @@ function hashConfig(config: {
 	region: string;
 	endpoint: string | null;
 	accessKey: string;
+	secretKey: string;
 }): string {
-	return `${config.region}-${config.endpoint}-${config.accessKey}`;
+	return `${config.region}-${config.endpoint}-${config.accessKey}-${config.secretKey}`;
+}
+
+function getS3DeleteTimeoutMs(): number {
+	const configuredTimeout = Number(process.env.S3_DELETE_TIMEOUT_MS);
+	if (Number.isFinite(configuredTimeout) && configuredTimeout > 0) {
+		return configuredTimeout;
+	}
+
+	return DEFAULT_S3_DELETE_TIMEOUT_MS;
 }
 
 async function getS3Client(): Promise<S3Client | null> {
@@ -44,7 +54,7 @@ async function getS3Client(): Promise<S3Client | null> {
 export async function deleteFromS3(
 	bucket: string,
 	key: string,
-	timeoutMs = DEFAULT_S3_DELETE_TIMEOUT_MS,
+	timeoutMs = getS3DeleteTimeoutMs(),
 ): Promise<void> {
 	const client = await getS3Client();
 	if (!client) {
diff --git a/web/next.config.ts b/web/next.config.ts
index 42b02f47..398b0d5c 100644
--- a/web/next.config.ts
+++ b/web/next.config.ts
@@ -1,13 +1,7 @@
 import type { NextConfig } from "next";
 
-const allowedDevOrigins = (process.env.ALLOWED_DEV_ORIGINS ?? "")
-	.split(",")
-	.map((value) => value.trim())
-	.filter(Boolean);
-
 const nextConfig: NextConfig = {
 	output: "standalone",
-	allowedDevOrigins,
 };
 
 export default nextConfig;
diff --git a/web/package.json b/web/package.json
index 638ba827..c07d2f15 100644
--- a/web/package.json
+++ b/web/package.json
@@ -3,7 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "dev": "portless cloud --app-port 3000 next dev",
+    "dev": "next dev",
     "build": "next build",
     "start": "next start",
     "test": "vitest run",
@@ -58,7 +58,6 @@
     "drizzle-kit": "^0.31.8",
     "eslint": "^9",
     "eslint-config-next": "16.2.9",
-    "portless": "^0.13.0",
     "tailwindcss": "^4",
     "tsx": "^4.19.2",
     "typescript": "^5",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index 1af7703c..29eed22b 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -145,9 +145,6 @@ importers:
       eslint-config-next:
         specifier: 16.2.9
         version: 16.2.9(@typescript-eslint/parser@8.61.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
-      portless:
-        specifier: ^0.13.0
-        version: 0.13.1
       tailwindcss:
         specifier: ^4
         version: 4.3.1
@@ -4958,12 +4955,6 @@ packages:
     resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
     engines: {node: '>=8'}
 
-  portless@0.13.1:
-    resolution: {integrity: sha512-1B+8Xd4tB1gOIkRHOUEi/LCOp8PIxUsL3ACBX5G2UkKw25TSRNUqAlQcBGxweJgdXb9DPQFX5utKLRUzPFhMnA==}
-    engines: {node: '>=24'}
-    os: [darwin, linux, win32]
-    hasBin: true
-
   possible-typed-array-names@1.1.0:
     resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
     engines: {node: '>= 0.4'}
@@ -10690,8 +10681,6 @@ snapshots:
     dependencies:
       find-up: 3.0.0
 
-  portless@0.13.1: {}
-
   possible-typed-array-names@1.1.0: {}
 
   postcss-selector-parser@7.1.4:
