@@ -221,6 +221,7 @@ export default function Page() {
isAdmin={isAdmin}
showDebug={showDebug}
defaultPeriod={defaultPeriod}
+ retentionLimitDays={retentionLimitDays}
/>
@@ -237,6 +238,7 @@ export default function Page() {
isAdmin={isAdmin}
showDebug={showDebug}
defaultPeriod={defaultPeriod}
+ retentionLimitDays={retentionLimitDays}
/>
-
- Showing last {retentionDays} {retentionDays === 1 ? 'day' : 'days'}
-
-
- Upgrade
-
-
- );
-}
-
function FiltersBar({
list,
isAdmin,
showDebug,
defaultPeriod,
+ retentionLimitDays,
}: {
list?: Exclude["data"]>, { error: string }>;
isAdmin: boolean;
showDebug: boolean;
defaultPeriod?: string;
+ retentionLimitDays: number;
}) {
const location = useOptimisticLocation();
const searchParams = new URLSearchParams(location.search);
@@ -317,12 +297,16 @@ function FiltersBar({
<>
-
-
+
+
{hasFilters && (
)}
>
@@ -330,24 +314,22 @@ function FiltersBar({
<>
-
-
+
+
{hasFilters && (
)}
>
)}
- {list?.retention?.wasClamped && (
-
- )}
{isAdmin && (
(location.search);
+ // Track whether the current fetch is a "check for new" request vs "load more"
+ const isCheckingForNewRef = useRef(false);
// Clear accumulated logs immediately when filters change (for instant visual feedback)
useEffect(() => {
@@ -410,7 +394,7 @@ function LogsList({
}
}, [selectedLogId]);
- // Append new logs when fetcher completes (with deduplication)
+ // Append/prepend new logs when fetcher completes (with deduplication)
useEffect(() => {
if (fetcher.data && fetcher.state === "idle") {
// Ignore fetcher data if it was loaded for a different filter state
@@ -418,14 +402,25 @@ function LogsList({
return;
}
- const existingIds = new Set(accumulatedLogs.map((log) => log.id));
- const newLogs = fetcher.data.logs.filter((log) => !existingIds.has(log.id));
- if (newLogs.length > 0) {
- setAccumulatedLogs((prev) => [...prev, ...newLogs]);
+ if (isCheckingForNewRef.current) {
+ // "Check for new" - prepend new logs, don't update cursor
+ setAccumulatedLogs((prev) => {
+ const existingIds = new Set(prev.map((log) => log.id));
+ const newLogs = fetcher.data!.logs.filter((log) => !existingIds.has(log.id));
+ return newLogs.length > 0 ? [...newLogs, ...prev] : prev;
+ });
+ isCheckingForNewRef.current = false;
+ } else {
+ // "Load more" - append logs and update cursor
+ setAccumulatedLogs((prev) => {
+ const existingIds = new Set(prev.map((log) => log.id));
+ const newLogs = fetcher.data!.logs.filter((log) => !existingIds.has(log.id));
+ return newLogs.length > 0 ? [...prev, ...newLogs] : prev;
+ });
+ setNextCursor(fetcher.data.pagination.next);
}
- setNextCursor(fetcher.data.pagination.next);
}
- }, [fetcher.data, fetcher.state, accumulatedLogs, location.search]);
+ }, [fetcher.data, fetcher.state, location.search]);
// Build resource URL for loading more
const loadMoreUrl = useMemo(() => {
@@ -477,6 +472,18 @@ function LogsList({
updateUrlWithLog(undefined);
}, [updateUrlWithLog, startTransition]);
+ const handleCheckForMore = useCallback(() => {
+ if (fetcher.state !== "idle") return;
+ // Fetch without cursor to check for new logs
+ const resourcePath = `/resources${location.pathname}`;
+ const params = new URLSearchParams(location.search);
+ params.delete("cursor");
+ params.delete("log");
+ fetcherFilterStateRef.current = location.search;
+ isCheckingForNewRef.current = true;
+ fetcher.load(`${resourcePath}?${params.toString()}`);
+ }, [fetcher, location.pathname, location.search]);
+
return (
@@ -488,6 +495,7 @@ function LogsList({
isLoadingMore={fetcher.state === "loading"}
hasMore={!!nextCursor}
onLoadMore={handleLoadMore}
+ onCheckForMore={handleCheckForMore}
selectedLogId={selectedLogId}
onLogSelect={handleLogSelect}
/>
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
index 1ffd128b30..e02d29b95b 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
@@ -1822,7 +1822,7 @@ function PreviousRunButton({ to }: { to: string | null }) {
leadingIconClassName="size-3 group-hover/button:text-text-bright transition-colors"
className={cn("flex size-6 max-w-6 items-center", !to && "cursor-not-allowed opacity-50")}
onClick={(e) => !to && e.preventDefault()}
- shortcut={{ key: "[" }}
+ shortcut={{ key: "j" }}
tooltip="Previous Run"
disabled={!to}
replace
@@ -1841,7 +1841,7 @@ function NextRunButton({ to }: { to: string | null }) {
leadingIconClassName="size-3 group-hover/button:text-text-bright transition-colors"
className={cn("flex size-6 max-w-6 items-center", !to && "cursor-not-allowed opacity-50")}
onClick={(e) => !to && e.preventDefault()}
- shortcut={{ key: "]" }}
+ shortcut={{ key: "k" }}
tooltip="Next Run"
disabled={!to}
replace
diff --git a/apps/webapp/app/routes/resources.timezone.ts b/apps/webapp/app/routes/resources.timezone.ts
new file mode 100644
index 0000000000..f06b44e614
--- /dev/null
+++ b/apps/webapp/app/routes/resources.timezone.ts
@@ -0,0 +1,43 @@
+import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
+import { z } from "zod";
+import {
+ setTimezonePreference,
+ uiPreferencesStorage,
+} from "~/services/preferences/uiPreferences.server";
+
+const schema = z.object({
+ timezone: z.string().min(1).max(100),
+});
+
+// Cache the supported timezones to avoid repeated calls
+const supportedTimezones = new Set(Intl.supportedValuesOf("timeZone"));
+
+export async function action({ request }: ActionFunctionArgs) {
+ let data: unknown;
+ try {
+ data = await request.json();
+ } catch {
+ return json({ success: false, error: "Invalid JSON" }, { status: 400 });
+ }
+
+ const result = schema.safeParse(data);
+
+ if (!result.success) {
+ return json({ success: false, error: "Invalid timezone" }, { status: 400 });
+ }
+
+ if (!supportedTimezones.has(result.data.timezone)) {
+ return json({ success: false, error: "Invalid timezone" }, { status: 400 });
+ }
+
+ const session = await setTimezonePreference(result.data.timezone, request);
+
+ return json(
+ { success: true },
+ {
+ headers: {
+ "Set-Cookie": await uiPreferencesStorage.commitSession(session),
+ },
+ }
+ );
+}
diff --git a/apps/webapp/app/services/preferences/uiPreferences.server.ts b/apps/webapp/app/services/preferences/uiPreferences.server.ts
index 0d23a546c2..44282499db 100644
--- a/apps/webapp/app/services/preferences/uiPreferences.server.ts
+++ b/apps/webapp/app/services/preferences/uiPreferences.server.ts
@@ -42,3 +42,15 @@ export async function setRootOnlyFilterPreference(rootOnly: boolean, request: Re
session.set("rootOnly", rootOnly);
return session;
}
+
+export async function getTimezonePreference(request: Request): Promise {
+ const session = await getUiPreferencesSession(request);
+ const timezone = session.get("timezone");
+ return typeof timezone === "string" ? timezone : "UTC";
+}
+
+export async function setTimezonePreference(timezone: string, request: Request) {
+ const session = await getUiPreferencesSession(request);
+ session.set("timezone", timezone);
+ return session;
+}