feat: polish release v2 experience (#6800)
This commit is contained in:
1 parent
925b0fb801
commit
a4a903d7c3
295 files changed
+10627
-7115
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import { isRecord } from "@homarr/common";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
|
||||
const generationTelemetryCache = new Map<string, { expiresAt: number; value: OpenRouterGenerationTelemetry }>();
|
||||
@@ -31,8 +32,7 @@ export type OpenRouterGenerationTelemetry = {
|
||||
cancelled?: boolean;
|
||||
};
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> | null =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
|
||||
const asRecord = (value: unknown): Record<string, unknown> | null => (isRecord(value) ? value : null);
|
||||
|
||||
const asFiniteNonNegativeNumber = (value: unknown) => {
|
||||
const number = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { and, eq } from "@homarr/db";
|
||||
import { items } from "@homarr/db/schema";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
|
||||
import { publicProcedure } from "../trpc";
|
||||
|
||||
export const createOneItemMiddleware = (kind: WidgetKind) => {
|
||||
return publicProcedure.input(z.object({ itemId: z.string() })).use(async ({ input, ctx, next }) => {
|
||||
const item = await ctx.db.query.items.findFirst({
|
||||
where: and(eq(items.id, input.itemId), eq(items.kind, kind)),
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: `Item with id ${input.itemId} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
item,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { parse, stringify } from "superjson";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { constructIntegrationPermissions } from "@homarr/auth/shared";
|
||||
import { createId } from "@homarr/common";
|
||||
import { createId, isRecord } from "@homarr/common";
|
||||
import { decryptSecret, encryptSecret } from "@homarr/common/server";
|
||||
import { and, asc, desc, eq, handleTransactionsAsync, inArray } from "@homarr/db";
|
||||
import type { Database } from "@homarr/db";
|
||||
@@ -175,20 +175,20 @@ const getProviderHeaders = (configuration: AssistantConfiguration) => {
|
||||
};
|
||||
|
||||
const getGenerationIdsFromMessageContent = (content: unknown) => {
|
||||
if (!content || typeof content !== "object" || Array.isArray(content)) return [];
|
||||
const metadata = (content as { metadata?: unknown }).metadata;
|
||||
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return [];
|
||||
const custom = (metadata as { custom?: unknown }).custom;
|
||||
if (!custom || typeof custom !== "object" || Array.isArray(custom)) return [];
|
||||
const telemetry = (custom as { telemetry?: unknown }).telemetry;
|
||||
if (!telemetry || typeof telemetry !== "object" || Array.isArray(telemetry)) return [];
|
||||
const steps = (telemetry as { steps?: unknown }).steps;
|
||||
if (!isRecord(content)) return [];
|
||||
const metadata = content.metadata;
|
||||
if (!isRecord(metadata)) return [];
|
||||
const custom = metadata.custom;
|
||||
if (!isRecord(custom)) return [];
|
||||
const telemetry = custom.telemetry;
|
||||
if (!isRecord(telemetry)) return [];
|
||||
const steps = telemetry.steps;
|
||||
if (!Array.isArray(steps)) return [];
|
||||
return steps
|
||||
.flatMap((step) => {
|
||||
if (!step || typeof step !== "object" || Array.isArray(step)) return [];
|
||||
const generationId = (step as { generationId?: unknown }).generationId;
|
||||
const accessToken = (step as { generationAccessToken?: unknown }).generationAccessToken;
|
||||
if (!isRecord(step)) return [];
|
||||
const generationId = step.generationId;
|
||||
const accessToken = step.generationAccessToken;
|
||||
return typeof generationId === "string" &&
|
||||
/^gen-[A-Za-z0-9_-]{1,128}$/.test(generationId) &&
|
||||
typeof accessToken === "string" &&
|
||||
@@ -541,16 +541,10 @@ const addFeedbackToMessageContent = (serializedContent: string, type: "positive"
|
||||
} catch {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "The stored message is invalid." });
|
||||
}
|
||||
if (!content || typeof content !== "object" || Array.isArray(content)) {
|
||||
if (!isRecord(content)) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "The stored message is invalid." });
|
||||
}
|
||||
const metadata =
|
||||
"metadata" in content &&
|
||||
content.metadata &&
|
||||
typeof content.metadata === "object" &&
|
||||
!Array.isArray(content.metadata)
|
||||
? content.metadata
|
||||
: {};
|
||||
const metadata = isRecord(content.metadata) ? content.metadata : {};
|
||||
return stringify({
|
||||
...content,
|
||||
metadata: { ...metadata, submittedFeedback: { type } },
|
||||
|
||||
@@ -2,6 +2,8 @@ import { isDeepStrictEqual } from "node:util";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { isRecord } from "@homarr/common";
|
||||
|
||||
interface BoardPlacement {
|
||||
id: string;
|
||||
kind: string;
|
||||
@@ -70,12 +72,7 @@ export const throwIfCustomWidgetBoardDuplicationForbidden = (
|
||||
function normalizeCustomWidgetOptions(options: Record<string, unknown>) {
|
||||
return {
|
||||
...options,
|
||||
configuration:
|
||||
options.configuration !== null &&
|
||||
typeof options.configuration === "object" &&
|
||||
!Array.isArray(options.configuration)
|
||||
? options.configuration
|
||||
: {},
|
||||
configuration: isRecord(options.configuration) ? options.configuration : {},
|
||||
configurationVersion:
|
||||
typeof options.configurationVersion === "number" && Number.isInteger(options.configurationVersion)
|
||||
? options.configurationVersion
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { legacyCustomWidgetDefinitions } from "@homarr/db/schema";
|
||||
import { buildCustomWidgetAiPrompt } from "@homarr/custom-widgets/authoring-prompt";
|
||||
import { isRecord } from "@homarr/common";
|
||||
|
||||
type LegacyCustomWidgetDefinition = typeof legacyCustomWidgetDefinitions.$inferSelect;
|
||||
|
||||
@@ -65,7 +66,7 @@ function redactLegacyRequestBody(value: string | null) {
|
||||
|
||||
function redactAllValues(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(redactAllValues);
|
||||
if (value !== null && typeof value === "object")
|
||||
if (isRecord(value))
|
||||
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, redactAllValues(child)]));
|
||||
if (value === null) return null;
|
||||
return "[REDACTED VALUE]";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { isRecord } from "@homarr/common";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { customWidgetDefinitionSchema } from "@homarr/custom-widgets/core";
|
||||
import { and, eq } from "@homarr/db";
|
||||
@@ -143,7 +144,7 @@ export const templateProcedures = {
|
||||
|
||||
function getAffectedRowCount(result: unknown): number {
|
||||
if (Array.isArray(result)) return getAffectedRowCount(result[0]);
|
||||
if (!result || typeof result !== "object") return 0;
|
||||
if (!isRecord(result)) return 0;
|
||||
if ("affectedRows" in result && typeof result.affectedRows === "number") return result.affectedRows;
|
||||
if ("rowCount" in result && typeof result.rowCount === "number") return result.rowCount;
|
||||
if ("changes" in result && typeof result.changes === "number") return result.changes;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getHeaderItems,
|
||||
headerPreferencesMutationSchema,
|
||||
parseHeaderPreferences,
|
||||
userByteUnitSystemSchema,
|
||||
userChangeColorSchemeSchema,
|
||||
userChangeHomeBoardsSchema,
|
||||
userChangePasswordApiSchema,
|
||||
@@ -212,7 +213,7 @@ export const userRouter = createTRPCRouter({
|
||||
image: z
|
||||
.string()
|
||||
.regex(/^data:image\/(png|jpeg|gif|webp);base64,[A-Za-z0-9/+]+=*$/g)
|
||||
.max(350000) // approximately 256KB in base64 (256 * 1024 * 4 / 3 + prefixes)
|
||||
.max(350000) // approximately 256 KiB in base64 (256 * 1024 * 4 / 3 + prefixes)
|
||||
.nullable(),
|
||||
}),
|
||||
)
|
||||
@@ -370,6 +371,7 @@ export const userRouter = createTRPCRouter({
|
||||
provider: true,
|
||||
homeBoardId: true,
|
||||
mobileHomeBoardId: true,
|
||||
byteUnitSystem: true,
|
||||
firstDayOfWeek: true,
|
||||
pingIconsEnabled: true,
|
||||
enableRightClickOnWidgets: true,
|
||||
@@ -408,6 +410,7 @@ export const userRouter = createTRPCRouter({
|
||||
provider: true,
|
||||
homeBoardId: true,
|
||||
mobileHomeBoardId: true,
|
||||
byteUnitSystem: true,
|
||||
firstDayOfWeek: true,
|
||||
pingIconsEnabled: true,
|
||||
enableRightClickOnWidgets: true,
|
||||
@@ -682,6 +685,38 @@ export const userRouter = createTRPCRouter({
|
||||
})
|
||||
.where(eq(users.id, ctx.session.user.id));
|
||||
}),
|
||||
changeByteUnitSystem: protectedProcedure
|
||||
.input(userByteUnitSystemSchema.and(byIdSchema))
|
||||
.output(z.void())
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
const dbUser = await ctx.db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
where: eq(users.id, input.id),
|
||||
});
|
||||
|
||||
if (!dbUser) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db
|
||||
.update(users)
|
||||
.set({
|
||||
byteUnitSystem: input.byteUnitSystem,
|
||||
})
|
||||
.where(eq(users.id, input.id));
|
||||
}),
|
||||
changeEnableRightClickOnWidgets: protectedProcedure
|
||||
.meta({
|
||||
openapi: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { isRecord } from "@homarr/common";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { mockWidgetData } from "@homarr/integrations";
|
||||
@@ -26,7 +27,7 @@ const assertMockNoteExists = (noteId: string) => {
|
||||
const isJsonDeltaString = (value: string) => {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(value);
|
||||
return typeof parsed === "object" && parsed !== null && Array.isArray((parsed as { ops?: unknown }).ops);
|
||||
return isRecord(parsed) && Array.isArray(parsed.ops);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server";
|
||||
import { parse as parseSuperJson } from "superjson";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { isRecord } from "@homarr/common";
|
||||
import { decryptSecret } from "@homarr/common/server";
|
||||
import { eq } from "@homarr/db";
|
||||
import { boards, customWidgetDefinitions, items, legacyCustomWidgetDefinitions } from "@homarr/db/schema";
|
||||
@@ -49,12 +50,7 @@ const parseItemOptions = (raw: string): CustomWidgetItemOptions => {
|
||||
if (typeof options.definitionId !== "string" || options.definitionId.length === 0) throw new Error();
|
||||
return {
|
||||
definitionId: options.definitionId,
|
||||
configuration:
|
||||
options.configuration !== null &&
|
||||
typeof options.configuration === "object" &&
|
||||
!Array.isArray(options.configuration)
|
||||
? (options.configuration as Record<string, unknown>)
|
||||
: {},
|
||||
configuration: isRecord(options.configuration) ? options.configuration : {},
|
||||
configurationVersion:
|
||||
typeof options.configurationVersion === "number" && Number.isInteger(options.configurationVersion)
|
||||
? options.configurationVersion
|
||||
|
||||
@@ -10,6 +10,7 @@ export const getRscUserSettingsAsync = cache(async (userId: string) =>
|
||||
columns: {
|
||||
homeBoardId: true,
|
||||
mobileHomeBoardId: true,
|
||||
byteUnitSystem: true,
|
||||
firstDayOfWeek: true,
|
||||
pingIconsEnabled: true,
|
||||
enableRightClickOnWidgets: true,
|
||||
|
||||
Reference in new issue
Block a user