Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
90a433e
Remove dead helper code
arjunkomath Jul 9, 2026
5afc15a
Inline low-use display helpers
arjunkomath Jul 9, 2026
c4cebe6
Inline one-off UI display helpers
arjunkomath Jul 9, 2026
a622398
Simplify checked-at label formatting
arjunkomath Jul 9, 2026
28e7b34
Use const checked-at label formatting
arjunkomath Jul 9, 2026
a5bac25
Merge pull request #121 from techulus/cleanup-dead-helper-code
arjunkomath Jul 9, 2026
3295595
Standardize datetime handling
techulus-agent Jul 10, 2026
dd3a648
Implement server-side log search
techulus-agent Jul 10, 2026
80d396b
Update Postgres images to 18
techulus-agent Jul 10, 2026
770fd6a
Merge pull request #126 from techulus/update-postgres-18
arjunkomath Jul 10, 2026
eb0b64d
Merge pull request #124 from techulus/codex/standardize-datetime
arjunkomath Jul 10, 2026
0832b23
Harden log query handling
techulus-agent Jul 10, 2026
cae8574
Fix log time range menu context
techulus-agent Jul 10, 2026
762307b
Dismiss log range menu after selection
techulus-agent Jul 10, 2026
42572b4
Fix deployment log cursor semantics
techulus-agent Jul 10, 2026
572e1cb
Deduplicate log query handling
techulus-agent Jul 10, 2026
6fba8f5
Widen log time range menu
techulus-agent Jul 10, 2026
7cbb2a6
Merge pull request #125 from techulus/codex/logs-server-search
arjunkomath Jul 10, 2026
3f1a13d
Require 2FA for project and server deletion
techulus-agent Jul 9, 2026
caac6b9
Fix stale 2FA session recovery
techulus-agent Jul 10, 2026
6779951
Merge pull request #123 from techulus/feat/2fa-project-server-delete
arjunkomath Jul 10, 2026
a6e8e62
Fix service metrics mobile layout
arjunkomath Jul 10, 2026
d4c7aba
Merge pull request #127 from techulus/feat/service-metrics-mobile-layout
arjunkomath Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
services:
postgres:
image: postgres:16-alpine
image: postgres:18-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: cloud
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
Expand Down
4 changes: 2 additions & 2 deletions deployment/compose.postgres.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ services:
restart: unless-stopped

postgres:
image: postgres:16
image: postgres:18
env_file:
- ./.env
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
volumes:
- postgres-data:/var/lib/postgresql/data
- postgres-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
interval: 30s
Expand Down
2 changes: 2 additions & 0 deletions docs/infrastructure/logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,5 @@ The control plane exposes logs at `https://logs.<ROOT_DOMAIN>` with basic auth.
## Accessing Logs

Logs are accessible from the web UI for each service, deployment, build, and server. The control plane queries Victoria Logs using LogSQL with filters for `service_id`, `deployment_id`, `server_id`, and time ranges.

Searches run against Victoria Logs rather than only the entries currently loaded in the browser. Continuous service, request, and server log views default to the last 24 hours and support 1-hour, 6-hour, 24-hour, and 7-day ranges. These query ranges do not change the separate `VL_RETENTION` storage setting.
9 changes: 3 additions & 6 deletions web/actions/members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ import { db } from "@/db";
import { account, memberInvitations, user } from "@/db/schema";
import type { InvitableMemberRole } from "@/db/types";
import { auth, requireAdminRole } from "@/lib/auth";
import { addMilliseconds, DAY_IN_MILLISECONDS, isExpired } from "@/lib/date";
import { sendMemberInviteEmail } from "@/lib/email";
import {
createInviteToken,
hashInviteToken,
isInvitableMemberRole,
} from "@/lib/members";

const INVITE_EXPIRY_MS = 1000 * 60 * 60 * 24 * 7;
const INVITE_EXPIRY_MS = 7 * DAY_IN_MILLISECONDS;

