diff --git a/web/actions/backups.ts b/web/actions/backups.ts index 978cdf2..181cae9 100644 --- a/web/actions/backups.ts +++ b/web/actions/backups.ts @@ -1,10 +1,10 @@ "use server"; -import { desc, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { db } from "@/db"; import { getBackupStorageConfig } from "@/db/queries"; -import { servers, volumeBackups } from "@/db/schema"; +import { volumeBackups } from "@/db/schema"; import { requireAuth } from "@/lib/auth"; import { triggerBackup } from "@/lib/backups/trigger-backup"; import { inngest } from "@/lib/inngest/client"; @@ -31,27 +31,6 @@ export async function createBackup(serviceId: string, volumeId: string) { return { success: true, backupId: result.backupId }; } -export async function listBackups(serviceId: string) { - await requireAuth(); - const backups = await db - .select({ - id: volumeBackups.id, - volumeName: volumeBackups.volumeName, - status: volumeBackups.status, - sizeBytes: volumeBackups.sizeBytes, - createdAt: volumeBackups.createdAt, - completedAt: volumeBackups.completedAt, - errorMessage: volumeBackups.errorMessage, - serverName: servers.name, - }) - .from(volumeBackups) - .leftJoin(servers, eq(volumeBackups.serverId, servers.id)) - .where(eq(volumeBackups.serviceId, serviceId)) - .orderBy(desc(volumeBackups.createdAt)); - - return backups; -} - export async function restoreBackup( serviceId: string, backupId: string, diff --git a/web/actions/migrations.ts b/web/actions/migrations.ts index f7b6353..f4b9e0b 100644 --- a/web/actions/migrations.ts +++ b/web/actions/migrations.ts @@ -1,101 +1,20 @@ "use server"; -import { and, eq } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { db } from "@/db"; -import { getBackupStorageConfig } from "@/db/queries"; -import { deployments, services, serviceVolumes } from "@/db/schema"; +import { services } from "@/db/schema"; import { requireAuth } from "@/lib/auth"; import { inngest } from "@/lib/inngest/client"; import { inngestEvents } from "@/lib/inngest/events"; +import { startMigrationInternal } from "@/lib/migrations"; export async function startMigration( serviceId: string, targetServerId: string, ) { await requireAuth(); - const storageConfig = await getBackupStorageConfig(); - if (!storageConfig) { - throw new Error( - "Backup storage not configured. Configure it in Settings first.", - ); - } - - const service = await db - .select() - .from(services) - .where(eq(services.id, serviceId)) - .then((r) => r[0]); - - if (!service) { - throw new Error("Service not found"); - } - - if (!service.stateful) { - throw new Error("Only stateful services can be migrated"); - } - - if (service.migrationStatus) { - throw new Error("Migration already in progress"); - } - - const volumes = await db - .select() - .from(serviceVolumes) - .where(eq(serviceVolumes.serviceId, serviceId)); - - if (volumes.length === 0) { - throw new Error("No volumes found for this service"); - } - - const deployment = await db - .select({ - id: deployments.id, - serverId: deployments.serverId, - containerId: deployments.containerId, - }) - .from(deployments) - .where( - and( - eq(deployments.serviceId, serviceId), - eq(deployments.status, "running"), - ), - ) - .then((r) => r[0]); - - if (!deployment?.serverId) { - throw new Error("No running deployment found"); - } - - if (!deployment.containerId) { - throw new Error("Deployment is missing container ID"); - } - - if (deployment.serverId === targetServerId) { - throw new Error("Service is already running on the target server"); - } - - await db - .update(services) - .set({ - migrationStatus: "backing_up", - migrationTargetServerId: targetServerId, - migrationBackupId: null, - migrationError: null, - }) - .where(eq(services.id, serviceId)); - - await inngest.send( - inngestEvents.migrationStarted.create({ - serviceId, - targetServerId, - sourceServerId: deployment.serverId, - sourceDeploymentId: deployment.id, - sourceContainerId: deployment.containerId, - volumes: volumes.map((v) => ({ id: v.id, name: v.name })), - }), - ); - + await startMigrationInternal(serviceId, targetServerId); revalidatePath(`/dashboard/projects`); return { success: true }; } @@ -117,17 +36,3 @@ export async function cancelMigration(serviceId: string) { revalidatePath(`/dashboard/projects`); return { success: true }; } - -export async function getMigrationStatus(serviceId: string) { - await requireAuth(); - const service = await db - .select({ - migrationStatus: services.migrationStatus, - migrationError: services.migrationError, - }) - .from(services) - .where(eq(services.id, serviceId)) - .then((r) => r[0]); - - return service ?? null; -} diff --git a/web/actions/projects.ts b/web/actions/projects.ts index b07053e..057e878 100644 --- a/web/actions/projects.ts +++ b/web/actions/projects.ts @@ -30,6 +30,7 @@ import { } from "@/db/schema"; import { requireAuth } from "@/lib/auth"; import { DEFAULT_RESOURCE_LIMITS } from "@/lib/constants"; +import { deployServiceInternal } from "@/lib/deploy-service"; import { markDeploymentUndesired } from "@/lib/deployment-status"; import { inngest } from "@/lib/inngest/client"; import { inngestEvents } from "@/lib/inngest/events"; @@ -47,7 +48,6 @@ import type { import { getZodErrorMessage, slugify } from "@/lib/utils"; import { enqueueWork } from "@/lib/work-queue"; import { deleteBackup } from "./backups"; -import { startMigration } from "./migrations"; function isValidImageReferencePart(reference: string): boolean { const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/; @@ -860,62 +860,7 @@ export async function updateServiceGithubRepo( export async function deployService(serviceId: string) { await requireAuth(); - const service = await getService(serviceId); - if (!service) { - throw new Error("Service not found"); - } - - if (service.stateful) { - const configuredReplicas = await db - .select({ - serverId: serviceReplicas.serverId, - replicas: serviceReplicas.count, - }) - .from(serviceReplicas) - .where(eq(serviceReplicas.serviceId, serviceId)); - - const placements = configuredReplicas.filter((p) => p.replicas > 0); - const totalReplicas = placements.reduce((sum, p) => sum + p.replicas, 0); - - if (totalReplicas !== 1) { - throw new Error("Stateful services can only have exactly 1 replica"); - } - - const serverIds = placements.map((p) => p.serverId); - if (serverIds.length !== 1) { - throw new Error( - "Stateful services must be deployed to exactly one server", - ); - } - - const targetServerId = serverIds[0]; - if (service.lockedServerId && service.lockedServerId !== targetServerId) { - if (service.migrationStatus) { - throw new Error("Migration already in progress"); - } - await startMigration(serviceId, targetServerId); - revalidatePath(`/dashboard/projects`); - return { migrationStarted: true }; - } - } - - const rolloutId = randomUUID(); - - await db.insert(rollouts).values({ - id: rolloutId, - serviceId, - status: "in_progress", - currentStage: "queued", - }); - - await inngest.send( - inngestEvents.rolloutCreated.create({ - rolloutId, - serviceId, - }), - ); - - return { rolloutId }; + return deployServiceInternal(serviceId); } export async function deleteDeployments(serviceId: string) { diff --git a/web/actions/servers.ts b/web/actions/servers.ts index 5802add..cda9740 100644 --- a/web/actions/servers.ts +++ b/web/actions/servers.ts @@ -51,11 +51,6 @@ export async function deleteServer(id: string) { await db.delete(servers).where(eq(servers.id, id)); } -export async function approveServer(id: string) { - await requireAuth(); - await db.update(servers).set({ status: "pending" }).where(eq(servers.id, id)); -} - export async function updateServerName(id: string, name: string) { await requireAuth(); try { diff --git a/web/app/api/v1/agent/builds/[id]/status/route.ts b/web/app/api/v1/agent/builds/[id]/status/route.ts index bf35d19..da67cf2 100644 --- a/web/app/api/v1/agent/builds/[id]/status/route.ts +++ b/web/app/api/v1/agent/builds/[id]/status/route.ts @@ -1,6 +1,5 @@ import { and, eq, isNull } from "drizzle-orm"; import { type NextRequest, NextResponse } from "next/server"; -import { deployService } from "@/actions/projects"; import { db } from "@/db"; import { builds, @@ -10,6 +9,7 @@ import { services, } from "@/db/schema"; import { verifyAgentRequest } from "@/lib/agent-auth"; +import { deployServiceInternal } from "@/lib/deploy-service"; import { sendBuildFailureAlert } from "@/lib/email"; import { updateGitHubDeploymentStatus } from "@/lib/github"; import { inngest } from "@/lib/inngest/client"; @@ -351,7 +351,7 @@ export async function POST( ); try { - await deployService(build.serviceId); + await deployServiceInternal(build.serviceId); } catch (error) { console.error("[build:complete] deployment failed:", error); await db diff --git a/web/db/queries.ts b/web/db/queries.ts index ebe3546..ea2bae5 100644 --- a/web/db/queries.ts +++ b/web/db/queries.ts @@ -1,14 +1,10 @@ import { and, eq, isNotNull, isNull } from "drizzle-orm"; import { db } from "@/db"; import { - deploymentPorts, deployments, environments, projects, - secrets, servers, - servicePorts, - serviceReplicas, services, settings, } from "@/db/schema"; @@ -61,14 +57,6 @@ export async function getProjectBySlug(slug: string) { return results[0] || null; } -export async function listServices(projectId: string) { - return db - .select() - .from(services) - .where(and(eq(services.projectId, projectId), isNull(services.deletedAt))) - .orderBy(services.createdAt); -} - export async function getService(id: string) { const results = await db .select() @@ -77,14 +65,6 @@ export async function getService(id: string) { return results[0] || null; } -export async function getDeletedService(id: string) { - const results = await db - .select() - .from(services) - .where(and(eq(services.id, id), isNotNull(services.deletedAt))); - return results[0] || null; -} - export async function listDeletedServices( projectId: string, environmentId?: string, @@ -104,83 +84,10 @@ export async function listDeletedServices( .orderBy(services.deletedAt); } -export async function getOnlineServers() { - return db - .select({ - id: servers.id, - name: servers.name, - wireguardIp: servers.wireguardIp, - }) - .from(servers) - .where(eq(servers.status, "online")); -} - -export async function getServicePorts(serviceId: string) { - return db - .select() - .from(servicePorts) - .where(eq(servicePorts.serviceId, serviceId)) - .orderBy(servicePorts.port); -} - -export async function getDeploymentPorts(deploymentId: string) { - return db - .select({ - id: deploymentPorts.id, - hostPort: deploymentPorts.hostPort, - containerPort: servicePorts.port, - }) - .from(deploymentPorts) - .innerJoin(servicePorts, eq(deploymentPorts.servicePortId, servicePorts.id)) - .where(eq(deploymentPorts.deploymentId, deploymentId)); -} - -export async function listDeployments(serviceId: string) { - return db - .select() - .from(deployments) - .where(eq(deployments.serviceId, serviceId)) - .orderBy(deployments.createdAt); -} - -export async function getServiceReplicas(serviceId: string) { - const replicas = await db - .select({ - id: serviceReplicas.id, - serverId: serviceReplicas.serverId, - serverName: servers.name, - count: serviceReplicas.count, - }) - .from(serviceReplicas) - .innerJoin(servers, eq(serviceReplicas.serverId, servers.id)) - .where(eq(serviceReplicas.serviceId, serviceId)); - - return replicas; -} - -export async function listSecrets(serviceId: string) { - const secretsList = await db - .select({ - id: secrets.id, - key: secrets.key, - createdAt: secrets.createdAt, - }) - .from(secrets) - .where(eq(secrets.serviceId, serviceId)) - .orderBy(secrets.createdAt); - - return secretsList; -} - export async function listServers() { return db.select().from(servers).orderBy(servers.createdAt); } -export async function getServer(id: string) { - const results = await db.select().from(servers).where(eq(servers.id, id)); - return results[0] || null; -} - export async function getServerDetails(id: string) { const serverResults = await db .select({ diff --git a/web/lib/api-auth.ts b/web/lib/api-auth.ts index 8d7396f..954e9d8 100644 --- a/web/lib/api-auth.ts +++ b/web/lib/api-auth.ts @@ -22,7 +22,7 @@ function getAuthErrorResponse(error: unknown) { ); } -export async function getRequestSession(request: Request) { +async function getRequestSession(request: Request) { return auth.api.getSession({ headers: request.headers, }); diff --git a/web/lib/auth.ts b/web/lib/auth.ts index 99e700b..700965f 100644 --- a/web/lib/auth.ts +++ b/web/lib/auth.ts @@ -6,7 +6,7 @@ import { headers } from "next/headers"; import { db } from "@/db"; import * as schema from "@/db/schema"; -export const TECHULUS_CLI_CLIENT_ID = "techulus-cli"; +const TECHULUS_CLI_CLIENT_ID = "techulus-cli"; export const auth = betterAuth({ database: drizzleAdapter(db, { diff --git a/web/lib/cli-service.ts b/web/lib/cli-service.ts index 7b221a7..3d1222e 100644 --- a/web/lib/cli-service.ts +++ b/web/lib/cli-service.ts @@ -1,6 +1,5 @@ import { and, desc, eq, ne } from "drizzle-orm"; import { - deployService, updateServiceConfig, updateServiceResourceLimits, updateServiceStartCommand, @@ -25,6 +24,7 @@ import { techulusManifestSchema, } from "@/lib/cli-manifest"; import { DEFAULT_RESOURCE_LIMITS } from "@/lib/constants"; +import { deployServiceInternal } from "@/lib/deploy-service"; import { slugify } from "@/lib/utils"; export type ManifestChange = { @@ -714,7 +714,7 @@ export async function deployManifest(manifest: TechulusManifest) { manifest.service.replicas.count, ); - const result = await deployService(service.id); + const result = await deployServiceInternal(service.id); return { serviceId: service.id, diff --git a/web/lib/date.ts b/web/lib/date.ts index a7684c0..8132505 100644 --- a/web/lib/date.ts +++ b/web/lib/date.ts @@ -19,7 +19,7 @@ export function formatDateTime(date: string | Date): string { }); } -export function formatDate(date: string | Date): string { +function formatDate(date: string | Date): string { return new Date(date).toLocaleDateString(); } diff --git a/web/lib/deploy-service.ts b/web/lib/deploy-service.ts new file mode 100644 index 0000000..4e846dd --- /dev/null +++ b/web/lib/deploy-service.ts @@ -0,0 +1,68 @@ +import { randomUUID } from "node:crypto"; +import { eq } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import { db } from "@/db"; +import { getService } from "@/db/queries"; +import { rollouts, serviceReplicas } from "@/db/schema"; +import { inngest } from "@/lib/inngest/client"; +import { inngestEvents } from "@/lib/inngest/events"; +import { startMigrationInternal } from "@/lib/migrations"; + +export async function deployServiceInternal(serviceId: string) { + const service = await getService(serviceId); + if (!service) { + throw new Error("Service not found"); + } + + if (service.stateful) { + const configuredReplicas = await db + .select({ + serverId: serviceReplicas.serverId, + replicas: serviceReplicas.count, + }) + .from(serviceReplicas) + .where(eq(serviceReplicas.serviceId, serviceId)); + + const placements = configuredReplicas.filter((p) => p.replicas > 0); + const totalReplicas = placements.reduce((sum, p) => sum + p.replicas, 0); + + if (totalReplicas !== 1) { + throw new Error("Stateful services can only have exactly 1 replica"); + } + + const serverIds = placements.map((p) => p.serverId); + if (serverIds.length !== 1) { + throw new Error( + "Stateful services must be deployed to exactly one server", + ); + } + + const targetServerId = serverIds[0]; + if (service.lockedServerId && service.lockedServerId !== targetServerId) { + if (service.migrationStatus) { + throw new Error("Migration already in progress"); + } + await startMigrationInternal(serviceId, targetServerId); + revalidatePath(`/dashboard/projects`); + return { migrationStarted: true }; + } + } + + const rolloutId = randomUUID(); + + await db.insert(rollouts).values({ + id: rolloutId, + serviceId, + status: "in_progress", + currentStage: "queued", + }); + + await inngest.send( + inngestEvents.rolloutCreated.create({ + rolloutId, + serviceId, + }), + ); + + return { rolloutId }; +} diff --git a/web/lib/deployment-status.ts b/web/lib/deployment-status.ts index 4e95753..a1aa2cd 100644 --- a/web/lib/deployment-status.ts +++ b/web/lib/deployment-status.ts @@ -13,7 +13,7 @@ type DeploymentStatusCapabilities = { dns: boolean; }; -export const deploymentStatusCapabilities = { +const deploymentStatusCapabilities = { pending: { expected: true, routable: false, dns: false }, pulling: { expected: true, routable: false, dns: false }, starting: { expected: true, routable: false, dns: false }, diff --git a/web/lib/email/index.ts b/web/lib/email/index.ts index 1f29288..a918401 100644 --- a/web/lib/email/index.ts +++ b/web/lib/email/index.ts @@ -10,11 +10,11 @@ import { formatDateTime } from "@/lib/date"; import type { SmtpConfig } from "@/lib/settings-keys"; import { Alert } from "./templates/alert"; -export function getAppBaseUrl(): string | undefined { +function getAppBaseUrl(): string | undefined { return process.env.APP_URL; } -export function createTransporter(config: SmtpConfig): Transporter { +function createTransporter(config: SmtpConfig): Transporter { const secure = config.encryption === "tls"; const requireTLS = config.encryption === "starttls"; @@ -33,23 +33,13 @@ export function createTransporter(config: SmtpConfig): Transporter { }); } -export async function verifyConnection(config: SmtpConfig): Promise { - const transporter = createTransporter(config); - try { - await transporter.verify(); - return true; - } finally { - transporter.close(); - } -} - type SendEmailOptions = { to: string; subject: string; template: ReactElement; }; -export async function sendEmail( +async function sendEmail( config: SmtpConfig, options: SendEmailOptions, ): Promise { @@ -81,7 +71,7 @@ type AlertOptions = { template: ReactElement; }; -export async function sendAlert(options: AlertOptions): Promise { +async function sendAlert(options: AlertOptions): Promise { const config = getSmtpConfig(); if (!config?.enabled || !config.alertEmails) { diff --git a/web/lib/inngest/functions/build-workflow.ts b/web/lib/inngest/functions/build-workflow.ts index e9129d9..d9c1ec4 100644 --- a/web/lib/inngest/functions/build-workflow.ts +++ b/web/lib/inngest/functions/build-workflow.ts @@ -1,7 +1,7 @@ import { and, eq, isNull } from "drizzle-orm"; -import { deployService } from "@/actions/projects"; import { db } from "@/db"; import { builds, serviceReplicas, services } from "@/db/schema"; +import { deployServiceInternal } from "@/lib/deploy-service"; import { inngest } from "../client"; import { inngestEvents } from "../events"; @@ -69,7 +69,7 @@ export const buildWorkflow = inngest.createFunction( if (shouldDeploy) { await step.run("trigger-deploy", async () => { - await deployService(serviceId); + await deployServiceInternal(serviceId); }); } @@ -145,7 +145,7 @@ export const buildWorkflow = inngest.createFunction( if (shouldDeploy) { await step.run("trigger-deploy-group", async () => { - await deployService(serviceId); + await deployServiceInternal(serviceId); }); } diff --git a/web/lib/inngest/functions/migration-workflow.ts b/web/lib/inngest/functions/migration-workflow.ts index 0f2a8c4..bb66e71 100644 --- a/web/lib/inngest/functions/migration-workflow.ts +++ b/web/lib/inngest/functions/migration-workflow.ts @@ -1,6 +1,5 @@ import { randomUUID } from "node:crypto"; import { and, eq } from "drizzle-orm"; -import { deployService } from "@/actions/projects"; import { db } from "@/db"; import { getBackupStorageConfig } from "@/db/queries"; import { @@ -9,6 +8,7 @@ import { services, volumeBackups, } from "@/db/schema"; +import { deployServiceInternal } from "@/lib/deploy-service"; import { markDeploymentUndesired } from "@/lib/deployment-status"; import { enqueueWork } from "@/lib/work-queue"; import { inngest } from "../client"; @@ -327,7 +327,7 @@ export const migrationWorkflow = inngest.createFunction( .set({ lockedServerId: targetServerId }) .where(eq(services.id, serviceId)); - await deployService(serviceId); + await deployServiceInternal(serviceId); }); await step.run("complete-migration", async () => { diff --git a/web/lib/inngest/functions/service-deletion-workflow.ts b/web/lib/inngest/functions/service-deletion-workflow.ts index c3e40b2..d13911a 100644 --- a/web/lib/inngest/functions/service-deletion-workflow.ts +++ b/web/lib/inngest/functions/service-deletion-workflow.ts @@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto"; import { and, eq, inArray, isNotNull, isNull, lte, or } from "drizzle-orm"; import { cron } from "inngest"; import { deleteBackup } from "@/actions/backups"; -import { deployService } from "@/actions/projects"; import { db } from "@/db"; import { getBackupStorageConfig } from "@/db/queries"; import { @@ -13,6 +12,7 @@ import { serviceVolumes, volumeBackups, } from "@/db/schema"; +import { deployServiceInternal } from "@/lib/deploy-service"; import { markDeploymentUndesired } from "@/lib/deployment-status"; import { enqueueWork } from "@/lib/work-queue"; import { inngest } from "../client"; @@ -432,7 +432,7 @@ export const serviceRestoreWorkflow = inngest.createFunction( .where(eq(services.id, serviceId)); try { - const result = await deployService(serviceId); + const result = await deployServiceInternal(serviceId); if (!("rolloutId" in result) || !result.rolloutId) { throw new Error("Restore could not start a deployment"); } diff --git a/web/lib/migrations.ts b/web/lib/migrations.ts new file mode 100644 index 0000000..3d041c0 --- /dev/null +++ b/web/lib/migrations.ts @@ -0,0 +1,95 @@ +import { and, eq } from "drizzle-orm"; +import { db } from "@/db"; +import { getBackupStorageConfig } from "@/db/queries"; +import { deployments, services, serviceVolumes } from "@/db/schema"; +import { inngest } from "@/lib/inngest/client"; +import { inngestEvents } from "@/lib/inngest/events"; + +export async function startMigrationInternal( + serviceId: string, + targetServerId: string, +) { + const storageConfig = await getBackupStorageConfig(); + if (!storageConfig) { + throw new Error( + "Backup storage not configured. Configure it in Settings first.", + ); + } + + const service = await db + .select() + .from(services) + .where(eq(services.id, serviceId)) + .then((r) => r[0]); + + if (!service) { + throw new Error("Service not found"); + } + + if (!service.stateful) { + throw new Error("Only stateful services can be migrated"); + } + + if (service.migrationStatus) { + throw new Error("Migration already in progress"); + } + + const volumes = await db + .select() + .from(serviceVolumes) + .where(eq(serviceVolumes.serviceId, serviceId)); + + if (volumes.length === 0) { + throw new Error("No volumes found for this service"); + } + + const deployment = await db + .select({ + id: deployments.id, + serverId: deployments.serverId, + containerId: deployments.containerId, + }) + .from(deployments) + .where( + and( + eq(deployments.serviceId, serviceId), + eq(deployments.status, "running"), + ), + ) + .then((r) => r[0]); + + if (!deployment?.serverId) { + throw new Error("No running deployment found"); + } + + if (!deployment.containerId) { + throw new Error("Deployment is missing container ID"); + } + + if (deployment.serverId === targetServerId) { + throw new Error("Service is already running on the target server"); + } + + await db + .update(services) + .set({ + migrationStatus: "backing_up", + migrationTargetServerId: targetServerId, + migrationBackupId: null, + migrationError: null, + }) + .where(eq(services.id, serviceId)); + + await inngest.send( + inngestEvents.migrationStarted.create({ + serviceId, + targetServerId, + sourceServerId: deployment.serverId, + sourceDeploymentId: deployment.id, + sourceContainerId: deployment.containerId, + volumes: volumes.map((v) => ({ id: v.id, name: v.name })), + }), + ); + + return { success: true }; +} diff --git a/web/lib/s3.ts b/web/lib/s3.ts index 1c289c3..bf0ba75 100644 --- a/web/lib/s3.ts +++ b/web/lib/s3.ts @@ -14,7 +14,7 @@ function hashConfig(config: { return `${config.region}-${config.endpoint}-${config.accessKey}`; } -export async function getS3Client(): Promise { +async function getS3Client(): Promise { const config = await getBackupStorageConfig(); if (!config) { return null; diff --git a/web/lib/scheduler.ts b/web/lib/scheduler.ts index 89527b6..d157c19 100644 --- a/web/lib/scheduler.ts +++ b/web/lib/scheduler.ts @@ -1,7 +1,6 @@ import { CronExpressionParser } from "cron-parser"; import { and, eq, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm"; import { triggerBuild } from "@/actions/builds"; -import { deployService } from "@/actions/projects"; import { db } from "@/db"; import { deployments, @@ -10,6 +9,7 @@ import { services, workQueue, } from "@/db/schema"; +import { deployServiceInternal } from "@/lib/deploy-service"; import { sendManualRecoveryRequiredAlert, sendServerOfflineAlert, @@ -21,7 +21,7 @@ import { const STALE_THRESHOLD_MS = 120_000; // 2 minutes -export async function triggerRecoveryForOfflineServers( +async function triggerRecoveryForOfflineServers( offlineServerIds: string[], ): Promise { if (offlineServerIds.length === 0) return; @@ -216,7 +216,7 @@ export async function checkAndRunScheduledDeployments(): Promise { if (service.sourceType === "github") { await triggerBuild(service.id, "scheduled"); } else { - await deployService(service.id); + await deployServiceInternal(service.id); } console.log( diff --git a/web/lib/schemas.ts b/web/lib/schemas.ts index f82cbcd..c4db42e 100644 --- a/web/lib/schemas.ts +++ b/web/lib/schemas.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import cronstrue from "cronstrue"; export const nameSchema = z .string() @@ -8,18 +7,6 @@ export const nameSchema = z .transform((val) => val.trim()) .refine((val) => val.length > 0, "Name cannot be empty"); -export const slugSchema = z - .string() - .min(1, "Slug is required") - .transform((val) => val.toLowerCase().replace(/[^a-z0-9-]/g, "-")) - .refine((val) => val.length > 0, "Invalid slug"); - -export const replicaCountSchema = z - .number() - .int("Replicas must be a whole number") - .min(1, "Replicas must be at least 1") - .max(10, "Replicas cannot exceed 10"); - export const volumeNameSchema = z .string() .min(1, "Volume name is required") @@ -44,24 +31,7 @@ export const githubRepoUrlSchema = z "Repository URL must be a GitHub URL (https://github.com/...)", ); -export const cronScheduleSchema = z - .string() - .nullable() - .refine((val) => { - if (!val) return true; - try { - cronstrue.toString(val); - return true; - } catch { - return false; - } - }, "Invalid cron expression"); - -export const envVarKeySchema = z - .string() - .regex(/^[A-Z_][A-Z0-9_]*$/, "Invalid environment variable key"); - -export const secretItemSchema = z.object({ +const secretItemSchema = z.object({ key: z.string().min(1, "Key is required"), value: z.string().min(1, "Value is required"), }); diff --git a/web/lib/settings-keys.ts b/web/lib/settings-keys.ts index 6fda3c3..eccabcd 100644 --- a/web/lib/settings-keys.ts +++ b/web/lib/settings-keys.ts @@ -10,8 +10,6 @@ export const SETTING_KEYS = { export const DEFAULT_BUILD_TIMEOUT_MINUTES = 30; export const DEFAULT_BACKUP_RETENTION_DAYS = 7; -export const MIN_BACKUP_RETENTION_DAYS = 7; -export const MAX_BACKUP_RETENTION_DAYS = 30; export type BackupStorageProvider = "s3" | "r2" | "gcs" | "custom"; @@ -44,7 +42,7 @@ const commaSeparatedEmails = z { message: "Invalid email address in list" }, ); -export const smtpConfigSchema = z.object({ +const smtpConfigSchema = z.object({ enabled: z.boolean(), fromName: z.string().transform((val) => val.trim()), fromAddress: z diff --git a/web/package.json b/web/package.json index dbccf6e..638ba82 100644 --- a/web/package.json +++ b/web/package.json @@ -22,7 +22,6 @@ "better-auth": "^1.4.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cmdk": "^1.1.1", "cron-parser": "^5.4.0", "cronstrue": "^3.9.0", "drizzle-orm": "^0.45.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index bd82b55..1af7703 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -42,9 +42,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - cmdk: - specifier: ^1.1.1 - version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) cron-parser: specifier: ^5.4.0 version: 5.5.0 @@ -2059,177 +2056,6 @@ packages: '@protobufjs/utf8@1.1.1': resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@radix-ui/primitive@1.1.4': - resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} - - '@radix-ui/react-compose-refs@1.1.3': - resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context@1.1.4': - resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dialog@1.1.17': - resolution: {integrity: sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==} - peerDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.13': - resolution: {integrity: sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==} - peerDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.4': - resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.10': - resolution: {integrity: sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==} - peerDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.2': - resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-portal@1.1.12': - resolution: {integrity: sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==} - peerDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.6': - resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} - peerDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.6': - resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==} - peerDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.3.0': - resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.2': - resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.3': - resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.3': - resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-escape-keydown@1.1.2': - resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.2': - resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@react-email/body@0.3.0': resolution: {integrity: sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==} engines: {node: '>=20.0.0'} @@ -3049,10 +2875,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -3318,12 +3140,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cmdk@1.1.1: - resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - react-dom: ^18 || ^19 || ^19.0.0-rc - code-block-writer@13.0.3: resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} @@ -3514,9 +3330,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} @@ -4093,10 +3906,6 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - get-own-enumerable-keys@1.0.0: resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} engines: {node: '>=14.16'} @@ -5261,36 +5070,6 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.2: - resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - react-style-singleton@2.2.3: - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -5807,26 +5586,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-callback-ref@1.3.3: - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - use-sidecar@1.1.3: - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': 19.2.17 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -8080,148 +7839,6 @@ snapshots: '@protobufjs/utf8@1.1.1': {} - '@radix-ui/primitive@1.1.4': {} - - '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-dialog@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-dismissable-layer@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-focus-scope@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-portal@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - '@react-email/body@0.3.0(react@19.2.7)': dependencies: react: 19.2.7 @@ -8933,10 +8550,6 @@ snapshots: argparse@2.0.1: {} - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -9199,18 +8812,6 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - code-block-writer@13.0.3: {} color-convert@2.0.1: @@ -9365,8 +8966,6 @@ snapshots: detect-libc@2.1.2: {} - detect-node-es@1.1.0: {} - diff@8.0.4: {} doctrine@2.1.0: @@ -10141,8 +9740,6 @@ snapshots: hasown: 2.0.4 math-intrinsics: 1.1.0 - get-nonce@1.0.1: {} - get-own-enumerable-keys@1.0.0: {} get-proto@1.0.1: @@ -11198,33 +10795,6 @@ snapshots: react-is@16.13.1: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) - use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - get-nonce: 1.0.1 - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - react@19.2.7: {} recast@0.23.11: @@ -11904,21 +11474,6 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - detect-node-es: 1.1.0 - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - use-sync-external-store@1.6.0(react@19.2.7): dependencies: react: 19.2.7