Skip to content

Commit 839a9be

Browse files
committed
Update Wireguard setup, clean up UI
1 parent 89ef1b3 commit 839a9be

10 files changed

Lines changed: 220 additions & 103 deletions

File tree

‎web/actions/projects.ts‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,41 @@ export async function createProject(name: string) {
4747
}
4848

4949
export async function listProjects() {
50-
return db.select().from(projects).orderBy(projects.createdAt);
50+
const projectList = await db.select().from(projects).orderBy(projects.createdAt);
51+
52+
const projectsWithCounts = await Promise.all(
53+
projectList.map(async (project) => {
54+
const serviceCount = await db
55+
.select({ count: services.id })
56+
.from(services)
57+
.where(eq(services.projectId, project.id));
58+
return {
59+
...project,
60+
serviceCount: serviceCount.length,
61+
};
62+
})
63+
);
64+
65+
return projectsWithCounts;
5166
}
5267

5368
export async function getProject(id: string) {
5469
const results = await db.select().from(projects).where(eq(projects.id, id));
5570
return results[0] || null;
5671
}
5772

73+
export async function getProjectBySlug(slug: string) {
74+
const results = await db.select().from(projects).where(eq(projects.slug, slug));
75+
return results[0] || null;
76+
}
77+
78+
async function revalidateProject(projectId: string) {
79+
const project = await getProject(projectId);
80+
if (project) {
81+
revalidatePath(`/dashboard/projects/${project.slug}`);
82+
}
83+
}
84+
5885
export async function deleteProject(id: string) {
5986
await db.delete(projects).where(eq(projects.id, id));
6087
revalidatePath("/dashboard");
@@ -84,7 +111,7 @@ export async function createService(
84111
});
85112
}
86113

87-
revalidatePath(`/dashboard/projects/${projectId}`);
114+
await revalidateProject(projectId);
88115
return { id, name, image, ports };
89116
}
90117

@@ -143,7 +170,7 @@ export async function deleteService(serviceId: string) {
143170
await db.delete(secrets).where(eq(secrets.serviceId, serviceId));
144171
await db.delete(services).where(eq(services.id, serviceId));
145172

146-
revalidatePath(`/dashboard/projects/${service.projectId}`);
173+
await revalidateProject(service.projectId);
147174
return { success: true };
148175
}
149176

@@ -238,7 +265,7 @@ export async function updateServicePorts(serviceId: string, changes: PortChange[
238265
await deployService(serviceId, placements);
239266
}
240267

241-
revalidatePath(`/dashboard/projects/${service.projectId}`);
268+
await revalidateProject(service.projectId);
242269
return { success: true, redeployed: runningDeployments.length > 0 };
243270
}
244271

@@ -420,7 +447,7 @@ export async function deployService(serviceId: string, placements: ServerPlaceme
420447
}
421448
}
422449

423-
revalidatePath(`/dashboard/projects/${service.projectId}`);
450+
await revalidateProject(service.projectId);
424451
return { deploymentIds, replicaCount: totalReplicas };
425452
}
426453

