Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCallback, useState } from "react";
import { toast } from "sonner";
import { useSWRConfig } from "swr";
import { deleteService } from "@/actions/projects";
import { LocalDate } from "@/components/core/local-date";
import { HealthCheckSection } from "@/components/service/details/health-check-section";
import { PortsSection } from "@/components/service/details/ports-section";
import { ReplicasSection } from "@/components/service/details/replicas-section";
Expand Down Expand Up @@ -47,11 +48,6 @@ type TwoFactorSessionUser = {
twoFactorEnabled?: boolean | null;
};

function formatBackupDate(value: Date | string | null | undefined) {
if (!value) return "an unknown time";
return new Date(value).toLocaleString();
}

export default function ConfigurationPage() {
const router = useRouter();
const { mutate: globalMutate } = useSWRConfig();
Expand Down Expand Up @@ -193,10 +189,13 @@ export default function ConfigurationPage() {
</span>{" "}
Restore will use the latest completed backups for
its volumes. The oldest selected backup is from{" "}
{formatBackupDate(
service.deletionBackupFallback
?.oldestLatestBackupAt,
)}
<LocalDate
value={
service.deletionBackupFallback
?.oldestLatestBackupAt
}
fallback="an unknown time"
/>
; changes after that backup will not be restored.
</>
)}
Expand Down
3 changes: 2 additions & 1 deletion web/app/api/cluster-metrics/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { headers } from "next/headers";
import { listServers } from "@/db/queries";
import { auth } from "@/lib/auth";
import { subtractMilliseconds } from "@/lib/date";
import {
isMetricsEnabled,
METRIC_RANGE_OPTIONS,
Expand Down Expand Up @@ -33,7 +34,7 @@ export async function GET(request: Request) {

const end = new Date();
const option = METRIC_RANGE_OPTIONS[range];
const start = new Date(end.getTime() - option.durationMs);
const start = subtractMilliseconds(end, option.durationMs);
const servers = await listServers();
const selectedServers =
serverId && serverId !== "all"
Expand Down
13 changes: 7 additions & 6 deletions web/app/api/projects/[id]/services/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import {
volumeBackups,
} from "@/db/schema";
import { auth } from "@/lib/auth";

function getBackupTime(value: Date | string | null) {
return value ? new Date(value).getTime() : 0;
}
import { getTimestamp } from "@/lib/date";

export async function GET(
request: Request,
Expand Down Expand Up @@ -181,13 +178,17 @@ export async function GET(
oldestLatestBackupAt:
latestBackupTimes.length > 0
? latestBackupTimes.reduce((oldest, value) =>
getBackupTime(value) < getBackupTime(oldest) ? value : oldest,
getTimestamp(value, 0) < getTimestamp(oldest, 0)
? value
: oldest,
)
: null,
newestLatestBackupAt:
latestBackupTimes.length > 0
? latestBackupTimes.reduce((newest, value) =>
getBackupTime(value) > getBackupTime(newest) ? value : newest,
getTimestamp(value, 0) > getTimestamp(newest, 0)
? value
: newest,
)
: null,
};
Expand Down
3 changes: 2 additions & 1 deletion web/app/api/servers/[id]/metrics/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import { subtractMilliseconds } from "@/lib/date";
import {
emptyHistory,
isMetricsEnabled,
Expand Down Expand Up @@ -38,7 +39,7 @@ export async function GET(

const end = new Date();
const option = METRIC_RANGE_OPTIONS[range];
const start = new Date(end.getTime() - option.durationMs);
const start = subtractMilliseconds(end, option.durationMs);

try {
const [current, history] = await Promise.all([
Expand Down
6 changes: 4 additions & 2 deletions web/app/api/v1/agent/register/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { and, eq, gt, isNull } from "drizzle-orm";
import { type NextRequest, NextResponse } from "next/server";
import { db } from "@/db";
import { servers } from "@/db/schema";
import { HOUR_IN_MILLISECONDS, subtractMilliseconds } from "@/lib/date";
import { agentRegisterSchema } from "@/lib/schemas";
import { formatZodErrors } from "@/lib/utils";
import { assignSubnet } from "@/lib/wireguard";
Expand Down Expand Up @@ -30,8 +31,9 @@ export async function POST(request: NextRequest) {
} = parseResult.data;

const now = new Date();
const expiryThreshold = new Date(
now.getTime() - TOKEN_EXPIRY_HOURS * 60 * 60 * 1000,
const expiryThreshold = subtractMilliseconds(
now,
TOKEN_EXPIRY_HOURS * HOUR_IN_MILLISECONDS,
);

const serverResults = await db
Expand Down
22 changes: 5 additions & 17 deletions web/components/builds/build-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
ItemTitle,
} from "@/components/ui/item";
import type { Build, BuildStatus, GithubRepo, Service } from "@/db/types";
import { formatRelativeTime } from "@/lib/date";
import { formatElapsedDurationBetween, formatRelativeTime } from "@/lib/date";
import { fetcher } from "@/lib/fetcher";

type BuildWithDates = Omit<
Expand Down Expand Up @@ -100,21 +100,6 @@ const STATUS_CONFIG: Record<
},
};

function formatDuration(
start: string | Date,
end: string | Date | null,
): string {
const startDate = new Date(start);
const endDate = end ? new Date(end) : new Date();
const diff = endDate.getTime() - startDate.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
}

export function BuildDetails({
projectSlug,
envName,
Expand Down Expand Up @@ -296,7 +281,10 @@ export function BuildDetails({
<div className="flex justify-between">
<span>Duration</span>
<span>
{formatDuration(build.startedAt, build.completedAt)}
{formatElapsedDurationBetween(
build.startedAt,
build.completedAt,
)}
</span>
</div>
)}
Expand Down
19 changes: 5 additions & 14 deletions web/components/builds/builds-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
} from "@/components/ui/item";
import { Spinner } from "@/components/ui/spinner";
import type { Build, BuildStatus } from "@/db/types";
import { formatRelativeTime } from "@/lib/date";
import { formatElapsedDurationBetween, formatRelativeTime } from "@/lib/date";
import { fetcher } from "@/lib/fetcher";

type BuildListItem = Pick<
Expand Down Expand Up @@ -118,18 +118,6 @@ function StatusBadge({ status }: { status: BuildStatus }) {
);
}

function formatDuration(start: string, end: string | null): string {
const startDate = new Date(start);
const endDate = end ? new Date(end) : new Date();
const diff = endDate.getTime() - startDate.getTime();
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
}

export function BuildsViewer({
serviceId,
projectSlug,
Expand Down Expand Up @@ -280,7 +268,10 @@ export function BuildsViewer({
{build.startedAt && (
<span className="ml-3">
Duration:{" "}
{formatDuration(build.startedAt, build.completedAt)}
{formatElapsedDurationBetween(
build.startedAt,
build.completedAt,
)}
</span>
)}
</ItemDescription>
Expand Down
74 changes: 74 additions & 0 deletions web/components/core/local-date.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client";

import { useSyncExternalStore } from "react";
import {
type DateFormatOptions,
type DateInput,
formatCompactDate,
formatCompactDateTime,
formatDate,
formatDateTime,
formatPreciseDateTime,
formatTime,
toDate,
} from "@/lib/date";

type LocalDateFormat =
| "date"
| "dateTime"
| "preciseDateTime"
| "time"
| "compactDate"
| "compactDateTime";

type DateFormatter = (
value: DateInput | null | undefined,
options?: DateFormatOptions,
) => string;

const FORMATTERS: Record<LocalDateFormat, DateFormatter> = {
date: formatDate,
dateTime: formatDateTime,
preciseDateTime: formatPreciseDateTime,
time: formatTime,
compactDate: formatCompactDate,
compactDateTime: formatCompactDateTime,
};

const subscribe = () => () => {};
const getClientSnapshot = () => true;
const getServerSnapshot = () => false;

export function LocalDate({
value,
format = "dateTime",
fallback = "—",
}: {
value: DateInput | null | undefined;
format?: LocalDateFormat;
fallback?: string;
}) {
const isHydrated = useSyncExternalStore(
subscribe,
getClientSnapshot,
getServerSnapshot,
);
const date = toDate(value);

if (!date) return <>{fallback}</>;

const label = FORMATTERS[format](date, {
fallback,
timeZone: isHydrated ? undefined : "UTC",
});

return (
<time
dateTime={date.toISOString()}
className={isHydrated ? undefined : "invisible"}
aria-hidden={isHydrated ? undefined : true}
>
{label}
</time>
);
}
10 changes: 5 additions & 5 deletions web/components/logs/log-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
import { Empty, EmptyTitle } from "@/components/ui/empty";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { formatDateTime, formatTime } from "@/lib/date";
import { formatPreciseDateTime, formatTime } from "@/lib/date";
import { fetcher } from "@/lib/fetcher";

type LogLevel = "error" | "warn" | "info" | "debug";
Expand Down Expand Up @@ -478,7 +478,7 @@ function ServiceLogRow({
<div className="flex hover:bg-black/5 dark:hover:bg-white/5 -mx-2 px-2 py-0.5 group">
<span
className="shrink-0 w-[70px] text-slate-400 dark:text-slate-600 select-none pr-2 tabular-nums"
title={formatDateTime(entry.timestamp)}
title={formatPreciseDateTime(entry.timestamp)}
>
{formatTime(entry.timestamp)}
</span>
Expand Down Expand Up @@ -524,7 +524,7 @@ function RequestRow({
<div className="flex hover:bg-black/5 dark:hover:bg-white/5 -mx-2 px-2 py-0.5 group">
<span
className="shrink-0 w-[70px] text-slate-400 dark:text-slate-600 select-none pr-2 tabular-nums"
title={formatDateTime(entry.timestamp)}
title={formatPreciseDateTime(entry.timestamp)}
>
{formatTime(entry.timestamp)}
</span>
Expand Down Expand Up @@ -560,7 +560,7 @@ function BuildLogRow({
<div className="flex hover:bg-black/5 dark:hover:bg-white/5 -mx-2 px-2 py-0.5">
<span
className="shrink-0 w-[70px] text-slate-400 dark:text-slate-600 select-none pr-2 tabular-nums"
title={formatDateTime(entry.timestamp)}
title={formatPreciseDateTime(entry.timestamp)}
>
{formatTime(entry.timestamp)}
</span>
Expand All @@ -582,7 +582,7 @@ function ServerLogRow({
<div className="flex hover:bg-black/5 dark:hover:bg-white/5 -mx-2 px-2 py-0.5">
<span
className="shrink-0 w-[70px] text-slate-400 dark:text-slate-600 select-none pr-2 tabular-nums"
title={formatDateTime(entry.timestamp)}
title={formatPreciseDateTime(entry.timestamp)}
>
{formatTime(entry.timestamp)}
</span>
Expand Down
Loading
Loading