const inviteMemberSchema = z.object({
email: z.string().trim().email(),
Expand Down Expand Up @@ -50,10 +51,6 @@ const createAuthUser = auth.api.createUser as (
data: CreateUserInput,
) => Promise<CreateUserResult>;

function isExpired(expiresAt: Date) {
return expiresAt.getTime() <= Date.now();
}

async function requireAdminSession() {
const session = await requireAdminRole();
if (!session) {
Expand Down Expand Up @@ -191,7 +188,7 @@ export async function inviteMember(input: {
}

const inviteUrl = `${baseUrl}/invite/${encodeURIComponent(token)}`;
const expiresAt = new Date(Date.now() + INVITE_EXPIRY_MS);
const expiresAt = addMilliseconds(new Date(), INVITE_EXPIRY_MS);

await db.insert(memberInvitations).values({
id: randomUUID(),
Expand Down
11 changes: 0 additions & 11 deletions web/actions/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,6 @@ import { services } from "@/db/schema";
import { requireDeveloperRole } 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 requireDeveloperRole();
await startMigrationInternal(serviceId, targetServerId);
revalidatePath(`/dashboard/projects`);
return { success: true };
}

export async function cancelMigration(serviceId: string) {
await requireDeveloperRole();
Expand Down
47 changes: 10 additions & 37 deletions web/actions/projects.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"use server";

import { randomUUID } from "node:crypto";
import { isAPIError } from "better-auth/api";
import cronstrue from "cronstrue";
import { and, desc, eq, inArray, isNotNull, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import { ZodError, z } from "zod";
import { db } from "@/db";
import {
Expand All @@ -30,7 +28,7 @@ import {
volumeBackups,
workQueue,
} from "@/db/schema";
import { auth, requireDeveloperRole } from "@/lib/auth";
import { requireDeveloperRole, verifyDeleteConfirmation } from "@/lib/auth";
import { DEFAULT_RESOURCE_LIMITS } from "@/lib/constants";
import { deployServiceInternal } from "@/lib/deploy-service";
import {
Expand All @@ -54,14 +52,11 @@ import type {
HealthCheckConfig as ServiceHealthCheckConfig,
} from "@/lib/service-config";
import { MIN_SERVERLESS_SLEEP_AFTER_SECONDS } from "@/lib/service-config";
import type { DeleteConfirmation } from "@/lib/two-factor";
import { getZodErrorMessage, slugify } from "@/lib/utils";
import { enqueueWork } from "@/lib/work-queue";
import { deleteBackup } from "./backups";

type ServiceDeleteConfirmation = {
totpCode?: string;
};

function isValidImageReferencePart(reference: string): boolean {
const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
const digestPattern = /^[A-Za-z0-9_+.-]+:[0-9a-fA-F]{32,256}$/;
Expand Down Expand Up @@ -268,8 +263,12 @@ export async function createProject(name: string) {
}
}

export async function deleteProject(id: string) {
await requireDeveloperRole();
export async function deleteProject(
id: string,
confirmation?: DeleteConfirmation,
) {
const session = await requireDeveloperRole();
await verifyDeleteConfirmation(session, confirmation, "project");
const projectServices = await db
.select()
.from(services)
Expand Down Expand Up @@ -582,36 +581,10 @@ async function hardDeleteService(serviceId: string) {

export async function deleteService(
serviceId: string,
confirmation?: ServiceDeleteConfirmation,
confirmation?: DeleteConfirmation,
) {
const session = await requireDeveloperRole();
if (!session) {
throw new Error("Unauthorized");
}

const twoFactorEnabled = Boolean(
(session.user as { twoFactorEnabled?: boolean | null }).twoFactorEnabled,
);

if (twoFactorEnabled) {
const totpCode = confirmation?.totpCode ?? "";

if (!/^\d{6}$/.test(totpCode)) {
throw new Error("Authenticator code is required to delete this service");
}

try {
await auth.api.verifyTOTP({
body: { code: totpCode },
headers: await headers(),
});
} catch (error) {
if (isAPIError(error)) {
throw new Error("Invalid authenticator code");
}
throw error;
}
}
await verifyDeleteConfirmation(session, confirmation, "service");

const service = await getService(serviceId);
if (!service) {
Expand Down
11 changes: 8 additions & 3 deletions web/actions/servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { ZodError } from "zod";
import { db } from "@/db";
import { servers } from "@/db/schema";
import { enqueueAgentUpgrade } from "@/lib/agent-upgrades";
import { requireDeveloperRole } from "@/lib/auth";
import { requireDeveloperRole, verifyDeleteConfirmation } from "@/lib/auth";
import { nameSchema } from "@/lib/schemas";
import type { DeleteConfirmation } from "@/lib/two-factor";
import { getZodErrorMessage } from "@/lib/utils";

function generateId(): string {
Expand Down Expand Up @@ -48,8 +49,12 @@ export async function createServer(name: string) {
}
}

export async function deleteServer(id: string) {
await requireDeveloperRole();
export async function deleteServer(
id: string,
confirmation?: DeleteConfirmation,
) {
const session = await requireDeveloperRole();
await verifyDeleteConfirmation(session, confirmation, "server");
await db.delete(servers).where(eq(servers.id, id));
}

Expand Down
Loading
Loading