From 2106260e207eaf4e81d360b83c4060c587e68f83 Mon Sep 17 00:00:00 2001 From: myftija Date: Sun, 9 Nov 2025 20:46:58 +0100 Subject: [PATCH 01/11] Use read-only project-scoped s2 tokens for streaming deployment logs --- apps/webapp/app/env.server.ts | 3 +++ .../v3/DeploymentPresenter.server.ts | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 68d05563f6..6649d1cde2 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -1201,6 +1201,9 @@ const EnvironmentSchema = z EVENT_LOOP_MONITOR_UTILIZATION_SAMPLE_RATE: z.coerce.number().default(0.05), VERY_SLOW_QUERY_THRESHOLD_MS: z.coerce.number().int().optional(), + + S2_ACCESS_TOKEN: z.string(), + S2_DEPLOYMENT_LOGS_BASIN_NAME: z.string(), }) .and(GithubAppEnvSchema); diff --git a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts index 8387269cb6..b9346ac72f 100644 --- a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts @@ -3,7 +3,7 @@ import { ExternalBuildData, prepareDeploymentError, } from "@trigger.dev/core/v3"; -import { RuntimeEnvironment, type WorkerDeployment } from "@trigger.dev/database"; +import { type RuntimeEnvironment, type WorkerDeployment } from "@trigger.dev/database"; import { type PrismaClient, prisma } from "~/db.server"; import { type Organization } from "~/models/organization.server"; import { type Project } from "~/models/project.server"; @@ -11,6 +11,8 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { type User } from "~/models/user.server"; import { getUsername } from "~/utils/username"; import { processGitMetadata } from "./BranchesPresenter.server"; +import { S2 } from "@s2-dev/streamstore"; +import { env } from "~/env.server"; export type ErrorData = { name: string; @@ -43,6 +45,7 @@ export class DeploymentPresenter { select: { id: true, organizationId: true, + externalRef: true, }, where: { slug: projectSlug, @@ -142,7 +145,27 @@ export class DeploymentPresenter { ? ExternalBuildData.safeParse(deployment.externalBuildData) : undefined; + const s2 = new S2({ accessToken: env.S2_ACCESS_TOKEN }); + const projectS2AccessToken = await s2.accessTokens.issue({ + id: `${project.externalRef}-${new Date().getTime()}`, + expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour + scope: { + ops: ["read"], + basins: { + exact: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, + }, + streams: { + prefix: `projects/${project.externalRef}/deployments/`, + }, + }, + }); + return { + s2Logs: { + basin: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, + stream: `projects/${project.externalRef}/deployments/${deployment.shortCode}`, + accessToken: projectS2AccessToken.access_token, + }, deployment: { id: deployment.id, shortCode: deployment.shortCode, From 0a14b6942a610dd51f393f32821ba1d2455bf831 Mon Sep 17 00:00:00 2001 From: myftija Date: Sun, 9 Nov 2025 20:44:25 +0100 Subject: [PATCH 02/11] Add http2 to remix polyfills Needed for using s2 client-side. --- apps/webapp/remix.config.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/webapp/remix.config.js b/apps/webapp/remix.config.js index 69f28dda5c..eb8a0f024b 100644 --- a/apps/webapp/remix.config.js +++ b/apps/webapp/remix.config.js @@ -28,5 +28,12 @@ module.exports = { "parse-duration", "uncrypto", ], - browserNodeBuiltinsPolyfill: { modules: { path: true, os: true, crypto: true } }, + browserNodeBuiltinsPolyfill: { + modules: { + path: true, + os: true, + crypto: true, + http2: true, + }, + }, }; From c87adc4ee778eb6c68e988ff05d9de1bf25622ea Mon Sep 17 00:00:00 2001 From: myftija Date: Thu, 6 Nov 2025 16:03:38 +0100 Subject: [PATCH 03/11] Stream build-server logs in the deployment details page --- .../app/components/primitives/DateTime.tsx | 8 +- .../app/components/primitives/Paragraph.tsx | 12 + .../route.tsx | 239 ++++++++++++++- .../route.tsx | 2 +- apps/webapp/package.json | 1 + pnpm-lock.yaml | 274 ++++++++++++------ 6 files changed, 432 insertions(+), 104 deletions(-) diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index 9ce1b7957c..11de7f2451 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -13,6 +13,7 @@ type DateTimeProps = { includeTime?: boolean; showTimezone?: boolean; showTooltip?: boolean; + hideDate?: boolean; previousDate?: Date | string | null; // Add optional previous date for comparison }; @@ -184,7 +185,7 @@ function formatSmartDateTime(date: Date, timeZone: string, locales: string[]): s // Format time only function formatTimeOnly(date: Date, timeZone: string, locales: string[]): string { return new Intl.DateTimeFormat(locales, { - hour: "numeric", + hour: "2-digit", minute: "numeric", second: "numeric", timeZone, @@ -198,6 +199,7 @@ export const DateTimeAccurate = ({ timeZone = "UTC", previousDate = null, showTooltip = true, + hideDate = false, }: DateTimeProps) => { const locales = useLocales(); const [localTimeZone, setLocalTimeZone] = useState("UTC"); @@ -214,7 +216,9 @@ export const DateTimeAccurate = ({ }, []); // Smart formatting based on whether date changed - const formattedDateTime = realPrevDate + const formattedDateTime = hideDate + ? formatTimeOnly(realDate, localTimeZone, locales) + : realPrevDate ? isSameDay(realDate, realPrevDate) ? formatTimeOnly(realDate, localTimeZone, locales) : formatDateTimeAccurate(realDate, localTimeZone, locales) diff --git a/apps/webapp/app/components/primitives/Paragraph.tsx b/apps/webapp/app/components/primitives/Paragraph.tsx index 971ce3b4e5..9d699cc4b9 100644 --- a/apps/webapp/app/components/primitives/Paragraph.tsx +++ b/apps/webapp/app/components/primitives/Paragraph.tsx @@ -17,6 +17,10 @@ const paragraphVariants = { text: "font-sans text-sm font-normal text-text-bright", spacing: "mb-2", }, + "small/dimmed": { + text: "font-sans text-sm font-normal text-text-dimmed", + spacing: "mb-2", + }, "extra-small": { text: "font-sans text-xs font-normal text-text-dimmed", spacing: "mb-1.5", @@ -25,6 +29,14 @@ const paragraphVariants = { text: "font-sans text-xs font-normal text-text-bright", spacing: "mb-1.5", }, + "extra-small/dimmed": { + text: "font-sans text-xs font-normal text-text-dimmed", + spacing: "mb-1.5", + }, + "extra-small/dimmed/mono": { + text: "font-mono text-xs font-normal text-text-dimmed", + spacing: "mb-1.5", + }, "extra-small/mono": { text: "font-mono text-xs font-normal text-text-dimmed", spacing: "mb-1.5", diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index 63c0fc41a6..d731fa05e4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -1,6 +1,9 @@ import { Link, useLocation } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; +import { useEffect, useState, useRef, useCallback } from "react"; +import { S2, S2Error } from "@s2-dev/streamstore"; +import { Clipboard, ClipboardCheck } from "lucide-react"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { GitMetadata } from "~/components/GitMetadata"; import { RuntimeIcon } from "~/components/RuntimeIcon"; @@ -22,10 +25,15 @@ import { } from "~/components/primitives/Table"; import { DeploymentError } from "~/components/runs/v3/DeploymentError"; import { DeploymentStatus } from "~/components/runs/v3/DeploymentStatus"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "~/components/primitives/Tooltip"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; -import { useUser } from "~/hooks/useUser"; import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; @@ -40,7 +48,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { try { const presenter = new DeploymentPresenter(); - const { deployment } = await presenter.call({ + const { deployment, s2Logs } = await presenter.call({ userId, organizationSlug, projectSlug: projectParam, @@ -48,7 +56,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { deploymentShortCode: deploymentParam, }); - return typedjson({ deployment }); + return typedjson({ deployment, s2Logs }); } catch (error) { console.error(error); throw new Response(undefined, { @@ -58,15 +66,92 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { } }; +type LogEntry = { + message: string; + timestamp: Date; + level: "info" | "error" | "warn"; +}; + export default function Page() { - const { deployment } = useTypedLoaderData(); + const { deployment, s2Logs } = useTypedLoaderData(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const location = useLocation(); - const user = useUser(); const page = new URLSearchParams(location.search).get("page"); + const [logs, setLogs] = useState([]); + const [isStreaming, setIsStreaming] = useState(true); + const [streamError, setStreamError] = useState(null); + + useEffect(() => { + const abortController = new AbortController(); + + setLogs([]); + setStreamError(null); + + const streamLogs = async () => { + try { + const s2 = new S2({ accessToken: s2Logs.accessToken }); + const basin = s2.basin(s2Logs.basin); + const stream = basin.stream(s2Logs.stream); + + const readSession = await stream.readSession( + { + seq_num: 0, + wait: 60, + as: "bytes", + }, + { signal: abortController.signal } + ); + + const decoder = new TextDecoder(); + + for await (const record of readSession) { + try { + const headers: Record = {}; + + if (record.headers) { + for (const [nameBytes, valueBytes] of record.headers) { + headers[decoder.decode(nameBytes)] = decoder.decode(valueBytes); + } + } + const level = (headers["level"]?.toLowerCase() as LogEntry["level"]) ?? "info"; + + setLogs((prevLogs) => [ + ...prevLogs, + { + timestamp: new Date(record.timestamp), + message: decoder.decode(record.body), + level, + }, + ]); + } catch (err) { + console.error("Failed to parse log record:", err); + } + } + } catch (error) { + if (abortController.signal.aborted) return; + + const isNotFoundError = error instanceof S2Error && error.code === "stream_not_found"; + if (isNotFoundError) return; + + console.error("Failed to stream logs:", error); + setStreamError("Failed to stream logs"); + } finally { + if (!abortController.signal.aborted) { + setIsStreaming(false); + } + } + }; + + streamLogs(); + + return () => { + abortController.abort(); + }; + }, [s2Logs.basin, s2Logs.stream]); + return (
@@ -158,6 +243,10 @@ export default function Page() { /> + + Logs + + {deployment.canceledAt && ( Canceled at @@ -320,3 +409,143 @@ export default function Page() {
); } + +type LogsDisplayProps = { + logs: LogEntry[]; + isStreaming: boolean; + streamError: string | null; +}; + +function LogsDisplay({ logs, isStreaming, streamError }: LogsDisplayProps) { + const [copied, setCopied] = useState(false); + const [mouseOver, setMouseOver] = useState(false); + const logsContainerRef = useRef(null); + + // auto-scroll log container to bottom when new logs arrive + useEffect(() => { + if (logsContainerRef.current) { + logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; + } + }, [logs]); + + const onCopyLogs = useCallback( + (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const logsText = logs.map((log) => log.message).join("\n"); + navigator.clipboard.writeText(logsText); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1500); + }, + [logs] + ); + + const errorCount = logs.filter((log) => log.level === "error").length; + const warningCount = logs.filter((log) => log.level === "warn").length; + + return ( +
+
+
+
+
0 ? "bg-error/80" : "bg-charcoal-600" + )} + /> + + {`${errorCount} ${errorCount === 1 ? "error" : "errors"}`} + +
+
+
0 ? "bg-warning/80" : "bg-charcoal-600" + )} + /> + + {`${warningCount} ${warningCount === 1 ? "warning" : "warnings"}`} + +
+
+ {logs.length > 0 && ( + + + setMouseOver(true)} + onMouseLeave={() => setMouseOver(false)} + className={cn( + "transition-colors duration-100 focus-custom hover:cursor-pointer", + copied ? "text-success" : "text-text-dimmed hover:text-text-bright" + )} + > + {copied ? : } + + + {copied ? "Copied" : "Copy"} + + + + )} +
+ +
+
+ {logs.length === 0 && ( +
+ {streamError ? ( + Failed fetching logs + ) : ( + + {isStreaming ? "Waiting for logs..." : "No logs yet"} + + )} +
+ )} + {logs.map((log, index) => { + return ( +
+ + + + + {log.message} + +
+ ); + })} +
+
+
+ ); +} diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index 7f1f94dc31..6f161eea98 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -372,7 +372,7 @@ export default function Page() { {deploymentParam && ( <> - + diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 5820ac7949..10ca00982d 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -105,6 +105,7 @@ "@remix-run/serve": "2.1.0", "@remix-run/server-runtime": "2.1.0", "@remix-run/v1-meta": "^0.1.3", + "@s2-dev/streamstore": "^0.17.2", "@sentry/remix": "9.46.0", "@slack/web-api": "7.9.1", "@socket.io/redis-adapter": "^8.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92e835de43..db976ba354 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -404,25 +404,28 @@ importers: version: 3.7.1(react@18.2.0) '@remix-run/express': specifier: 2.1.0 - version: 2.1.0(express@4.20.0)(typescript@5.5.4) + version: 2.1.0(express@4.20.0)(typescript@5.9.3) '@remix-run/node': specifier: 2.1.0 - version: 2.1.0(typescript@5.5.4) + version: 2.1.0(typescript@5.9.3) '@remix-run/react': specifier: 2.1.0 - version: 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) + version: 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) '@remix-run/router': specifier: ^1.15.3 version: 1.15.3 '@remix-run/serve': specifier: 2.1.0 - version: 2.1.0(typescript@5.5.4) + version: 2.1.0(typescript@5.9.3) '@remix-run/server-runtime': specifier: 2.1.0 - version: 2.1.0(typescript@5.5.4) + version: 2.1.0(typescript@5.9.3) '@remix-run/v1-meta': specifier: ^0.1.3 version: 0.1.3(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0) + '@s2-dev/streamstore': + specifier: ^0.17.2 + version: 0.17.2(typescript@5.9.3) '@sentry/remix': specifier: 9.46.0 version: 9.46.0(patch_hash=biuxdxyvvwd3otdrxnv2y3covi)(@remix-run/node@2.1.0)(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0)(react@18.2.0) @@ -497,7 +500,7 @@ importers: version: 1.0.18 class-variance-authority: specifier: ^0.5.2 - version: 0.5.2(typescript@5.5.4) + version: 0.5.2(typescript@5.9.3) clsx: specifier: ^1.2.1 version: 1.2.1 @@ -545,7 +548,7 @@ importers: version: 10.12.11(react-dom@18.2.0)(react@18.2.0) graphile-worker: specifier: 0.16.6 - version: 0.16.6(patch_hash=hdpetta7btqcc7xb5wfkcnanoa)(typescript@5.5.4) + version: 0.16.6(patch_hash=hdpetta7btqcc7xb5wfkcnanoa)(typescript@5.9.3) humanize-duration: specifier: ^3.27.3 version: 3.27.3 @@ -762,13 +765,13 @@ importers: version: link:../../internal-packages/testcontainers '@remix-run/dev': specifier: 2.1.0 - version: 2.1.0(@remix-run/serve@2.1.0)(@types/node@20.14.14)(ts-node@10.9.1)(typescript@5.5.4) + version: 2.1.0(@remix-run/serve@2.1.0)(@types/node@20.14.14)(ts-node@10.9.1)(typescript@5.9.3) '@remix-run/eslint-config': specifier: 2.1.0 - version: 2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.5.4) + version: 2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.9.3) '@remix-run/testing': specifier: ^2.1.0 - version: 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) + version: 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) '@sentry/cli': specifier: 2.50.2 version: 2.50.2 @@ -867,10 +870,10 @@ importers: version: 8.5.4 '@typescript-eslint/eslint-plugin': specifier: ^5.59.6 - version: 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.5.4) + version: 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.9.3) '@typescript-eslint/parser': specifier: ^5.59.6 - version: 5.59.6(eslint@8.31.0)(typescript@5.5.4) + version: 5.59.6(eslint@8.31.0)(typescript@5.9.3) autoevals: specifier: ^0.0.130 version: 0.0.130(ws@8.12.0) @@ -915,7 +918,7 @@ importers: version: 16.0.1(postcss@8.5.4) postcss-loader: specifier: ^8.1.1 - version: 8.1.1(postcss@8.5.4)(typescript@5.5.4)(webpack@5.99.9) + version: 8.1.1(postcss@8.5.4)(typescript@5.9.3)(webpack@5.99.9) prettier: specifier: ^2.8.8 version: 2.8.8 @@ -942,13 +945,13 @@ importers: version: 3.4.1(ts-node@10.9.1) ts-node: specifier: ^10.7.0 - version: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4) + version: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.9.3) tsconfig-paths: specifier: ^3.14.1 version: 3.14.1 vite-tsconfig-paths: specifier: ^4.0.5 - version: 4.0.5(typescript@5.5.4) + version: 4.0.5(typescript@5.9.3) docs: {} @@ -9950,6 +9953,10 @@ packages: - supports-color dev: false + /@protobuf-ts/runtime@2.11.1: + resolution: {integrity: sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==} + dev: false + /@protobufjs/aspromise@1.1.2: resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -15285,7 +15292,7 @@ packages: - encoding dev: false - /@remix-run/dev@2.1.0(@remix-run/serve@2.1.0)(@types/node@20.14.14)(ts-node@10.9.1)(typescript@5.5.4): + /@remix-run/dev@2.1.0(@remix-run/serve@2.1.0)(@types/node@20.14.14)(ts-node@10.9.1)(typescript@5.9.3): resolution: {integrity: sha512-Hn5lw46F+a48dp5uHKe68ckaHgdStW4+PmLod+LMFEqrMbkF0j4XD1ousebxlv989o0Uy/OLgfRMgMy4cBOvHg==} engines: {node: '>=18.0.0'} hasBin: true @@ -15307,8 +15314,8 @@ packages: '@babel/traverse': 7.22.17 '@mdx-js/mdx': 2.3.0 '@npmcli/package-json': 4.0.1 - '@remix-run/serve': 2.1.0(typescript@5.5.4) - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/serve': 2.1.0(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) '@types/mdx': 2.0.5 '@vanilla-extract/integration': 6.2.1(@types/node@20.14.14) arg: 5.0.2 @@ -15346,7 +15353,7 @@ packages: semver: 7.6.3 tar-fs: 2.1.3 tsconfig-paths: 4.2.0 - typescript: 5.5.4 + typescript: 5.9.3 ws: 7.5.9 transitivePeerDependencies: - '@types/node' @@ -15364,7 +15371,7 @@ packages: - utf-8-validate dev: true - /@remix-run/eslint-config@2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.5.4): + /@remix-run/eslint-config@2.1.0(eslint@8.31.0)(react@18.2.0)(typescript@5.9.3): resolution: {integrity: sha512-yfeUnHpUG+XveujMi6QODKMGhs5CvKWCKzASU397BPXiPWbMv6r2acfODSWK64ZdBMu9hcLbOb42GBFydVQeHA==} engines: {node: '>=18.0.0'} peerDependencies: @@ -15379,28 +15386,28 @@ packages: '@babel/eslint-parser': 7.21.8(@babel/core@7.22.17)(eslint@8.31.0) '@babel/preset-react': 7.18.6(@babel/core@7.22.17) '@rushstack/eslint-patch': 1.2.0 - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.29.1)(eslint@8.31.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.59.6)(eslint-import-resolver-typescript@3.5.5)(eslint@8.31.0) - eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.6)(eslint@8.31.0)(typescript@5.5.4) + eslint-plugin-jest: 26.9.0(@typescript-eslint/eslint-plugin@5.59.6)(eslint@8.31.0)(typescript@5.9.3) eslint-plugin-jest-dom: 4.0.3(eslint@8.31.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.31.0) eslint-plugin-node: 11.1.0(eslint@8.31.0) eslint-plugin-react: 7.32.2(eslint@8.31.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.31.0) - eslint-plugin-testing-library: 5.11.0(eslint@8.31.0)(typescript@5.5.4) + eslint-plugin-testing-library: 5.11.0(eslint@8.31.0)(typescript@5.9.3) react: 18.2.0 - typescript: 5.5.4 + typescript: 5.9.3 transitivePeerDependencies: - eslint-import-resolver-webpack - jest - supports-color dev: true - /@remix-run/express@2.1.0(express@4.20.0)(typescript@5.5.4): + /@remix-run/express@2.1.0(express@4.20.0)(typescript@5.9.3): resolution: {integrity: sha512-R5myPowQx6LYWY3+EqP42q19MOCT3+ZGwb2f0UKNs9a34R8U3nFpGWL7saXryC+To+EasujEScc8rTQw5Pftog==} engines: {node: '>=18.0.0'} peerDependencies: @@ -15410,11 +15417,11 @@ packages: typescript: optional: true dependencies: - '@remix-run/node': 2.1.0(typescript@5.5.4) + '@remix-run/node': 2.1.0(typescript@5.9.3) express: 4.20.0 - typescript: 5.5.4 + typescript: 5.9.3 - /@remix-run/node@2.1.0(typescript@5.5.4): + /@remix-run/node@2.1.0(typescript@5.9.3): resolution: {integrity: sha512-TeSgjXnZUUlmw5FVpBVnXY7MLpracjdnwFNwoJE5NQkiUEFnGD/Yhvk4F2fOCkszqc2Z25KRclc5noweyiFu6Q==} engines: {node: '>=18.0.0'} peerDependencies: @@ -15423,7 +15430,7 @@ packages: typescript: optional: true dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) '@remix-run/web-fetch': 4.4.1 '@remix-run/web-file': 3.1.0 '@remix-run/web-stream': 1.1.0 @@ -15431,9 +15438,9 @@ packages: cookie-signature: 1.2.0 source-map-support: 0.5.21 stream-slice: 0.1.2 - typescript: 5.5.4 + typescript: 5.9.3 - /@remix-run/react@2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4): + /@remix-run/react@2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3): resolution: {integrity: sha512-DeYgfsvNxHqNn29sGA3XsZCciMKo2EFTQ9hHkuVPTsJXC4ipHr6Dja1j6UzZYPe/ZuKppiuTjueWCQlE2jOe1w==} engines: {node: '>=18.0.0'} peerDependencies: @@ -15445,11 +15452,11 @@ packages: optional: true dependencies: '@remix-run/router': 1.10.0 - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-router-dom: 6.17.0(react-dom@18.2.0)(react@18.2.0) - typescript: 5.5.4 + typescript: 5.9.3 /@remix-run/router@1.10.0: resolution: {integrity: sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw==} @@ -15460,13 +15467,13 @@ packages: engines: {node: '>=14.0.0'} dev: false - /@remix-run/serve@2.1.0(typescript@5.5.4): + /@remix-run/serve@2.1.0(typescript@5.9.3): resolution: {integrity: sha512-XHI+vPYz217qrg1QcV38TTPlEBTzMJzAt0SImPutyF0S2IBrZGZIFMEsspI0i0wNvdcdQz1IqmSx+mTghzW8eQ==} engines: {node: '>=18.0.0'} hasBin: true dependencies: - '@remix-run/express': 2.1.0(express@4.20.0)(typescript@5.5.4) - '@remix-run/node': 2.1.0(typescript@5.5.4) + '@remix-run/express': 2.1.0(express@4.20.0)(typescript@5.9.3) + '@remix-run/node': 2.1.0(typescript@5.9.3) chokidar: 3.6.0 compression: 1.7.4 express: 4.20.0 @@ -15477,7 +15484,7 @@ packages: - supports-color - typescript - /@remix-run/server-runtime@2.1.0(typescript@5.5.4): + /@remix-run/server-runtime@2.1.0(typescript@5.9.3): resolution: {integrity: sha512-Uz69yF4Gu6F3VYQub3JgDo9godN8eDMeZclkadBTAWN7bYLonu0ChR/GlFxS35OLeF7BDgudxOSZob0nE1WHNg==} engines: {node: '>=18.0.0'} peerDependencies: @@ -15492,9 +15499,9 @@ packages: cookie: 0.4.2 set-cookie-parser: 2.6.0 source-map: 0.7.4 - typescript: 5.5.4 + typescript: 5.9.3 - /@remix-run/testing@2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4): + /@remix-run/testing@2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3): resolution: {integrity: sha512-eLPx4Bmjt243kyRpQTong1eFo6nkvSfCr65bb5PfoF172DKnsSSCYWAmBmB72VwtAPESHxBm3g6AUbhwphkU6A==} engines: {node: '>=18.0.0'} peerDependencies: @@ -15504,12 +15511,12 @@ packages: typescript: optional: true dependencies: - '@remix-run/node': 2.1.0(typescript@5.5.4) - '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) + '@remix-run/node': 2.1.0(typescript@5.9.3) + '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) '@remix-run/router': 1.10.0 react: 18.2.0 react-router-dom: 6.17.0(react-dom@18.2.0)(react@18.2.0) - typescript: 5.5.4 + typescript: 5.9.3 transitivePeerDependencies: - react-dom dev: true @@ -15520,8 +15527,8 @@ packages: '@remix-run/react': ^1.15.0 || ^2.0.0 '@remix-run/server-runtime': ^1.15.0 || ^2.0.0 dependencies: - '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) dev: false /@remix-run/web-blob@3.1.0: @@ -15721,6 +15728,15 @@ packages: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} dev: true + /@s2-dev/streamstore@0.17.2(typescript@5.9.3): + resolution: {integrity: sha512-Tb0U5YOUHBPRloK9AK/pmzmeDmp5VWIFWS9yAM6ynL5mc0G+yLaOf38ExnOSyWYaFIormb8bwaKpWGjbjQ3xAw==} + peerDependencies: + typescript: ^5.9.3 + dependencies: + '@protobuf-ts/runtime': 2.11.1 + typescript: 5.9.3 + dev: false + /@sec-ant/readable-stream@0.4.1: resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} dev: true @@ -15975,10 +15991,10 @@ packages: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.36.0 - '@remix-run/node': 2.1.0(typescript@5.5.4) - '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) + '@remix-run/node': 2.1.0(typescript@5.9.3) + '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) '@remix-run/router': 1.15.3 - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) '@sentry/cli': 2.50.2 '@sentry/core': 9.46.0 '@sentry/node': 9.46.0 @@ -18534,7 +18550,7 @@ packages: dev: false optional: true - /@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.5.4): + /@typescript-eslint/eslint-plugin@5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.9.3): resolution: {integrity: sha512-sXtOgJNEuRU5RLwPUb1jxtToZbgvq3M6FPpY4QENxoOggK+UpTxUBpj6tD8+Qh2g46Pi9We87E+eHnUw8YcGsw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -18546,23 +18562,23 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.5.1 - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) '@typescript-eslint/scope-manager': 5.59.6 - '@typescript-eslint/type-utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/type-utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) debug: 4.3.4 eslint: 8.31.0 grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 semver: 7.6.3 - tsutils: 3.21.0(typescript@5.5.4) - typescript: 5.5.4 + tsutils: 3.21.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.5.4): + /@typescript-eslint/parser@5.59.6(eslint@8.31.0)(typescript@5.9.3): resolution: {integrity: sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -18574,10 +18590,10 @@ packages: dependencies: '@typescript-eslint/scope-manager': 5.59.6 '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.9.3) debug: 4.4.0 eslint: 8.31.0 - typescript: 5.5.4 + typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true @@ -18590,7 +18606,7 @@ packages: '@typescript-eslint/visitor-keys': 5.59.6 dev: true - /@typescript-eslint/type-utils@5.59.6(eslint@8.31.0)(typescript@5.5.4): + /@typescript-eslint/type-utils@5.59.6(eslint@8.31.0)(typescript@5.9.3): resolution: {integrity: sha512-A4tms2Mp5yNvLDlySF+kAThV9VTBPCvGf0Rp8nl/eoDX9Okun8byTKoj3fJ52IJitjWOk0fKPNQhXEB++eNozQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -18600,12 +18616,12 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.9.3) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) debug: 4.4.0 eslint: 8.31.0 - tsutils: 3.21.0(typescript@5.5.4) - typescript: 5.5.4 + tsutils: 3.21.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true @@ -18615,7 +18631,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /@typescript-eslint/typescript-estree@5.59.6(typescript@5.5.4): + /@typescript-eslint/typescript-estree@5.59.6(typescript@5.9.3): resolution: {integrity: sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -18630,13 +18646,13 @@ packages: globby: 11.1.0 is-glob: 4.0.3 semver: 7.7.2 - tsutils: 3.21.0(typescript@5.5.4) - typescript: 5.5.4 + tsutils: 3.21.0(typescript@5.9.3) + typescript: 5.9.3 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/utils@5.59.6(eslint@8.31.0)(typescript@5.5.4): + /@typescript-eslint/utils@5.59.6(eslint@8.31.0)(typescript@5.9.3): resolution: {integrity: sha512-vzaaD6EXbTS29cVH0JjXBdzMt6VBlv+hE31XktDRMX1j3462wZCJa7VzO2AxXEXcIl8GQqZPcOPuW/Z1tZVogg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -18647,7 +18663,7 @@ packages: '@types/semver': 7.5.1 '@typescript-eslint/scope-manager': 5.59.6 '@typescript-eslint/types': 5.59.6 - '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.5.4) + '@typescript-eslint/typescript-estree': 5.59.6(typescript@5.9.3) eslint: 8.31.0 eslint-scope: 5.1.1 semver: 7.7.2 @@ -20733,7 +20749,7 @@ packages: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} dev: false - /class-variance-authority@0.5.2(typescript@5.5.4): + /class-variance-authority@0.5.2(typescript@5.9.3): resolution: {integrity: sha512-j7Qqw3NPbs4IpO80gvdACWmVvHiLLo5MECacUBLnJG17CrLpWaQ7/4OaWX6P0IO1j2nvZ7AuSfBS/ImtEUZJGA==} peerDependencies: typescript: '>= 4.5.5 < 6' @@ -20741,7 +20757,7 @@ packages: typescript: optional: true dependencies: - typescript: 5.5.4 + typescript: 5.9.3 dev: false /class-variance-authority@0.7.0: @@ -21144,7 +21160,23 @@ packages: typescript: 5.5.4 dev: false - /cosmiconfig@9.0.0(typescript@5.5.4): + /cosmiconfig@8.3.6(typescript@5.9.3): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.9.3 + dev: false + + /cosmiconfig@9.0.0(typescript@5.9.3): resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} peerDependencies: @@ -21157,7 +21189,7 @@ packages: import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 - typescript: 5.5.4 + typescript: 5.9.3 dev: true /cp-file@10.0.0: @@ -22907,7 +22939,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) debug: 3.2.7 eslint: 8.31.0 eslint-import-resolver-node: 0.3.7 @@ -22937,7 +22969,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) debug: 3.2.7 eslint: 8.31.0 eslint-import-resolver-node: 0.3.9 @@ -22967,7 +22999,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/parser': 5.59.6(eslint@8.31.0)(typescript@5.9.3) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -23004,7 +23036,7 @@ packages: requireindex: 1.2.0 dev: true - /eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.59.6)(eslint@8.31.0)(typescript@5.5.4): + /eslint-plugin-jest@26.9.0(@typescript-eslint/eslint-plugin@5.59.6)(eslint@8.31.0)(typescript@5.9.3): resolution: {integrity: sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -23017,8 +23049,8 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.5.4) - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/eslint-plugin': 5.59.6(@typescript-eslint/parser@5.59.6)(eslint@8.31.0)(typescript@5.9.3) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) eslint: 8.31.0 transitivePeerDependencies: - supports-color @@ -23098,13 +23130,13 @@ packages: string.prototype.matchall: 4.0.8 dev: true - /eslint-plugin-testing-library@5.11.0(eslint@8.31.0)(typescript@5.5.4): + /eslint-plugin-testing-library@5.11.0(eslint@8.31.0)(typescript@5.9.3): resolution: {integrity: sha512-ELY7Gefo+61OfXKlQeXNIDVVLPcvKTeiQOoMZG9TeuWa7Ln4dUNRv8JdRWBQI9Mbb427XGlVB1aa1QPZxBJM8Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.5.4) + '@typescript-eslint/utils': 5.59.6(eslint@8.31.0)(typescript@5.9.3) eslint: 8.31.0 transitivePeerDependencies: - supports-color @@ -24498,6 +24530,27 @@ packages: dev: false patched: true + /graphile-worker@0.16.6(patch_hash=hdpetta7btqcc7xb5wfkcnanoa)(typescript@5.9.3): + resolution: {integrity: sha512-e7gGYDmGqzju2l83MpzX8vNG/lOtVJiSzI3eZpAFubSxh/cxs7sRrRGBGjzBP1kNG0H+c95etPpNRNlH65PYhw==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@graphile/logger': 0.2.0 + '@types/debug': 4.1.12 + '@types/pg': 8.11.6 + cosmiconfig: 8.3.6(typescript@5.9.3) + graphile-config: 0.0.1-beta.8 + json5: 2.2.3 + pg: 8.11.5 + tslib: 2.6.2 + yargs: 17.7.2 + transitivePeerDependencies: + - pg-native + - supports-color + - typescript + dev: false + patched: true + /graphql@16.6.0: resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -28944,7 +28997,7 @@ packages: dependencies: lilconfig: 2.1.0 postcss: 8.4.29 - ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4) + ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.9.3) yaml: 2.3.1 dev: true @@ -28962,7 +29015,7 @@ packages: dependencies: lilconfig: 3.1.3 postcss: 8.5.3 - ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4) + ts-node: 10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.9.3) yaml: 2.7.1 /postcss-load-config@6.0.1(postcss@8.5.4)(tsx@4.17.0): @@ -28988,7 +29041,7 @@ packages: tsx: 4.17.0 dev: true - /postcss-loader@8.1.1(postcss@8.5.4)(typescript@5.5.4)(webpack@5.99.9): + /postcss-loader@8.1.1(postcss@8.5.4)(typescript@5.9.3)(webpack@5.99.9): resolution: {integrity: sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==} engines: {node: '>= 18.12.0'} peerDependencies: @@ -29001,7 +29054,7 @@ packages: webpack: optional: true dependencies: - cosmiconfig: 9.0.0(typescript@5.5.4) + cosmiconfig: 9.0.0(typescript@5.9.3) jiti: 1.21.0 postcss: 8.5.4 semver: 7.6.3 @@ -30602,7 +30655,7 @@ packages: '@remix-run/server-runtime': ^1.1.1 remix-auth: ^3.2.1 dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) crypto-js: 4.1.1 remix-auth: 3.6.0(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0) dev: false @@ -30613,7 +30666,7 @@ packages: '@remix-run/server-runtime': ^1.0.0 remix-auth: ^3.4.0 dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) remix-auth: 3.6.0(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0) remix-auth-oauth2: 1.11.0(@remix-run/server-runtime@2.1.0)(remix-auth@3.6.0) transitivePeerDependencies: @@ -30626,7 +30679,7 @@ packages: '@remix-run/server-runtime': ^1.0.0 || ^2.0.0 remix-auth: ^3.6.0 dependencies: - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) debug: 4.4.0 remix-auth: 3.6.0(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0) transitivePeerDependencies: @@ -30639,8 +30692,8 @@ packages: '@remix-run/react': ^1.0.0 || ^2.0.0 '@remix-run/server-runtime': ^1.0.0 || ^2.0.0 dependencies: - '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) uuid: 8.3.2 dev: false @@ -30651,8 +30704,8 @@ packages: '@remix-run/server-runtime': ^1.16.0 || ^2.0 react: ^17.0.2 || ^18.0.0 dependencies: - '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) - '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) + '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) + '@remix-run/server-runtime': 2.1.0(typescript@5.9.3) react: 18.2.0 dev: false @@ -30689,8 +30742,8 @@ packages: zod: optional: true dependencies: - '@remix-run/node': 2.1.0(typescript@5.5.4) - '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.5.4) + '@remix-run/node': 2.1.0(typescript@5.9.3) + '@remix-run/react': 2.1.0(react-dom@18.2.0)(react@18.2.0)(typescript@5.9.3) '@remix-run/router': 1.15.3 intl-parse-accept-language: 1.0.0 react: 18.2.0 @@ -32884,7 +32937,7 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - /ts-node@10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.5.4): + /ts-node@10.9.1(@swc/core@1.3.26)(@types/node@20.14.14)(typescript@5.9.3): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -32911,7 +32964,7 @@ packages: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.5.4 + typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -32955,6 +33008,19 @@ packages: typescript: 5.5.4 dev: true + /tsconfck@2.1.2(typescript@5.9.3): + resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} + engines: {node: ^14.13.1 || ^16 || >=18} + hasBin: true + peerDependencies: + typescript: ^4.3.5 || ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + dependencies: + typescript: 5.9.3 + dev: true + /tsconfck@3.1.3(typescript@5.5.4): resolution: {integrity: sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==} engines: {node: ^18 || >=20} @@ -33074,14 +33140,14 @@ packages: - yaml dev: true - /tsutils@3.21.0(typescript@5.5.4): + /tsutils@3.21.0(typescript@5.9.3): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' dependencies: tslib: 1.14.1 - typescript: 5.5.4 + typescript: 5.9.3 dev: true /tsx@3.12.2: @@ -33348,6 +33414,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + /typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + /ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} @@ -33964,6 +34035,17 @@ packages: - typescript dev: true + /vite-tsconfig-paths@4.0.5(typescript@5.9.3): + resolution: {integrity: sha512-/L/eHwySFYjwxoYt1WRJniuK/jPv+WGwgRGBYx3leciR5wBeqntQpUE6Js6+TJemChc+ter7fDBKieyEWDx4yQ==} + dependencies: + debug: 4.3.7(supports-color@10.0.0) + globrex: 0.1.2 + tsconfck: 2.1.2(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /vite@4.1.4(@types/node@20.14.14): resolution: {integrity: sha512-3knk/HsbSTKEin43zHu7jTwYWv81f8kgAL99G5NWBcA1LKvtvcVAC4JjBH1arBunO9kQka+1oGbrMKOjk4ZrBg==} engines: {node: ^14.18.0 || >=16.0.0} From 407ff50f90ec3a17ca225e83389be000e880ec8a Mon Sep 17 00:00:00 2001 From: myftija Date: Sun, 9 Nov 2025 20:45:25 +0100 Subject: [PATCH 04/11] Disable 12-hour format in the DateTime component --- apps/webapp/app/components/primitives/DateTime.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index 11de7f2451..dcefae81d4 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -77,6 +77,7 @@ export function formatDateTime( minute: includeTime ? "numeric" : undefined, second: includeTime && includeSeconds ? "numeric" : undefined, timeZone, + hour12: false, }).format(date); } @@ -179,6 +180,7 @@ function formatSmartDateTime(date: Date, timeZone: string, locales: string[]): s timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, + hour12: false, }).format(date); } @@ -191,6 +193,7 @@ function formatTimeOnly(date: Date, timeZone: string, locales: string[]): string timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, + hour12: false, }).format(date); } @@ -255,6 +258,7 @@ function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]) timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, + hour12: false, }).format(date); return formattedDateTime; @@ -282,6 +286,7 @@ function formatDateTimeShort(date: Date, timeZone: string, locales: string[]): s timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, + hour12: false, }).format(date); return formattedDateTime; From 42b942bbb4aa3f4638f4a11b3d951d38938fd6bd Mon Sep 17 00:00:00 2001 From: myftija Date: Sun, 9 Nov 2025 21:10:36 +0100 Subject: [PATCH 05/11] Enable collapsing the logs panel --- .../route.tsx | 178 +++++++++++------- 1 file changed, 110 insertions(+), 68 deletions(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index d731fa05e4..31cf036a6f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -3,7 +3,7 @@ import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { useEffect, useState, useRef, useCallback } from "react"; import { S2, S2Error } from "@s2-dev/streamstore"; -import { Clipboard, ClipboardCheck } from "lucide-react"; +import { Clipboard, ClipboardCheck, ChevronDown, ChevronUp } from "lucide-react"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { GitMetadata } from "~/components/GitMetadata"; import { RuntimeIcon } from "~/components/RuntimeIcon"; @@ -410,15 +410,20 @@ export default function Page() { ); } -type LogsDisplayProps = { +function LogsDisplay({ + logs, + isStreaming, + streamError, + initialCollapsed = false, +}: { logs: LogEntry[]; isStreaming: boolean; streamError: string | null; -}; - -function LogsDisplay({ logs, isStreaming, streamError }: LogsDisplayProps) { + initialCollapsed?: boolean; +}) { const [copied, setCopied] = useState(false); const [mouseOver, setMouseOver] = useState(false); + const [collapsed, setCollapsed] = useState(initialCollapsed); const logsContainerRef = useRef(null); // auto-scroll log container to bottom when new logs arrive @@ -473,78 +478,115 @@ function LogsDisplay({ logs, isStreaming, streamError }: LogsDisplayProps) {
{logs.length > 0 && ( - - - setMouseOver(true)} - onMouseLeave={() => setMouseOver(false)} - className={cn( - "transition-colors duration-100 focus-custom hover:cursor-pointer", - copied ? "text-success" : "text-text-dimmed hover:text-text-bright" - )} - > - {copied ? : } - - - {copied ? "Copied" : "Copy"} - - - +
+ + + setMouseOver(true)} + onMouseLeave={() => setMouseOver(false)} + className={cn( + "transition-colors duration-100 focus-custom hover:cursor-pointer", + copied ? "text-success" : "text-text-dimmed hover:text-text-bright" + )} + > +
+ {copied ? ( + + ) : ( + + )} +
+
+ + {copied ? "Copied" : "Copy"} + +
+
+ + + + setCollapsed(!collapsed)} + className={cn( + "transition-colors duration-100 focus-custom hover:cursor-pointer", + "text-text-dimmed hover:text-text-bright" + )} + > + {collapsed ? ( + + ) : ( + + )} + + + {collapsed ? "Expand" : "Collapse"} + + + +
)}
-
-
- {logs.length === 0 && ( -
- {streamError ? ( - Failed fetching logs - ) : ( - - {isStreaming ? "Waiting for logs..." : "No logs yet"} - - )} -
+
+
{ - return ( -
+
+ {logs.length === 0 && ( +
+ {streamError ? ( + Failed fetching logs + ) : ( + + {isStreaming ? "Waiting for logs..." : "No logs yet"} + )} - > - - - - + )} + {logs.map((log, index) => { + return ( +
- {log.message} - -
- ); - })} + + + + + {log.message} + +
+ ); + })} +
+ {collapsed && ( +
+ )}
); From f54ec87b878df6f8e5479199f8e3d39b5fbca051 Mon Sep 17 00:00:00 2001 From: myftija Date: Sun, 9 Nov 2025 21:32:49 +0100 Subject: [PATCH 06/11] Auto-collapse logs for succesful/timedout/queued deployments --- .../route.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index 31cf036a6f..ae48da440e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -245,7 +245,14 @@ export default function Page() { Logs - + {deployment.canceledAt && ( @@ -426,6 +433,10 @@ function LogsDisplay({ const [collapsed, setCollapsed] = useState(initialCollapsed); const logsContainerRef = useRef(null); + useEffect(() => { + setCollapsed(initialCollapsed); + }, [initialCollapsed]); + // auto-scroll log container to bottom when new logs arrive useEffect(() => { if (logsContainerRef.current) { From c272b5ba2d057da49bdeb4a27989bd290084e900 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 10 Nov 2025 11:26:44 +0100 Subject: [PATCH 07/11] Make S2 env vars optional --- apps/webapp/app/env.server.ts | 27 ++++++++++++-- .../v3/DeploymentPresenter.server.ts | 37 +++++++++++-------- .../route.tsx | 29 +++++++++------ 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts index 6649d1cde2..f3b1ef54d4 100644 --- a/apps/webapp/app/env.server.ts +++ b/apps/webapp/app/env.server.ts @@ -25,6 +25,27 @@ const GithubAppEnvSchema = z.preprocess( ]) ); +// eventually we can make all S2 env vars required once the S2 OSS version is out +const S2EnvSchema = z.preprocess( + (val) => { + const obj = val as any; + if (!obj || !obj.S2_ENABLED) { + return { ...obj, S2_ENABLED: "0" }; + } + return obj; + }, + z.discriminatedUnion("S2_ENABLED", [ + z.object({ + S2_ENABLED: z.literal("1"), + S2_ACCESS_TOKEN: z.string(), + S2_DEPLOYMENT_LOGS_BASIN_NAME: z.string(), + }), + z.object({ + S2_ENABLED: z.literal("0"), + }), + ]) +); + const EnvironmentSchema = z .object({ NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]), @@ -1201,11 +1222,9 @@ const EnvironmentSchema = z EVENT_LOOP_MONITOR_UTILIZATION_SAMPLE_RATE: z.coerce.number().default(0.05), VERY_SLOW_QUERY_THRESHOLD_MS: z.coerce.number().int().optional(), - - S2_ACCESS_TOKEN: z.string(), - S2_DEPLOYMENT_LOGS_BASIN_NAME: z.string(), }) - .and(GithubAppEnvSchema); + .and(GithubAppEnvSchema) + .and(S2EnvSchema); export type Environment = z.infer; export const env = EnvironmentSchema.parse(process.env); diff --git a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts index b9346ac72f..eab554cd3f 100644 --- a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts @@ -145,27 +145,32 @@ export class DeploymentPresenter { ? ExternalBuildData.safeParse(deployment.externalBuildData) : undefined; - const s2 = new S2({ accessToken: env.S2_ACCESS_TOKEN }); - const projectS2AccessToken = await s2.accessTokens.issue({ - id: `${project.externalRef}-${new Date().getTime()}`, - expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour - scope: { - ops: ["read"], - basins: { - exact: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, - }, - streams: { - prefix: `projects/${project.externalRef}/deployments/`, + let s2Logs = undefined; + if (env.S2_ENABLED === "1") { + const s2 = new S2({ accessToken: env.S2_ACCESS_TOKEN }); + const projectS2AccessToken = await s2.accessTokens.issue({ + id: `${project.externalRef}-${new Date().getTime()}`, + expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour + scope: { + ops: ["read"], + basins: { + exact: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, + }, + streams: { + prefix: `projects/${project.externalRef}/deployments/`, + }, }, - }, - }); + }); - return { - s2Logs: { + s2Logs = { basin: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, stream: `projects/${project.externalRef}/deployments/${deployment.shortCode}`, accessToken: projectS2AccessToken.access_token, - }, + }; + } + + return { + s2Logs, deployment: { id: deployment.id, shortCode: deployment.shortCode, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index ae48da440e..784e887783 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -80,11 +80,14 @@ export default function Page() { const location = useLocation(); const page = new URLSearchParams(location.search).get("page"); + const logsDisabled = s2Logs === undefined; const [logs, setLogs] = useState([]); const [isStreaming, setIsStreaming] = useState(true); const [streamError, setStreamError] = useState(null); useEffect(() => { + if (logsDisabled) return; + const abortController = new AbortController(); setLogs([]); @@ -150,7 +153,7 @@ export default function Page() { return () => { abortController.abort(); }; - }, [s2Logs.basin, s2Logs.stream]); + }, [s2Logs?.basin, s2Logs?.stream]); return (
@@ -243,17 +246,19 @@ export default function Page() { /> - - Logs - - + {!logsDisabled && ( + + Logs + + + )} {deployment.canceledAt && ( Canceled at From 285a5fa784cfbcb123e700322c8528ef387b793b Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 10 Nov 2025 11:31:22 +0100 Subject: [PATCH 08/11] Show the logs section only for gh-triggered deployments --- apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts index eab554cd3f..47284c480c 100644 --- a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts @@ -141,12 +141,14 @@ export class DeploymentPresenter { }, }); + const gitMetadata = processGitMetadata(deployment.git); + const externalBuildData = deployment.externalBuildData ? ExternalBuildData.safeParse(deployment.externalBuildData) : undefined; let s2Logs = undefined; - if (env.S2_ENABLED === "1") { + if (env.S2_ENABLED === "1" && gitMetadata?.source === "trigger_github_app") { const s2 = new S2({ accessToken: env.S2_ACCESS_TOKEN }); const projectS2AccessToken = await s2.accessTokens.issue({ id: `${project.externalRef}-${new Date().getTime()}`, @@ -206,7 +208,7 @@ export class DeploymentPresenter { errorData: DeploymentPresenter.prepareErrorData(deployment.errorData), isBuilt: !!deployment.builtAt, type: deployment.type, - git: processGitMetadata(deployment.git), + git: gitMetadata, }, }; } From 7d0ef6e7827a6b678e57b4a52d678046d8a49a02 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 10 Nov 2025 12:08:48 +0100 Subject: [PATCH 09/11] Cache s2 access tokens in redis --- .../v3/DeploymentPresenter.server.ts | 80 ++++++++++++++----- .../route.tsx | 2 +- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts index 47284c480c..e4db2bd17f 100644 --- a/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts @@ -1,6 +1,7 @@ import { DeploymentErrorData, ExternalBuildData, + logger, prepareDeploymentError, } from "@trigger.dev/core/v3"; import { type RuntimeEnvironment, type WorkerDeployment } from "@trigger.dev/database"; @@ -13,6 +14,21 @@ import { getUsername } from "~/utils/username"; import { processGitMetadata } from "./BranchesPresenter.server"; import { S2 } from "@s2-dev/streamstore"; import { env } from "~/env.server"; +import { createRedisClient } from "~/redis.server"; +import { tryCatch } from "@trigger.dev/core"; + +const S2_TOKEN_KEY_PREFIX = "s2-token:project:"; + +const s2TokenRedis = createRedisClient("s2-token-cache", { + host: env.CACHE_REDIS_HOST, + port: env.CACHE_REDIS_PORT, + username: env.CACHE_REDIS_USERNAME, + password: env.CACHE_REDIS_PASSWORD, + tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true", + clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1", +}); + +const s2 = env.S2_ENABLED === "1" ? new S2({ accessToken: env.S2_ACCESS_TOKEN }) : undefined; export type ErrorData = { name: string; @@ -149,26 +165,17 @@ export class DeploymentPresenter { let s2Logs = undefined; if (env.S2_ENABLED === "1" && gitMetadata?.source === "trigger_github_app") { - const s2 = new S2({ accessToken: env.S2_ACCESS_TOKEN }); - const projectS2AccessToken = await s2.accessTokens.issue({ - id: `${project.externalRef}-${new Date().getTime()}`, - expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour - scope: { - ops: ["read"], - basins: { - exact: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, - }, - streams: { - prefix: `projects/${project.externalRef}/deployments/`, - }, - }, - }); + const [error, accessToken] = await tryCatch(this.getS2AccessToken(project.externalRef)); - s2Logs = { - basin: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, - stream: `projects/${project.externalRef}/deployments/${deployment.shortCode}`, - accessToken: projectS2AccessToken.access_token, - }; + if (error) { + logger.error("Failed getting S2 access token", { error }); + } else { + s2Logs = { + basin: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, + stream: `projects/${project.externalRef}/deployments/${deployment.shortCode}`, + accessToken, + }; + } } return { @@ -213,6 +220,41 @@ export class DeploymentPresenter { }; } + private async getS2AccessToken(projectRef: string): Promise { + if (env.S2_ENABLED !== "1" || !s2) { + throw new Error("Failed getting S2 access token: S2 is not enabled"); + } + + const redisKey = `${S2_TOKEN_KEY_PREFIX}${projectRef}`; + const cachedToken = await s2TokenRedis.get(redisKey); + + if (cachedToken) { + return cachedToken; + } + + const { access_token: accessToken } = await s2.accessTokens.issue({ + id: `${projectRef}-${new Date().getTime()}`, + expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour + scope: { + ops: ["read"], + basins: { + exact: env.S2_DEPLOYMENT_LOGS_BASIN_NAME, + }, + streams: { + prefix: `projects/${projectRef}/deployments/`, + }, + }, + }); + + await s2TokenRedis.setex( + redisKey, + 59 * 60, // slightly shorter than the token validity period + accessToken + ); + + return accessToken; + } + public static prepareErrorData(errorData: WorkerDeployment["errorData"]): ErrorData | undefined { if (!errorData) { return; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index 784e887783..d86c0b12ef 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -153,7 +153,7 @@ export default function Page() { return () => { abortController.abort(); }; - }, [s2Logs?.basin, s2Logs?.stream]); + }, [s2Logs?.basin, s2Logs?.stream, s2Logs?.accessToken]); return (
From 1daa07e232ddd0a594d5c1e732a78ec37111fb54 Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 10 Nov 2025 12:12:16 +0100 Subject: [PATCH 10/11] Reset streaming state --- .../route.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index d86c0b12ef..3d5d4ec0e4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -92,6 +92,7 @@ export default function Page() { setLogs([]); setStreamError(null); + setIsStreaming(true); const streamLogs = async () => { try { From aeea73606ff057a9f5bce5a2dc2d096f889cc21c Mon Sep 17 00:00:00 2001 From: myftija Date: Mon, 10 Nov 2025 12:33:46 +0100 Subject: [PATCH 11/11] Expose 12h format as a param for the Datetime components --- .../app/components/primitives/DateTime.tsx | 55 ++++++++++--------- .../route.tsx | 2 +- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/apps/webapp/app/components/primitives/DateTime.tsx b/apps/webapp/app/components/primitives/DateTime.tsx index dcefae81d4..258a18d538 100644 --- a/apps/webapp/app/components/primitives/DateTime.tsx +++ b/apps/webapp/app/components/primitives/DateTime.tsx @@ -15,6 +15,7 @@ type DateTimeProps = { showTooltip?: boolean; hideDate?: boolean; previousDate?: Date | string | null; // Add optional previous date for comparison + hour12?: boolean; }; export const DateTime = ({ @@ -24,6 +25,7 @@ export const DateTime = ({ includeTime = true, showTimezone = false, showTooltip = true, + hour12 = true, }: DateTimeProps) => { const locales = useLocales(); const [localTimeZone, setLocalTimeZone] = useState("UTC"); @@ -51,7 +53,8 @@ export const DateTime = ({ timeZone ?? localTimeZone, locales, includeSeconds, - includeTime + includeTime, + hour12 ).replace(/\s/g, String.fromCharCode(32))} {showTimezone ? ` (${timeZone ?? "UTC"})` : null} @@ -67,7 +70,8 @@ export function formatDateTime( timeZone: string, locales: string[], includeSeconds: boolean, - includeTime: boolean + includeTime: boolean, + hour12: boolean = true ): string { return new Intl.DateTimeFormat(locales, { year: "numeric", @@ -77,7 +81,7 @@ export function formatDateTime( minute: includeTime ? "numeric" : undefined, second: includeTime && includeSeconds ? "numeric" : undefined, timeZone, - hour12: false, + hour12, }).format(date); } @@ -124,7 +128,7 @@ export function formatDateTimeISO(date: Date, timeZone: string): string { } // New component that only shows date when it changes -export const SmartDateTime = ({ date, previousDate = null, timeZone = "UTC" }: DateTimeProps) => { +export const SmartDateTime = ({ date, previousDate = null, timeZone = "UTC", hour12 = true }: DateTimeProps) => { const locales = useLocales(); const realDate = typeof date === "string" ? new Date(date) : date; const realPrevDate = previousDate @@ -134,8 +138,8 @@ export const SmartDateTime = ({ date, previousDate = null, timeZone = "UTC" }: D : null; // Initial formatted values - const initialTimeOnly = formatTimeOnly(realDate, timeZone, locales); - const initialWithDate = formatSmartDateTime(realDate, timeZone, locales); + const initialTimeOnly = formatTimeOnly(realDate, timeZone, locales, hour12); + const initialWithDate = formatSmartDateTime(realDate, timeZone, locales, hour12); // State for the formatted time const [formattedDateTime, setFormattedDateTime] = useState( @@ -152,10 +156,10 @@ export const SmartDateTime = ({ date, previousDate = null, timeZone = "UTC" }: D // Format with appropriate function setFormattedDateTime( showDatePart - ? formatSmartDateTime(realDate, userTimeZone, locales) - : formatTimeOnly(realDate, userTimeZone, locales) + ? formatSmartDateTime(realDate, userTimeZone, locales, hour12) + : formatTimeOnly(realDate, userTimeZone, locales, hour12) ); - }, [locales, realDate, realPrevDate]); + }, [locales, realDate, realPrevDate, hour12]); return {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}; }; @@ -170,7 +174,7 @@ function isSameDay(date1: Date, date2: Date): boolean { } // Format with date and time -function formatSmartDateTime(date: Date, timeZone: string, locales: string[]): string { +function formatSmartDateTime(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string { return new Intl.DateTimeFormat(locales, { month: "short", day: "numeric", @@ -180,12 +184,12 @@ function formatSmartDateTime(date: Date, timeZone: string, locales: string[]): s timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, - hour12: false, + hour12, }).format(date); } // Format time only -function formatTimeOnly(date: Date, timeZone: string, locales: string[]): string { +function formatTimeOnly(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string { return new Intl.DateTimeFormat(locales, { hour: "2-digit", minute: "numeric", @@ -193,7 +197,7 @@ function formatTimeOnly(date: Date, timeZone: string, locales: string[]): string timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, - hour12: false, + hour12, }).format(date); } @@ -203,6 +207,7 @@ export const DateTimeAccurate = ({ previousDate = null, showTooltip = true, hideDate = false, + hour12 = true, }: DateTimeProps) => { const locales = useLocales(); const [localTimeZone, setLocalTimeZone] = useState("UTC"); @@ -220,12 +225,12 @@ export const DateTimeAccurate = ({ // Smart formatting based on whether date changed const formattedDateTime = hideDate - ? formatTimeOnly(realDate, localTimeZone, locales) + ? formatTimeOnly(realDate, localTimeZone, locales, hour12) : realPrevDate ? isSameDay(realDate, realPrevDate) - ? formatTimeOnly(realDate, localTimeZone, locales) - : formatDateTimeAccurate(realDate, localTimeZone, locales) - : formatDateTimeAccurate(realDate, localTimeZone, locales); + ? formatTimeOnly(realDate, localTimeZone, locales, hour12) + : formatDateTimeAccurate(realDate, localTimeZone, locales, hour12) + : formatDateTimeAccurate(realDate, localTimeZone, locales, hour12); if (!showTooltip) return {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}; @@ -248,7 +253,7 @@ export const DateTimeAccurate = ({ ); }; -function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]): string { +function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string { const formattedDateTime = new Intl.DateTimeFormat(locales, { month: "short", day: "numeric", @@ -258,27 +263,27 @@ function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[]) timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, - hour12: false, + hour12, }).format(date); return formattedDateTime; } -export const DateTimeShort = ({ date, timeZone = "UTC" }: DateTimeProps) => { +export const DateTimeShort = ({ date, timeZone = "UTC", hour12 = true }: DateTimeProps) => { const locales = useLocales(); const realDate = typeof date === "string" ? new Date(date) : date; - const initialFormattedDateTime = formatDateTimeShort(realDate, timeZone, locales); + const initialFormattedDateTime = formatDateTimeShort(realDate, timeZone, locales, hour12); const [formattedDateTime, setFormattedDateTime] = useState(initialFormattedDateTime); useEffect(() => { const resolvedOptions = Intl.DateTimeFormat().resolvedOptions(); - setFormattedDateTime(formatDateTimeShort(realDate, resolvedOptions.timeZone, locales)); - }, [locales, realDate]); + setFormattedDateTime(formatDateTimeShort(realDate, resolvedOptions.timeZone, locales, hour12)); + }, [locales, realDate, hour12]); return {formattedDateTime.replace(/\s/g, String.fromCharCode(32))}; }; -function formatDateTimeShort(date: Date, timeZone: string, locales: string[]): string { +function formatDateTimeShort(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string { const formattedDateTime = new Intl.DateTimeFormat(locales, { hour: "numeric", minute: "numeric", @@ -286,7 +291,7 @@ function formatDateTimeShort(date: Date, timeZone: string, locales: string[]): s timeZone, // @ts-ignore fractionalSecondDigits works in most modern browsers fractionalSecondDigits: 3, - hour12: false, + hour12, }).format(date); return formattedDateTime; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index 3d5d4ec0e4..9c5e9ec9fe 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -584,7 +584,7 @@ function LogsDisplay({ log.level === "info" && "text-text-dimmed" )} > - +