Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57e18f6ce6 | ||
|
|
29369e23c1 | ||
|
|
687edb9226 | ||
|
|
ba860aacaa | ||
|
|
161dafbdb8 | ||
|
|
a382c75968 | ||
|
|
6f47a05cc2 | ||
|
|
d8d5894b28 | ||
|
|
46ae756d15 | ||
|
|
8b63799f1b | ||
|
|
60a69e31ef | ||
|
|
57ef0af4a2 | ||
|
|
597b5fbc8d | ||
|
|
5412577519 |
No files matched your search
@@ -1,3 +1,15 @@
|
||||
## [1.69.2](https://github.com/homarr-labs/homarr/compare/v1.69.1...v1.69.2) (2026-07-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **layout:** raise AppShellHeader z-index above Indicator dots ([687edb9](https://github.com/homarr-labs/homarr/commit/687edb9226307cb83986097abb24fe7f672e1778)), closes [#6194](https://github.com/homarr-labs/homarr/issues/6194)
|
||||
|
||||
## [1.69.1](https://github.com/homarr-labs/homarr/compare/v1.69.0...v1.69.1) (2026-07-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* unblock query cache mutations in demo mode and add cache buster ([#6176](https://github.com/homarr-labs/homarr/issues/6176)) ([a382c75](https://github.com/homarr-labs/homarr/commit/a382c75968e73bafff55b5890b1e95f0779b50d3))
|
||||
|
||||
## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { AppRouter } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import {
|
||||
isPersistableWidgetQueryKey,
|
||||
queryCacheBuster,
|
||||
queryCacheDefaultGcTimeMs,
|
||||
queryCacheDefaultRefetchIntervalMs,
|
||||
queryCacheDefaultStaleTimeMs,
|
||||
@@ -136,6 +137,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
|
||||
client={queryClient}
|
||||
persistOptions={{
|
||||
persister,
|
||||
buster: queryCacheBuster,
|
||||
maxAge: queryCacheDefaultGcTimeMs,
|
||||
dehydrateOptions: {
|
||||
shouldDehydrateQuery: (query) =>
|
||||
|
||||
@@ -7,6 +7,8 @@ import "~/styles/gridstack.scss";
|
||||
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import type { PersistedClient } from "@tanstack/react-query-persist-client";
|
||||
import superjson from "superjson";
|
||||
import { queryCacheBuster } from "@homarr/api/query-cache";
|
||||
import { getQueryClient } from "@homarr/api/server";
|
||||
import { IntegrationProvider } from "@homarr/auth/client";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
@@ -121,7 +123,8 @@ async function QueryCacheHydration({ userId, boardId }: { userId: string; boardI
|
||||
const serialized = await getQueryCacheAsync(userId, boardId);
|
||||
if (!serialized) return null;
|
||||
|
||||
const persisted = JSON.parse(serialized) as PersistedClient;
|
||||
const persisted = superjson.parse<PersistedClient>(serialized);
|
||||
if (persisted.buster !== queryCacheBuster) return null;
|
||||
if (!persisted?.clientState?.queries?.length) return null;
|
||||
|
||||
return <HydrationBoundary state={persisted.clientState} />;
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
|
||||
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
||||
return (
|
||||
<AppShellHeader maw="100vw" style={{ overflowX: "hidden" }}>
|
||||
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
|
||||
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
|
||||
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
||||
{hasNavigation && <ClientBurger />}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.69.0",
|
||||
"version": "1.69.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
|
||||
@@ -5,6 +5,7 @@ export const queryCacheDefaultRefetchIntervalMs = 30_000;
|
||||
export const queryCacheDefaultGcTimeMs = 1000 * 60 * 60 * 24;
|
||||
export const queryCacheMaxValueBytes = 1024 * 1024;
|
||||
export const queryCacheStoragePrefix = "homarr-widget-query";
|
||||
export const queryCacheBuster = "v2-superjson";
|
||||
|
||||
let activeQueryCacheBoardId: string | null = null;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { boards } from "@homarr/db/schema";
|
||||
import { removeQueryCacheAsync, setQueryCacheAsync } from "@homarr/redis";
|
||||
|
||||
import { queryCacheDefaultGcTimeMs, queryCacheMaxValueBytes } from "../query-cache";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
import { createTRPCRouter, internalProcedure } from "../trpc";
|
||||
import { throwIfActionForbiddenAsync } from "./board/board-access";
|
||||
|
||||
const queryCacheBoardInput = z.object({
|
||||
@@ -21,7 +21,7 @@ const validateBoardAccessAsync = async (ctx: Parameters<typeof throwIfActionForb
|
||||
};
|
||||
|
||||
export const queryCacheRouter = createTRPCRouter({
|
||||
setItem: publicProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
|
||||
setItem: internalProcedure.input(queryCacheSetInput).mutation(async ({ ctx, input }) => {
|
||||
await validateBoardAccessAsync(ctx, input.boardId);
|
||||
const userId = ctx.session?.user.id ?? "anonymous";
|
||||
return {
|
||||
@@ -34,7 +34,7 @@ export const queryCacheRouter = createTRPCRouter({
|
||||
),
|
||||
};
|
||||
}),
|
||||
removeItem: publicProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
|
||||
removeItem: internalProcedure.input(queryCacheBoardInput).mutation(async ({ ctx, input }) => {
|
||||
await validateBoardAccessAsync(ctx, input.boardId);
|
||||
const userId = ctx.session?.user.id ?? "anonymous";
|
||||
await removeQueryCacheAsync(userId, input.boardId);
|
||||
|
||||
@@ -116,6 +116,8 @@ const baseProcedure = env.DEMO_MODE ? t.procedure.use(enforceDemoModeReadOnly) :
|
||||
*/
|
||||
export const publicProcedure = baseProcedure;
|
||||
|
||||
export const internalProcedure = t.procedure;
|
||||
|
||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session?.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
@@ -6,12 +6,9 @@ interface Options<TData, TInput extends Record<string, unknown>> {
|
||||
type CacheEntry<TData> = { data: TData; timestamp: Date; expiresAt: number };
|
||||
|
||||
const MAX_CACHE_SIZE = 1000;
|
||||
const cache = new Map<string, CacheEntry<unknown>>();
|
||||
const inflight = new Map<string, Promise<CacheEntry<unknown>>>();
|
||||
|
||||
const DEFAULT_TTL_MS = 10_000;
|
||||
|
||||
const evictExpired = () => {
|
||||
const evictExpired = <TData>(cache: Map<string, CacheEntry<TData>>) => {
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of cache) {
|
||||
if (now >= entry.expiresAt) cache.delete(key);
|
||||
@@ -20,41 +17,46 @@ const evictExpired = () => {
|
||||
|
||||
export const createRequestHandler = <TData, TInput extends Record<string, unknown>>(
|
||||
options: Options<TData, TInput>,
|
||||
) => ({
|
||||
handler: (input: TInput) => ({
|
||||
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
||||
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
||||
const key = JSON.stringify(input);
|
||||
) => {
|
||||
const cache = new Map<string, CacheEntry<TData>>();
|
||||
const inflight = new Map<string, Promise<CacheEntry<TData>>>();
|
||||
|
||||
const cached = cache.get(key) as CacheEntry<TData> | undefined;
|
||||
if (cached && Date.now() < cached.expiresAt) {
|
||||
return { data: cached.data, timestamp: cached.timestamp };
|
||||
}
|
||||
if (cached) cache.delete(key);
|
||||
return {
|
||||
handler: (input: TInput) => ({
|
||||
async getDataAsync(): Promise<{ data: TData; timestamp: Date }> {
|
||||
const ttl = options.cacheTtlMs ?? DEFAULT_TTL_MS;
|
||||
const key = JSON.stringify(input);
|
||||
|
||||
const existing = inflight.get(key);
|
||||
if (existing) return existing as Promise<CacheEntry<TData>>;
|
||||
const cached = cache.get(key);
|
||||
if (cached && Date.now() < cached.expiresAt) {
|
||||
return { data: cached.data, timestamp: cached.timestamp };
|
||||
}
|
||||
if (cached) cache.delete(key);
|
||||
|
||||
const promise = options
|
||||
.requestAsync(input)
|
||||
.then((data) => {
|
||||
if (cache.size >= MAX_CACHE_SIZE) evictExpired();
|
||||
if (cache.size >= MAX_CACHE_SIZE) {
|
||||
const oldest = cache.keys().next().value;
|
||||
if (oldest) cache.delete(oldest);
|
||||
}
|
||||
const entry: CacheEntry<TData> = { data, timestamp: new Date(), expiresAt: Date.now() + ttl };
|
||||
cache.set(key, entry as CacheEntry<unknown>);
|
||||
inflight.delete(key);
|
||||
return entry;
|
||||
})
|
||||
.catch((err) => {
|
||||
inflight.delete(key);
|
||||
throw err;
|
||||
});
|
||||
const existing = inflight.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
inflight.set(key, promise as Promise<CacheEntry<unknown>>);
|
||||
return promise;
|
||||
},
|
||||
}),
|
||||
});
|
||||
const promise = options
|
||||
.requestAsync(input)
|
||||
.then((data) => {
|
||||
if (cache.size >= MAX_CACHE_SIZE) evictExpired(cache);
|
||||
if (cache.size >= MAX_CACHE_SIZE) {
|
||||
const oldest = cache.keys().next().value;
|
||||
if (oldest) cache.delete(oldest);
|
||||
}
|
||||
const entry: CacheEntry<TData> = { data, timestamp: new Date(), expiresAt: Date.now() + ttl };
|
||||
cache.set(key, entry);
|
||||
inflight.delete(key);
|
||||
return entry;
|
||||
})
|
||||
.catch((err) => {
|
||||
inflight.delete(key);
|
||||
throw err;
|
||||
});
|
||||
|
||||
inflight.set(key, promise);
|
||||
return promise;
|
||||
},
|
||||
}),
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user