@@ -452,7 +479,7 @@ export async function deleteDeployment(deploymentId: string) {
452479

453480
const service = await getService(dep.serviceId);
454481
if (service) {
455-
revalidatePath(`/dashboard/projects/${service.projectId}`);
482+
await revalidateProject(service.projectId);
456483
}
457484

458485
return { success: true };
@@ -491,7 +518,7 @@ export async function stopDeployment(deploymentId: string) {
491518

492519
const service = await getService(dep.serviceId);
493520
if (service) {
494-
revalidatePath(`/dashboard/projects/${service.projectId}`);
521+
await revalidateProject(service.projectId);
495522
}
496523

497524
return { success: true };

‎web/app/(dashboard)/dashboard/page.tsx‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,20 @@ export default async function DashboardPage() {
3939
) : (
4040
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
4141
{projects.map((project) => (
42-
<Link key={project.id} href={`/dashboard/projects/${project.id}`}>
42+
<Link
43+
key={project.id}
44+
href={`/dashboard/projects/${project.slug}`}
45+
>
4346
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
44-
<CardHeader>
47+
<CardHeader className="min-h-24">
4548
<CardTitle>{project.name}</CardTitle>
46-
<CardDescription>{project.slug}</CardDescription>
49+
<CardDescription>
50+
{project.serviceCount === 0
51+
? "No services"
52+
: project.serviceCount === 1
53+
? "1 service"
54+
: `${project.serviceCount} services`}
55+
</CardDescription>
4756
</CardHeader>
4857
</Card>
4958
</Link>

web/app/(dashboard)/dashboard/projects/[id]/page.tsx renamed to web/app/(dashboard)/dashboard/projects/[slug]/page.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { notFound } from "next/navigation";
2-
import { getProject, listServices, listDeployments, getServicePorts, getDeploymentPorts } from "@/actions/projects";
2+
import { getProjectBySlug, listServices, listDeployments, getServicePorts, getDeploymentPorts } from "@/actions/projects";
33
import { ServiceList } from "@/components/service-list";
44
import { PageHeader } from "@/components/page-header";
55
import { CreateServiceDialog } from "@/components/create-service-dialog";
@@ -10,16 +10,16 @@ import { eq } from "drizzle-orm";
1010
export default async function ProjectPage({
1111
params,
1212
}: {
13-
params: Promise<{ id: string }>;
13+
params: Promise<{ slug: string }>;
1414
}) {
15-
const { id } = await params;
16-
const project = await getProject(id);
15+
const { slug } = await params;
16+
const project = await getProjectBySlug(slug);
1717

1818
if (!project) {
1919
notFound();
2020
}
2121

22-
const servicesList = await listServices(id);
22+
const servicesList = await listServices(project.id);
2323

2424
const initialServices = await Promise.all(
2525
servicesList.map(async (service) => {
@@ -60,10 +60,10 @@ export default async function ProjectPage({
6060
<PageHeader
6161
title={project.name}
6262
backHref="/dashboard"
63-
actions={<CreateServiceDialog projectId={id} />}
63+
actions={<CreateServiceDialog projectId={project.id} />}
6464
/>
6565

66-
<ServiceList projectId={id} initialServices={initialServices} />
66+
<ServiceList projectId={project.id} initialServices={initialServices} />
6767
</div>
6868
);
6969
}

‎web/app/api/v1/agent/register/route.ts‎

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { NextRequest, NextResponse } from "next/server";
22
import { db } from "@/db";
33
import { servers, workQueue } from "@/db/schema";
4-
import { eq, and, isNull, gt, ne } from "drizzle-orm";
5-
import { assignWireGuardIp, getWireGuardPeers } from "@/lib/wireguard";
4+
import { eq, and, isNull, gt } from "drizzle-orm";
5+
import { assignWireGuardIp, getWireGuardPeers, isProxyServer } from "@/lib/wireguard";
6+
import { PROXY_WIREGUARD_IP } from "@/lib/constants";
67
import { randomUUID } from "crypto";
78

89
const TOKEN_EXPIRY_HOURS = 24;
@@ -59,26 +60,32 @@ export async function POST(request: NextRequest) {
5960
})
6061
.where(eq(servers.id, server.id));
6162

62-
const peers = await getWireGuardPeers(server.id);
63+
const peers = await getWireGuardPeers(server.id, wireguardIp);
6364

64-
const otherServers = await db
65-
.select({ id: servers.id })
66-
.from(servers)
67-
.where(
68-
and(
69-
ne(servers.id, server.id),
70-
eq(servers.status, "online")
65+
if (!isProxyServer(wireguardIp)) {
66+
const proxyServer = await db
67+
.select({ id: servers.id, wireguardIp: servers.wireguardIp })
68+
.from(servers)
69+
.where(
70+
and(
71+
eq(servers.status, "online"),
72+
eq(servers.wireguardIp, PROXY_WIREGUARD_IP)
73+
)
7174
)
72-
);
75+
.then((r) => r[0]);
7376

74-
for (const otherServer of otherServers) {
75-
const otherPeers = await getWireGuardPeers(otherServer.id);
76-
await db.insert(workQueue).values({
77-
id: randomUUID(),
78-
serverId: otherServer.id,
79-
type: "update_wireguard",
80-
payload: JSON.stringify({ peers: otherPeers }),
81-
});
77+
if (proxyServer) {
78+
const proxyPeers = await getWireGuardPeers(
79+
proxyServer.id,
80+
proxyServer.wireguardIp!
81+
);
82+
await db.insert(workQueue).values({
83+
id: randomUUID(),
84+
serverId: proxyServer.id,
85+
type: "update_wireguard",
86+
payload: JSON.stringify({ peers: proxyPeers }),
87+
});
88+
}
8289
}
8390

8491
return NextResponse.json({

‎web/app/api/v1/agent/status/route.ts‎

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { db } from "@/db";
33
import { servers, workQueue, deployments, serverContainers, proxyRoutes, servicePorts } from "@/db/schema";
44
import { eq, and, lt, ne, notInArray } from "drizzle-orm";
55
import { verifyEd25519Signature } from "@/lib/crypto";
6-
import { getWireGuardPeers } from "@/lib/wireguard";
6+
import { getWireGuardPeers, isProxyServer } from "@/lib/wireguard";
7+
import { PROXY_WIREGUARD_IP } from "@/lib/constants";
78
import { randomUUID } from "crypto";
89

910
type ContainerInfo = {
@@ -104,20 +105,49 @@ export async function POST(request: NextRequest) {
104105

105106
await db.update(servers).set(updateData).where(eq(servers.id, serverId));
106107

107-
if (publicIpChanged) {
108-
const otherServers = await db
109-
.select({ id: servers.id })
110-
.from(servers)
111-
.where(and(ne(servers.id, serverId), eq(servers.status, "online")));
112-
113-
for (const otherServer of otherServers) {
114-
const peers = await getWireGuardPeers(otherServer.id);
115-
await db.insert(workQueue).values({
116-
id: randomUUID(),
117-
serverId: otherServer.id,
118-
type: "update_wireguard",
119-
payload: JSON.stringify({ peers }),
120-
});
108+
if (publicIpChanged && server.wireguardIp) {
109+
if (isProxyServer(server.wireguardIp)) {
110+
const workers = await db
111+
.select({ id: servers.id, wireguardIp: servers.wireguardIp })
112+
.from(servers)
113+
.where(
114+
and(
115+
ne(servers.id, serverId),
116+
eq(servers.status, "online"),
117+
ne(servers.wireguardIp, PROXY_WIREGUARD_IP)
118+
)
119+
);
120+
121+
for (const worker of workers) {
122+
const peers = await getWireGuardPeers(worker.id, worker.wireguardIp!);
123+
await db.insert(workQueue).values({
124+
id: randomUUID(),
125+
serverId: worker.id,
126+
type: "update_wireguard",
127+
payload: JSON.stringify({ peers }),
128+
});
129+
}
130+
} else {
131+
const proxyServer = await db
132+
.select({ id: servers.id, wireguardIp: servers.wireguardIp })
133+
.from(servers)
134+
.where(
135+
and(
136+
eq(servers.status, "online"),
137+
eq(servers.wireguardIp, PROXY_WIREGUARD_IP)
138+
)
139+
)
140+
.then((r) => r[0]);
141+
142+
if (proxyServer) {
143+
const peers = await getWireGuardPeers(proxyServer.id, proxyServer.wireguardIp!);
144+
await db.insert(workQueue).values({
145+
id: randomUUID(),
146+
serverId: proxyServer.id,
147+
type: "update_wireguard",
148+
payload: JSON.stringify({ peers }),
149+
});
150+
}
121151
}
122152
}
123153

‎web/app/globals.css‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
@theme inline {
88
--color-background: var(--background);
99
--color-foreground: var(--foreground);
10-
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
11-
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
12-
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
13-
monospace;
10+
--font-sans: var(--font-geist), ui-sans-serif, system-ui, -apple-system,
11+
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
12+
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, "SF Mono",
13+
Menlo, Consolas, monospace;
1414
--color-sidebar-ring: var(--sidebar-ring);
1515
--color-sidebar-border: var(--sidebar-border);
1616
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);

‎web/app/layout.tsx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import type { Metadata } from "next";
2+
import { Geist, Geist_Mono } from "next/font/google";
23
import { ThemeProvider } from "@/components/theme-provider";
34
import "./globals.css";
45

6+
const geist = Geist({
7+
subsets: ["latin"],
8+
variable: "--font-geist",
9+
});
10+
11+
const geistMono = Geist_Mono({
12+
subsets: ["latin"],
13+
variable: "--font-geist-mono",
14+
});
15+
516
export const metadata: Metadata = {
617
title: "Techulus Cloud",
718
description: "Stateless container deployment platform",
@@ -14,7 +25,7 @@ export default function RootLayout({
1425
}>) {
1526
return (
1627
<html lang="en" suppressHydrationWarning>
17-
<body className="font-sans antialiased">
28+
<body className={`${geist.variable} ${geistMono.variable} font-sans antialiased`}>
1829
<ThemeProvider>{children}</ThemeProvider>
1930
</body>
2031
</html>

‎web/components/create-project-dialog.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function CreateProjectDialog() {
2929
const project = await createProject(name.trim());
3030
setIsOpen(false);
3131
setName("");
32-
router.push(`/dashboard/projects/${project.id}`);
32+
router.push(`/dashboard/projects/${project.slug}`);
3333
} catch (error) {
3434
console.error("Failed to create project:", error);
3535
} finally {

0 commit comments

Comments
 (0)