fix: address review findings in the v2 stack fix PR
Board scaling: no sideways scrolling - Revert the pannable narrow canvas. The canvas fits the viewport again and `.viewport` goes back to `overflow: clip`, so a board can never widen the document or scroll horizontally. - Fix the unreadable-narrow-board regression at the real cause instead: cap the inverse UI compensation at 2x (50% canvas scale). Down to 50% text and controls keep their physical size; below it the board zooms out as a whole, so a 200x200 logical tile hosts at most 28px logical text rather than 96px and content stops overlapping its neighbours. Touch editing - Add `touch-action: none` to `.board-grid-entry`, the dnd-kit draggable and its own drag handle. The 200ms touch activation delay is useless if the browser has already handed the gesture to page panning and emitted `pointercancel`. Only edit mode renders this element. Custom widgets - Memoize the merged `data`/`status`/`options` passed to `CustomJsxRenderer`. They were rebuilt every render, so the compiled-template memo never hit and the new reference-identity boundary key changed on every render, resetting the error boundary and forcing an extra reconciliation of the widget subtree. - Keep the cache key when the shared Redis cache is unavailable so the process-local cache still absorbs polling instead of sending every widget refresh upstream. - Reclaim the shared cache's LRU/size/byte bookkeeping for TTL-expired entries and touch the LRU in the same script, so expired ghosts stop consuming the 64 MiB budget and a read costs one round trip instead of two. Forwarded IP - Restore `ipAddressFromHeaders` to the full forwarding chain and add `trustedIpAddressFromHeaders` for the last hop. Repointing the shared helper had collapsed MCP failed-auth rate limiting into a single bucket for every client behind a reverse proxy and replaced the client IP with the proxy IP in API-key auth logs. Runtime and restore - Make the new /appdata ownership migration best-effort. Under `set -e` a mount that rejects chown (CIFS/SMB, fixed-uid NFS) turned into a container that refuses to start, and /appdata was never chowned before. - Report malformed metadata.json as such instead of blaming the archive.
This commit is contained in:
1 parent
496fa60b5f
commit
1b544f599c
10 files changed
+153
-69
No files matched your search
@@ -25,6 +25,32 @@ end
|
||||
return #KEYS
|
||||
`;
|
||||
|
||||
// Reading has to touch the LRU on a hit and reclaim the byte/LRU/size
|
||||
// bookkeeping on a miss: entry keys carry their own PX expiry, so a TTL-expired
|
||||
// response would otherwise keep occupying a slot and its bytes forever and
|
||||
// shrink the usable cache to nothing over a long uptime.
|
||||
const READ_SCRIPT = `
|
||||
local value = redis.call('GET', KEYS[1])
|
||||
if value then
|
||||
redis.call('ZADD', KEYS[2], ARGV[1], KEYS[1])
|
||||
return value
|
||||
end
|
||||
local size = tonumber(redis.call('HGET', KEYS[3], KEYS[1]) or '0')
|
||||
if size > 0 then redis.call('DECRBY', KEYS[4], size) end
|
||||
redis.call('ZREM', KEYS[2], KEYS[1])
|
||||
redis.call('HDEL', KEYS[3], KEYS[1])
|
||||
return false
|
||||
`;
|
||||
|
||||
const FORGET_SCRIPT = `
|
||||
local size = tonumber(redis.call('HGET', KEYS[3], KEYS[1]) or '0')
|
||||
if size > 0 then redis.call('DECRBY', KEYS[4], size) end
|
||||
redis.call('DEL', KEYS[1])
|
||||
redis.call('ZREM', KEYS[2], KEYS[1])
|
||||
redis.call('HDEL', KEYS[3], KEYS[1])
|
||||
return 1
|
||||
`;
|
||||
|
||||
const STORE_SCRIPT = `
|
||||
local previousSize = tonumber(redis.call('HGET', KEYS[2], ARGV[1]) or '0')
|
||||
if previousSize > 0 then redis.call('DECRBY', KEYS[3], previousSize) end
|
||||
@@ -80,13 +106,14 @@ const getEntryKey = (generation: string, cacheKey: string) => {
|
||||
};
|
||||
|
||||
const readCachedResponse = async (client: RedisClient, entryKey: string) => {
|
||||
const serialized = await client.get(entryKey);
|
||||
const serialized = (await client.eval(READ_SCRIPT, 4, entryKey, LRU_KEY, SIZE_KEY, TOTAL_BYTES_KEY, Date.now())) as
|
||||
| string
|
||||
| null;
|
||||
if (serialized === null) return undefined;
|
||||
await client.zadd(LRU_KEY, Date.now(), entryKey);
|
||||
try {
|
||||
return JSON.parse(serialized) as CustomWidgetHttpResponse;
|
||||
} catch {
|
||||
await client.del(entryKey);
|
||||
await client.eval(FORGET_SCRIPT, 4, entryKey, LRU_KEY, SIZE_KEY, TOTAL_BYTES_KEY);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
@@ -136,7 +163,10 @@ export async function executeWithCustomWidgetResponseCache(
|
||||
logger.error("Shared custom widget response cache is unavailable", {
|
||||
errorName: error instanceof Error ? error.name : "UnknownError",
|
||||
});
|
||||
return execute({ ...input, cacheKey: undefined });
|
||||
// Keep the cache key so the caller's process-local cache still absorbs
|
||||
// polling while Redis is down. Dropping it would send every refresh of
|
||||
// every placed widget straight to the upstream API.
|
||||
return execute(input);
|
||||
}
|
||||
|
||||
const pendingKey = `${generation}:${cacheKey}`;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ZodError } from "zod/v4";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { extractBaseUrlFromHeaders, FlattenError } from "@homarr/common";
|
||||
import { ipAddressFromHeaders, userAgent } from "@homarr/common/server";
|
||||
import { trustedIpAddressFromHeaders, userAgent } from "@homarr/common/server";
|
||||
import type { DeviceType } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { db } from "@homarr/db";
|
||||
@@ -43,6 +43,7 @@ interface ApiContext {
|
||||
deviceType: DeviceType;
|
||||
baseUrl?: `${string}://${string}`;
|
||||
onboardingClaimToken?: string;
|
||||
/** Trusted transport peer, safe to key rate limits and quotas on. */
|
||||
ipAddress?: string;
|
||||
db: typeof db;
|
||||
}
|
||||
@@ -61,7 +62,7 @@ export const createTRPCContext = (opts: { headers: Headers; session: Session | n
|
||||
session,
|
||||
deviceType: userAgent(opts.headers).device.type,
|
||||
baseUrl: extractBaseUrlFromHeaders(opts.headers),
|
||||
ipAddress: ipAddressFromHeaders(opts.headers) ?? undefined,
|
||||
ipAddress: trustedIpAddressFromHeaders(opts.headers) ?? undefined,
|
||||
onboardingClaimToken: getOnboardingClaimTokenFromCookieHeader(opts.headers.get("cookie")),
|
||||
db,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user