From 8986f7c8a4c9e7f9f27b69fb4f28fb13b602f4d8 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 02:19:06 +0100 Subject: [PATCH 01/15] fixed the auth issue --- src/app/api/auth/callback/route.ts | 14 ++++++++------ src/lib/dockship/auth.ts | 12 +----------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/app/api/auth/callback/route.ts b/src/app/api/auth/callback/route.ts index 24b3ec5..a8ec8a3 100644 --- a/src/app/api/auth/callback/route.ts +++ b/src/app/api/auth/callback/route.ts @@ -1,16 +1,18 @@ import axios from "axios"; import { StatusCodes } from "http-status-codes"; import { cookies } from "next/headers"; -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; -export async function GET(request: Request) { +export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get("code"); + console.log(request.url, request.nextUrl); + if (!code) { return NextResponse.json( { error: "No code provided" }, - { status: StatusCodes.BAD_REQUEST } + { status: StatusCodes.BAD_REQUEST }, ); } @@ -26,7 +28,7 @@ export async function GET(request: Request) { headers: { Accept: "application/json", }, - } + }, ); const accessToken = response.data.access_token; @@ -38,12 +40,12 @@ export async function GET(request: Request) { secure: process.env.NODE_ENV === "production", }); - return NextResponse.redirect(new URL("/overview", request.url)); + return NextResponse.redirect(`${request.nextUrl.origin}/overview`); } catch (error) { console.log(error); return NextResponse.json( { error: "Error exchanging code for access token" }, - { status: StatusCodes.INTERNAL_SERVER_ERROR } + { status: StatusCodes.INTERNAL_SERVER_ERROR }, ); } } diff --git a/src/lib/dockship/auth.ts b/src/lib/dockship/auth.ts index 017f799..1b2c04f 100644 --- a/src/lib/dockship/auth.ts +++ b/src/lib/dockship/auth.ts @@ -1,21 +1,11 @@ "use server"; import { client } from "@/core/auth/oauth"; -import { cookies, headers } from "next/headers"; +import { cookies } from "next/headers"; import { redirect } from "next/navigation"; export async function redirectAuth() { - // Get headers of the incoming request - const hdrs = await headers(); - - // Get protocol and host - const host = hdrs.get("host"); - const proto = hdrs.get("x-forwarded-proto") || "http"; - - const baseUrl = `${proto}://${host}`; - const authorizationUri = client.authorizeURL({ - redirect_uri: `${baseUrl}/api/auth/callback`, scope: "user:email", state: "random_state_string", }); From 3dd6b22c06a0d76e31178ff290d01030624ae662 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 03:06:54 +0100 Subject: [PATCH 02/15] Prune after deployment --- src/core/deployment.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/core/deployment.ts b/src/core/deployment.ts index 99f6e7b..7ffea0d 100644 --- a/src/core/deployment.ts +++ b/src/core/deployment.ts @@ -7,7 +7,6 @@ import path from "path"; import fs from "fs"; import pLimit from "p-limit"; import { randomUUID } from "crypto"; -import simpleGit from "simple-git"; import { cloneRepo } from "./git"; const docker = new Docker(); @@ -35,7 +34,7 @@ function formatPorts(ports: Port[]) { } export async function getDeployments( - apps: Application[] + apps: Application[], ): Promise { const limit = pLimit(5); @@ -91,27 +90,27 @@ export async function getDeployments( status: containerInfo.State?.Running ? "running" : "stopped", ports: [...ports.entries()].map( ([publicPort, privatePort]) => - [publicPort, privatePort] as [number, number] + [publicPort, privatePort] as [number, number], ), }; } catch { // Container may have stopped / been removed return null; } - }) - ) + }), + ), ); // Remove failed containers return results.filter( - (deployment: any): deployment is Deployment => deployment !== null + (deployment: any): deployment is Deployment => deployment !== null, ); } export async function deployApp( name: string, app: Application, - ports: Port[] + ports: Port[], ): Promise { const deployId = randomUUID(); const deployPath = path.join("data", "deploys", deployId); @@ -134,12 +133,12 @@ export async function deployApp( context: deployPath, src: ["Dockerfile", "."], }, - { t: imageTag } + { t: imageTag }, ); await new Promise((resolve, reject) => { docker.modem.followProgress(stream, (err: Error | null) => - err ? reject(err) : resolve() + err ? reject(err) : resolve(), ); }); @@ -152,12 +151,14 @@ export async function deployApp( PortBindings: formatPorts(ports), }, Env: Object.entries(app.env).map( - ([key, { value }]) => `${key}="${value.replaceAll('"', '\\"')}"` + ([key, { value }]) => `${key}="${value.replaceAll('"', '\\"')}"`, ), }); await container.start(); + await docker.pruneImages(); + return container.id; } catch (err) { console.error("Failed to deploy app:", err); From 8b71b70570584f6f91a19986068c13009f2f378b Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 12:57:27 +0100 Subject: [PATCH 03/15] Tar pack --- package-lock.json | 222 +++++++++++++++++++++++++++++++++++++---- package.json | 4 +- src/core/deployment.ts | 11 +- 3 files changed, 211 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1b78881..65ff5ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/dockerode": "^3.3.47", "@types/js-cookie": "^3.0.6", "@types/simple-oauth2": "^5.0.8", + "@types/tar-fs": "^2.0.4", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -39,7 +40,8 @@ "simple-git": "^3.30.0", "simple-oauth2": "^5.1.0", "systeminformation": "^5.28.2", - "tailwind-merge": "^3.4.0" + "tailwind-merge": "^3.4.0", + "tar-fs": "^3.1.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -2989,6 +2991,25 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/@types/tar-fs": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-2.0.4.tgz", + "integrity": "sha512-ipPec0CjTmVDWE+QKr9cTmIIoTl7dFG/yARCM5MqK8i6CNLIG1P8x4kwDsOQY1ChZOZjH0wO9nvfgBvWl4R3kA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tar-stream": "*" + } + }, + "node_modules/@types/tar-stream": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz", + "integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3051,6 +3072,111 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/b4a": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz", + "integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz", + "integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3504,6 +3630,34 @@ "node": ">= 8.0" } }, + "node_modules/dockerode/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/dockerode/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -3617,6 +3771,15 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -3649,6 +3812,12 @@ "node": ">=6.0.0" } }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -5014,6 +5183,17 @@ "nan": "^2.23.0" } }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5142,31 +5322,37 @@ } }, "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "license": "MIT", "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz", + "integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" } }, "node_modules/tiny-invariant": { diff --git a/package.json b/package.json index 7e79ac0..cfdfe1e 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@types/dockerode": "^3.3.47", "@types/js-cookie": "^3.0.6", "@types/simple-oauth2": "^5.0.8", + "@types/tar-fs": "^2.0.4", "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -39,7 +40,8 @@ "simple-git": "^3.30.0", "simple-oauth2": "^5.1.0", "systeminformation": "^5.28.2", - "tailwind-merge": "^3.4.0" + "tailwind-merge": "^3.4.0", + "tar-fs": "^3.1.1" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/core/deployment.ts b/src/core/deployment.ts index 7ffea0d..3677ab4 100644 --- a/src/core/deployment.ts +++ b/src/core/deployment.ts @@ -8,6 +8,7 @@ import fs from "fs"; import pLimit from "p-limit"; import { randomUUID } from "crypto"; import { cloneRepo } from "./git"; +import tar from "tar-fs"; const docker = new Docker(); @@ -128,13 +129,9 @@ export async function deployApp( } try { - const stream = await docker.buildImage( - { - context: deployPath, - src: ["Dockerfile", "."], - }, - { t: imageTag }, - ); + const stream = await docker.buildImage(tar.pack(deployPath), { + t: imageTag, + }); await new Promise((resolve, reject) => { docker.modem.followProgress(stream, (err: Error | null) => From de3ac18b237ef33eca427899ace5c3cb9fc7b723 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 13:43:24 +0100 Subject: [PATCH 04/15] Improved deployments ui --- src/app/deployments/page.tsx | 125 +++++++++++++++-------------------- 1 file changed, 55 insertions(+), 70 deletions(-) diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 19cf303..7a37fd5 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -2,16 +2,32 @@ import { useEffect, useMemo, useState } from "react"; import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; -import { Card, CardContent } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Rocket, RotateCcw, Eye, MoreVertical, Plus } from "lucide-react"; +import { + Rocket, + RotateCcw, + Eye, + MoreVertical, + Plus, + Play, + Pause, + Trash, +} from "lucide-react"; import { Application, Deployment } from "@/lib/types"; import { getDeployments } from "@/lib/dockship/deploy"; import { getApps } from "@/lib/dockship/application"; import { cn } from "@/lib/utils"; import DeployAppDialog from "@/components/dialogs/DeployApp"; -import { useAsync } from "@/hooks/use-async"; +import { useAsync, useAsyncInterval } from "@/hooks/use-async"; +import RemoveDialog from "@/components/dialogs/Remove"; const statusDot = { running: "bg-chart-2", @@ -20,7 +36,7 @@ const statusDot = { } as const; export default function DeploymentsPage() { - const { value: apps } = useAsync([], getApps); + const { value: apps } = useAsyncInterval([], getApps, 5000); const { value: deployments, loading } = useAsync([], getDeployments, [apps]); @@ -44,7 +60,7 @@ export default function DeploymentsPage() { {loading ? "Loading…" - : `${deployments.length} deployment${deployments.length > 0 && 's'}`} + : `${deployments.length} deployment${deployments.length > 0 && "s"}`} @@ -55,79 +71,48 @@ export default function DeploymentsPage() { - {/* Desktop */} -
+
{deployments.map((dep) => { const appName = appNameByContainer.get(dep.container) ?? "Unknown app"; return ( -
-
- -
-
{appName}
-
+ + +
+
+ + {appName} + + {dep.ports.map(([to, from]) => ( + + {from} → {to} + + ))} + +
+

{dep.image} +

+
+ + {}} />
-
- -
- - - -
-
- ); - })} -
- - {/* Mobile */} -
- {deployments.map((dep) => { - const appName = - appNameByContainer.get(dep.container) ?? "Unknown app"; - - return ( -
-
-
- - {appName} -
- - {dep.status} - -
- -
- - - -
-
+ + ); })}
From 4efb09d21446f6f5c4dbb3a761f7722aaa9ca319 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 13:46:46 +0100 Subject: [PATCH 05/15] Improved deployments ui --- src/app/deployments/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 7a37fd5..08cf9dc 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -101,7 +101,11 @@ export default function DeploymentsPage() { {dep.image}

- {}} /> From 8f320747ad3a773add032a9208fd74f41ee2edc0 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:09:58 +0100 Subject: [PATCH 07/15] Start/stop deployments --- src/app/api/deploy/route.ts | 32 +++++++++++++++++++++++++++++--- src/app/deployments/page.tsx | 27 ++++++++++++++++++++++----- src/core/deployment.ts | 17 +++++++++++++++++ src/lib/dockship/deploy.ts | 30 ++++++++++++++++++++++++++---- 4 files changed, 94 insertions(+), 12 deletions(-) diff --git a/src/app/api/deploy/route.ts b/src/app/api/deploy/route.ts index 8639ff5..676d11f 100644 --- a/src/app/api/deploy/route.ts +++ b/src/app/api/deploy/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import testAuth from "../auth"; import { StatusCodes } from "http-status-codes"; -import { deployApp } from "@/core/deployment"; +import { deployApp, toggleDeployment } from "@/core/deployment"; import { Application, Port } from "@/lib/types"; export async function POST(req: NextRequest): Promise { @@ -15,14 +15,14 @@ export async function POST(req: NextRequest): Promise { if (auth) { return NextResponse.json( { message: auth }, - { status: StatusCodes.UNAUTHORIZED } + { status: StatusCodes.UNAUTHORIZED }, ); } if (!name || !app) { return NextResponse.json( { message: "Body missing name, appId, nodeId" }, - { status: StatusCodes.BAD_REQUEST } + { status: StatusCodes.BAD_REQUEST }, ); } @@ -33,3 +33,29 @@ export async function POST(req: NextRequest): Promise { deployId, }); } + +export async function PUT(req: NextRequest): Promise { + const auth = await testAuth(req); + const { deployId, action }: { deployId: string; action: "start" | "stop" } = + await req.json(); + + if (auth) { + return NextResponse.json( + { message: auth }, + { status: StatusCodes.UNAUTHORIZED }, + ); + } + + if (!deployId || !action) { + return NextResponse.json( + { message: "Body missing deployId or action" }, + { status: StatusCodes.BAD_REQUEST }, + ); + } + + await toggleDeployment(deployId, action); + + return NextResponse.json({ + message: "ok", + }); +} diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 3142284..3fa6ede 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -10,8 +10,8 @@ import { } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Plus, Play, Pause } from "lucide-react"; -import { getDeployments } from "@/lib/dockship/deploy"; +import { Plus, Play, Pause, Square } from "lucide-react"; +import { getDeployments, toggleDeployment } from "@/lib/dockship/deploy"; import { getApps } from "@/lib/dockship/application"; import { cn } from "@/lib/utils"; import DeployAppDialog from "@/components/dialogs/DeployApp"; @@ -39,6 +39,14 @@ export default function DeploymentsPage() { return map; }, [apps]); + const changeDeploymentStatus = async ( + deployId: string, + action: "start" | "stop", + ) => { + console.log(`Toggling deployment ${deployId} with action ${action}`); + await toggleDeployment(deployId, action); + }; + return (
- {}} /> diff --git a/src/core/deployment.ts b/src/core/deployment.ts index 3677ab4..1f73d8a 100644 --- a/src/core/deployment.ts +++ b/src/core/deployment.ts @@ -164,3 +164,20 @@ export async function deployApp( fs.rmSync(deployPath, { recursive: true, force: true }); } } + +export async function toggleDeployment( + containerId: string, + action: "start" | "stop", +) { + const container = docker.getContainer(containerId); + + try { + if (action === "start") { + await container.start(); + } else { + await container.stop(); + } + } catch (err) { + console.error(`Failed to ${action} container ${containerId}:`, err); + } +} diff --git a/src/lib/dockship/deploy.ts b/src/lib/dockship/deploy.ts index 0a39828..0c2fb87 100644 --- a/src/lib/dockship/deploy.ts +++ b/src/lib/dockship/deploy.ts @@ -17,8 +17,8 @@ export async function getDeployments(): Promise { await axios.post(`http://${n.ip}:3000/api/get-deployments`, apps, { headers: { Authorization: `ApiKey ${n.key}` }, }) - ).data.deployments - ) + ).data.deployments, + ), ) ).flat(); } @@ -27,7 +27,7 @@ export async function deployApp( name: string, appId: string, nodeId: string, - ports: Port[] + ports: Port[], ): Promise { const nodes = await getNodes(); @@ -44,7 +44,7 @@ export async function deployApp( { name, app, ports }, { headers: { Authorization: `ApiKey ${node.key}` }, - } + }, ) ).data.deployId; @@ -54,3 +54,25 @@ export async function deployApp( return deployId; } + +export async function toggleDeployment( + deployId: string, + action: "start" | "stop", +) { + const nodes = await getNodes(); + + await Promise.all( + nodes.map( + async (node) => + await axios.put( + `http://${node.ip}:3000/api/deploy`, + { deployId, action }, + { + headers: { Authorization: `ApiKey ${node.key}` }, + }, + ), + ), + ); + + return "ok"; +} From 32be7ee586b3bb9925a8fac5065c9b1e0a5d2983 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:11:21 +0100 Subject: [PATCH 08/15] Deployment data --- src/app/overview/page.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index b20c6f0..761ca8d 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -40,7 +40,7 @@ export default function OverviewPage() { const { value: deployments } = useAsyncInterval( [], async () => (await getDeployments()).filter((d) => d !== null), - 3000 + 3000, ); const appNameByContainer = useMemo(() => { @@ -67,8 +67,8 @@ export default function OverviewPage() { /> d.status === "running").length} running`} icon={} /> sum + n.containers.filter((c) => c.State === "running").length, - 0 + 0, )} subtitle="running" icon={} @@ -164,7 +164,7 @@ export default function OverviewPage() { "size-2 rounded-full", (dep.status === "running" && "bg-chart-2") || (dep.status === "pending" && "bg-chart-1") || - "bg-slate-400 dark:bg-slate-600" + "bg-slate-400 dark:bg-slate-600", )} />
From b088f19b185635026858729c2f1f7983e2b6d693 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:17:15 +0100 Subject: [PATCH 09/15] Deployment name --- src/app/deployments/page.tsx | 10 ++++++---- src/app/overview/page.tsx | 25 +++++++------------------ src/core/deployment.ts | 1 + src/lib/types.ts | 1 + 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 3fa6ede..93ea2d5 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -70,9 +70,6 @@ export default function DeploymentsPage() {
{deployments.map((dep) => { - const appName = - appNameByContainer.get(dep.container) ?? "Unknown app"; - return ( @@ -85,7 +82,12 @@ export default function DeploymentsPage() { statusDot.stopped, )} /> - {appName} + + {dep.name} @{" "} + + {appNameByContainer.get(dep.container) ?? "Unknown app"} + + {dep.ports.map(([to, from]) => ( diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 761ca8d..82d4666 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -5,26 +5,11 @@ import { StatCard } from "@/components/dashboard/StatCard"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Box, - Server, - Container, - Activity, - ArrowRight, - Clock, - Rocket, -} from "lucide-react"; +import { Box, Server, Container, ArrowRight, Rocket } from "lucide-react"; import { Progress } from "@/components/ui/progress"; import Link from "next/link"; import { getLiveNodes } from "@/lib/dockship/node"; -import { useEffect, useMemo, useState } from "react"; -import { - Application, - Deployment, - Docker, - Node, - NodeLiveData, -} from "@/lib/types"; +import { useMemo } from "react"; import { getDocker } from "@/lib/dockship/docker"; import { getApps } from "@/lib/dockship/application"; import { cn } from "@/lib/utils"; @@ -169,7 +154,11 @@ export default function OverviewPage() { />
- {appNameByContainer.get(dep.container)} + {dep.name} @{" "} + + {appNameByContainer.get(dep.container) ?? + "Unknown app"} +
{dep.image.slice(7, 19)} diff --git a/src/core/deployment.ts b/src/core/deployment.ts index 1f73d8a..e5c529c 100644 --- a/src/core/deployment.ts +++ b/src/core/deployment.ts @@ -84,6 +84,7 @@ export async function getDeployments( } return { + name: containerInfo.Name.replace(/^\//, ""), image: containerInfo.Image, container: containerInfo.Id, cpu, diff --git a/src/lib/types.ts b/src/lib/types.ts index a7bfac6..0c3465c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -58,6 +58,7 @@ export interface Application { } export interface Deployment { + name: string; image: string; container: string; cpu: number; From e1e717a54cd5aeb188cfae60b2c5edcfc27a9396 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:28:41 +0100 Subject: [PATCH 10/15] Caching --- src/app/applications/page.tsx | 3 +- src/app/deployments/page.tsx | 9 ++++- src/app/environment/page.tsx | 9 ++++- src/app/monitoring/page.tsx | 20 +++++++--- src/app/nodes/page.tsx | 12 ++++-- src/app/overview/page.tsx | 19 ++++++--- src/components/dialogs/DeployApp.tsx | 4 +- src/components/dialogs/RegisterApp.tsx | 3 -- src/hooks/use-async.ts | 54 +++++++++++++++++--------- 9 files changed, 90 insertions(+), 43 deletions(-) diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx index 041c584..ced7cbb 100644 --- a/src/app/applications/page.tsx +++ b/src/app/applications/page.tsx @@ -20,9 +20,10 @@ import { useAsyncInterval } from "@/hooks/use-async"; export default function ApplicationsPage() { const { value: apps, setValue: setApps } = useAsyncInterval( + "apps", [], getApps, - 3000 + 3000, ); const remove = async (appId: string) => { diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 93ea2d5..4bbb912 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -25,9 +25,14 @@ const statusDot = { } as const; export default function DeploymentsPage() { - const { value: apps } = useAsyncInterval([], getApps, 5000); + const { value: apps } = useAsyncInterval("apps", [], getApps, 5000); - const { value: deployments, loading } = useAsync([], getDeployments, [apps]); + const { value: deployments, loading } = useAsync( + "deployments", + [], + getDeployments, + [apps], + ); const appNameByContainer = useMemo(() => { const map = new Map(); diff --git a/src/app/environment/page.tsx b/src/app/environment/page.tsx index 458b577..6403dcc 100644 --- a/src/app/environment/page.tsx +++ b/src/app/environment/page.tsx @@ -21,11 +21,16 @@ import { useAsync } from "@/hooks/use-async"; export default function EnvironmentPage() { const [isSaving, setIsSaving] = useState(false); const [showSecrets, setShowSecrets] = useState>({}); - const { value: envGroups, setValue: setEnvGroups } = useAsync({}, getEnv); + const { value: envGroups, setValue: setEnvGroups } = useAsync( + "env", + {}, + getEnv, + ); const { value: selectedApp, setValue: setSelectedApp } = useAsync( + "selectedApp", "", async () => Object.keys(envGroups)[0] || "", - [envGroups] + [envGroups], ); const handleSave = async () => { diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index f290113..f67a656 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -31,10 +31,17 @@ export default function MonitoringPage() { const isMobile = useIsMobile(); // Metrics polling every 30 seconds - const { value: metrics } = useAsyncInterval([], getMetrics, 30000, []); + const { value: metrics } = useAsyncInterval( + "metrics", + [], + getMetrics, + 30000, + [], + ); // Live nodes polling every 3 seconds const { value: liveData } = useAsyncInterval( + "liveNodes", { cpuUsage: 0, ramUsage: 0 }, async () => { const nodes = await getLiveNodes(); @@ -44,11 +51,12 @@ export default function MonitoringPage() { }; }, 3000, - [] + [], ); // Container stats const { value: containerStats } = useAsync( + "containerStats", [], async () => { const nodes = await getNodes(); @@ -61,18 +69,18 @@ export default function MonitoringPage() { const stats = await getContainerStats(c.Id); if (!stats) return undefined; return { ...stats, name: c.Names[0] }; - }) - ) + }), + ), ) ).filter((s): s is SimpleStats & { name: string } => s !== undefined); }, - [] + [], ); const containerCPU = containerStats.reduce((acc, val) => acc + val.cpu, 0); const containerMemory = containerStats.reduce( (acc, val) => acc + val.memory, - 0 + 0, ); return ( diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx index 70df860..30a4d15 100644 --- a/src/app/nodes/page.tsx +++ b/src/app/nodes/page.tsx @@ -35,14 +35,18 @@ function DiskUsage({ liveData }: NodeLiveData) { export default function NodesPage() { const { value: nodes, setValue: setNodes } = useAsyncInterval( + "liveNodes", [], getLiveNodes, - 3000 + 3000, ); - const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [ - nodes, - ]); + const { value: dockerNodes } = useAsync( + "docker", + [], + async () => getDocker(nodes), + [nodes], + ); return ( diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 82d4666..285cb43 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -17,12 +17,21 @@ import { getDeployments } from "@/lib/dockship/deploy"; import { useAsync, useAsyncInterval } from "@/hooks/use-async"; export default function OverviewPage() { - const { value: nodes } = useAsyncInterval([], getLiveNodes, 3000); - const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [ - nodes, - ]); - const { value: apps } = useAsyncInterval([], getApps, 3000); + const { value: nodes } = useAsyncInterval( + "liveNodes", + [], + getLiveNodes, + 3000, + ); + const { value: dockerNodes } = useAsync( + "docker", + [], + async () => getDocker(nodes), + [nodes], + ); + const { value: apps } = useAsyncInterval("apps", [], getApps, 3000); const { value: deployments } = useAsyncInterval( + "deployments", [], async () => (await getDeployments()).filter((d) => d !== null), 3000, diff --git a/src/components/dialogs/DeployApp.tsx b/src/components/dialogs/DeployApp.tsx index 56351ab..5c48506 100644 --- a/src/components/dialogs/DeployApp.tsx +++ b/src/components/dialogs/DeployApp.tsx @@ -28,8 +28,8 @@ export default function DeployAppDialog({ defaultApp?: string; children: ReactNode; }) { - const { value: nodes } = useAsync([], getLiveNodes); - const { value: apps } = useAsync([], getApps); + const { value: nodes } = useAsync("liveNodes", [], getLiveNodes); + const { value: apps } = useAsync("apps", [], getApps); const [name, setName] = useState(""); const [app, setApp] = useState(defaultApp || ""); const [node, setNode] = useState(""); diff --git a/src/components/dialogs/RegisterApp.tsx b/src/components/dialogs/RegisterApp.tsx index 955914e..2cecf83 100644 --- a/src/components/dialogs/RegisterApp.tsx +++ b/src/components/dialogs/RegisterApp.tsx @@ -13,10 +13,7 @@ import { } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { getLiveNodes } from "@/lib/dockship/node"; -import { Combobox } from "../ui/combobox"; import { registerApp } from "@/lib/dockship/application"; -import { useAsync } from "@/hooks/use-async"; export default function RegisterAppDialog({ children, diff --git a/src/hooks/use-async.ts b/src/hooks/use-async.ts index 260b37c..973d976 100644 --- a/src/hooks/use-async.ts +++ b/src/hooks/use-async.ts @@ -1,20 +1,31 @@ import { useState, useEffect } from "react"; export function useAsync( + cacheKey: string, defaultValue: T, asyncFunction: () => Promise, - dependencies: any[] = [] + dependencies: any[] = [], ) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [value, setValue] = useState(defaultValue); + const [value, setValue] = useState(() => { + try { + const cached = localStorage.getItem(cacheKey); + return cached ? (JSON.parse(cached) as T) : defaultValue; + } catch { + return defaultValue; + } + }); useEffect(() => { let isMounted = true; setLoading(true); + asyncFunction() .then((result) => { - if (isMounted) setValue(result); + if (!isMounted) return; + setValue(result); + localStorage.setItem(cacheKey, JSON.stringify(result)); }) .catch((err) => { if (isMounted) setError(err); @@ -32,35 +43,42 @@ export function useAsync( } export function useAsyncInterval( + cacheKey: string, defaultValue: T, asyncFunction: () => Promise, interval: number, - dependencies: any[] = [] + dependencies: any[] = [], ) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [value, setValue] = useState(defaultValue); + const [value, setValue] = useState(() => { + try { + const cached = localStorage.getItem(cacheKey); + return cached ? (JSON.parse(cached) as T) : defaultValue; + } catch { + return defaultValue; + } + }); useEffect(() => { let isMounted = true; - const run = () => { + const run = async () => { setLoading(true); - asyncFunction() - .then((result) => { - if (isMounted) setValue(result); - }) - .catch((err) => { - if (isMounted) setError(err); - }) - .finally(() => { - if (isMounted) setLoading(false); - }); + try { + const result = await asyncFunction(); + if (!isMounted) return; + setValue(result); + localStorage.setItem(cacheKey, JSON.stringify(result)); + } catch (err) { + if (isMounted) setError(err as Error); + } finally { + if (isMounted) setLoading(false); + } }; - const iid = setInterval(run, interval); - run(); + const iid = setInterval(run, interval); return () => { isMounted = false; From b9c3ab9f81b3c97d34c26c1006f3b6ce50c12f9f Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:29:22 +0100 Subject: [PATCH 11/15] Caching logs --- src/app/logs/page.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx index 69b9444..1d2ab12 100644 --- a/src/app/logs/page.tsx +++ b/src/app/logs/page.tsx @@ -15,16 +15,17 @@ import { AnsiText } from "@/components/ansi/Ansi"; export default function LogsPage() { const [isPaused, setIsPaused] = useState(false); const [filter, setFilter] = useState<"all" | "info" | "warn" | "error">( - "all" + "all", ); const { value: logs, setValue: setLogs } = useAsyncInterval( + "logs", [], async () => (isPaused ? ([] as Log[]) : getAllContainerLogs()), - 1000 + 1000, ); const filteredLogs = logs.filter((log) => - filter === "all" ? true : log.level === filter + filter === "all" ? true : log.level === filter, ); const handleExport = () => { @@ -171,7 +172,7 @@ export default function LogsPage() { [{log.level}] From 0239438b946eddcc952e325a22503998dcb43320 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:33:07 +0100 Subject: [PATCH 12/15] Logs auto scroll --- src/app/logs/page.tsx | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/logs/page.tsx b/src/app/logs/page.tsx index 1d2ab12..f2d0a36 100644 --- a/src/app/logs/page.tsx +++ b/src/app/logs/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; @@ -14,6 +14,10 @@ import { AnsiText } from "@/components/ansi/Ansi"; export default function LogsPage() { const [isPaused, setIsPaused] = useState(false); + const containerRef = useRef(null); + const bottomRef = useRef(null); + const shouldAutoScroll = useRef(true); + const [filter, setFilter] = useState<"all" | "info" | "warn" | "error">( "all", ); @@ -28,6 +32,25 @@ export default function LogsPage() { filter === "all" ? true : log.level === filter, ); + const handleScroll = () => { + const el = containerRef.current; + if (!el) return; + + const threshold = 20; // px from bottom + const isAtBottom = + el.scrollHeight - el.scrollTop - el.clientHeight < threshold; + + shouldAutoScroll.current = isAtBottom; + }; + + useEffect(() => { + if (!shouldAutoScroll.current || isPaused) return; + + bottomRef.current?.scrollIntoView({ + behavior: "smooth", + }); + }, [filteredLogs, isPaused]); + const handleExport = () => { // Convert data to JSON string const json = JSON.stringify(filteredLogs, null, 2); @@ -156,7 +179,11 @@ export default function LogsPage() {
- +
{filteredLogs.map((log, index) => (
*/} + + {/* Scroll anchor */} +
))}
From aabb55b43c9562115d9cc3f0f9af86720a2fbafa Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:44:42 +0100 Subject: [PATCH 13/15] Redeploy --- src/app/deployments/page.tsx | 25 ++++++++++++++++++++----- src/components/dialogs/DeployApp.tsx | 8 ++++++-- src/lib/dockship/deploy.ts | 6 ++++-- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 4bbb912..052974b 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -10,13 +10,14 @@ import { } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Plus, Play, Pause, Square } from "lucide-react"; +import { Plus, Play, Pause, Square, RotateCcw } from "lucide-react"; import { getDeployments, toggleDeployment } from "@/lib/dockship/deploy"; import { getApps } from "@/lib/dockship/application"; import { cn } from "@/lib/utils"; import DeployAppDialog from "@/components/dialogs/DeployApp"; import { useAsync, useAsyncInterval } from "@/hooks/use-async"; import RemoveDialog from "@/components/dialogs/Remove"; +import { Application } from "@/lib/types"; const statusDot = { running: "bg-chart-2", @@ -34,11 +35,11 @@ export default function DeploymentsPage() { [apps], ); - const appNameByContainer = useMemo(() => { - const map = new Map(); + const appByContainer = useMemo(() => { + const map = new Map(); for (const app of apps) { for (const container of app.deployments) { - map.set(container, app.name); + map.set(container, app); } } return map; @@ -90,7 +91,8 @@ export default function DeploymentsPage() { {dep.name} @{" "} - {appNameByContainer.get(dep.container) ?? "Unknown app"} + {appByContainer.get(dep.container)?.name ?? + "Unknown app"} @@ -123,6 +125,19 @@ export default function DeploymentsPage() { )} + ({ + containerPort: from.toString(), + hostPort: to.toString(), + protocol: "tcp", + }))} + nodeId={dep.nodeId} + > + + {}} />
diff --git a/src/components/dialogs/DeployApp.tsx b/src/components/dialogs/DeployApp.tsx index 5c48506..b91d80e 100644 --- a/src/components/dialogs/DeployApp.tsx +++ b/src/components/dialogs/DeployApp.tsx @@ -24,17 +24,21 @@ import { Plus } from "lucide-react"; export default function DeployAppDialog({ defaultApp, children, + ports: initialPorts, + nodeId: defaultNode, }: { defaultApp?: string; + nodeId?: string; + ports?: Port[]; children: ReactNode; }) { const { value: nodes } = useAsync("liveNodes", [], getLiveNodes); const { value: apps } = useAsync("apps", [], getApps); const [name, setName] = useState(""); const [app, setApp] = useState(defaultApp || ""); - const [node, setNode] = useState(""); + const [node, setNode] = useState(defaultNode || ""); - const [ports, setPorts] = useState([]); + const [ports, setPorts] = useState(initialPorts || []); const [newPort, setNewPort] = useState({ containerPort: "", hostPort: "", diff --git a/src/lib/dockship/deploy.ts b/src/lib/dockship/deploy.ts index 0c2fb87..0ba8a02 100644 --- a/src/lib/dockship/deploy.ts +++ b/src/lib/dockship/deploy.ts @@ -5,7 +5,9 @@ import { getNodes } from "./node"; import axios from "axios"; import { editApp, getApp, getApps } from "./application"; -export async function getDeployments(): Promise { +export async function getDeployments(): Promise< + (Deployment & { nodeId: string })[] +> { const nodes = await getNodes(); const apps = await getApps(); @@ -17,7 +19,7 @@ export async function getDeployments(): Promise { await axios.post(`http://${n.ip}:3000/api/get-deployments`, apps, { headers: { Authorization: `ApiKey ${n.key}` }, }) - ).data.deployments, + ).data.deployments.map((d: any) => ({ ...d, nodeId: n.node_id })), ), ) ).flat(); From d8d017c3e0e5bed1b92461fe395fd06b6405ebd9 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:50:06 +0100 Subject: [PATCH 14/15] Deploy gap --- src/app/deployments/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 052974b..2f6c116 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -74,7 +74,7 @@ export default function DeploymentsPage() {
-
+
{deployments.map((dep) => { return ( From d58769d0d72879a5252f7017ebb4a83da6881dc5 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Sat, 14 Feb 2026 14:53:32 +0100 Subject: [PATCH 15/15] Fix env problems --- src/core/deployment.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/deployment.ts b/src/core/deployment.ts index e5c529c..1d0b52b 100644 --- a/src/core/deployment.ts +++ b/src/core/deployment.ts @@ -148,9 +148,7 @@ export async function deployApp( RestartPolicy: { Name: "unless-stopped" }, PortBindings: formatPorts(ports), }, - Env: Object.entries(app.env).map( - ([key, { value }]) => `${key}="${value.replaceAll('"', '\\"')}"`, - ), + Env: Object.entries(app.env).map(([key, { value }]) => `${key}=${value}`), }); await container.start();