Improvements: remove dead config, fix ESM, add URL validation, unify types, add missing logging
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m52s

- Remove .prettierrc (dead config, Biome replaces Prettier)
- Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat
- Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit
- Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts
- Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars
- Replace barrel export src/types/index.ts with direct @/types/common imports
- Make trustHost conditional (development only) in auth.ts
- Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations
- Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
This commit is contained in:
openhands committed 2026-07-26 20:28:11 +02:00
1 parent b922f6d49f
commit 17847545dd
292 files changed
+69195 -67915

No files matched your search

+4 -3
View File
@@ -97,11 +97,12 @@ export function AdminSectionTabs({ tabs }: { tabs: AdminTab[] }) {
return (
<div className="space-y-3">
{order.map((groupId) => {
const groupTabs = tabs.filter((tab) => (tab.group ?? "primary") === groupId);
const groupTabs = tabs.filter(
(tab) => (tab.group ?? "primary") === groupId,
);
const rowOffset = offset;
offset += groupTabs.length;
const label =
groupId === "tools" ? t("tabs.toolsGroup") : undefined;
const label = groupId === "tools" ? t("tabs.toolsGroup") : undefined;
return (
<TabRow
key={groupId}
+7 -3
View File
@@ -1,10 +1,10 @@
"use client";
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { slugify } from "@/lib/format";
import { MediaPicker } from "./media-picker";
import { RichText } from "./rich-text";
import { slugify } from "@/lib/format";
export function ArticleForm({
action,
@@ -30,7 +30,11 @@ export function ArticleForm({
const suggestedSlug = useMemo(() => slugify(title), [title]);
return (
<form action={action} className="admin-card" style={{ display: "grid", gap: "1rem" }}>
<form
action={action}
className="admin-card"
style={{ display: "grid", gap: "1rem" }}
>
<div className="grid gap-4 md:grid-cols-2">
<label className="block">
<span className="text-xs font-medium text-[var(--admin-text-muted)]">
@@ -15,8 +15,8 @@ import { RootTabs } from "./root-tabs";
import { SortableTree } from "./sortable-tree";
import { type CatalogStatsTotals, StatsBar } from "./stats-bar";
import {
CatalogManagerProvider,
type CatalogManagerMode,
CatalogManagerProvider,
catalogTreeQs,
useCatalogManager,
} from "./use-catalog-store";
@@ -58,8 +58,7 @@ export function CatalogManagerDialog({
};
}, [open]);
const title =
catalogType === "bc" ? "BC Visual Manager" : "Visual Manager";
const title = catalogType === "bc" ? "BC Visual Manager" : "Visual Manager";
return (
<>
@@ -120,9 +119,7 @@ function CatalogManagerInner({
const loadRootPages = useCallback(async () => {
dispatch({ type: "SET_LOADING_ROOT", loading: true });
try {
const res = await fetch(
`/api/admin/catalog/tree?parentId=-1${catQs}`,
);
const res = await fetch(`/api/admin/catalog/tree?parentId=-1${catQs}`);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Failed to load roots");
const pages: RootTab[] = (data.pages || []).map(
@@ -163,17 +160,21 @@ function CatalogManagerInner({
}
}
void loadRootPages();
}, [open, initialRoots, state.rootPages.length, state.activeTabId, dispatch, loadRootPages]);
}, [
open,
initialRoots,
state.rootPages.length,
state.activeTabId,
dispatch,
loadRootPages,
]);
useEffect(() => {
if (!open) dispatch({ type: "ON_DIALOG_CLOSE" });
}, [open, dispatch]);
const handleTabClick = (id: number) => {
if (
state.editorDirty &&
!confirm("You have unsaved changes. Discard?")
) {
if (state.editorDirty && !confirm("You have unsaved changes. Discard?")) {
return;
}
dispatch({ type: "SET_ACTIVE_TAB", id });
@@ -203,10 +204,7 @@ function CatalogManagerInner({
};
const handleSelectPage = (id: number) => {
if (
state.editorDirty &&
!confirm("You have unsaved changes. Discard?")
) {
if (state.editorDirty && !confirm("You have unsaved changes. Discard?")) {
return;
}
dispatch({ type: "SELECT_PAGE", id: id > 0 ? id : null });
@@ -216,10 +214,7 @@ function CatalogManagerInner({
if (state.activeTabId !== id) {
dispatch({ type: "SET_ACTIVE_TAB", id });
}
if (
state.editorDirty &&
!confirm("You have unsaved changes. Discard?")
) {
if (state.editorDirty && !confirm("You have unsaved changes. Discard?")) {
return;
}
dispatch({ type: "SELECT_PAGE", id });
@@ -150,9 +150,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
async (id: number) => {
setLoading(true);
try {
const res = await fetch(
`/api/admin/catalog/tree?pageId=${id}${catQs}`,
);
const res = await fetch(`/api/admin/catalog/tree?pageId=${id}${catQs}`);
if (!res.ok) throw new Error();
const data = await res.json();
const p = data.page as PageDetail;
@@ -653,9 +651,7 @@ export function InlineEditor({ pageId, onSaved }: InlineEditorProps) {
>
{catalogType === "bc" ? (
<div className="rounded-md border border-dashed px-4 py-8 text-center text-sm text-muted-foreground space-y-3">
<p>
BC items are managed on the full Builder Club page editor.
</p>
<p>BC items are managed on the full Builder Club page editor.</p>
<Link href={pageHref} target="_blank">
<Button variant="outline" size="sm" className="gap-1">
<ExternalLink className="h-3 w-3" />
@@ -30,8 +30,8 @@ import {
reorderBcTreePage,
toggleBcPage,
} from "@/actions/catalog-bc";
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
import { CatalogIcon } from "@/components/admin/catalog-tree";
import { useConfirmDialog } from "@/components/admin/confirm-dialog";
import { Button } from "@/components/ui/button";
import {
ContextMenu,
@@ -125,20 +125,23 @@ export function SortableTree({
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
);
const normalizePages = useCallback((pages: TreeNodeData[]): TreeNodeData[] => {
return pages.map((p) => ({
...p,
id: Number(p.id),
parentId: Number(p.parentId),
enabled: String(p.enabled ?? "0"),
visible: String(p.visible ?? "0"),
iconImage: Number(p.iconImage) || 0,
iconColor: Number(p.iconColor) || 0,
childCount: Number(p.childCount) || 0,
itemCount: Number(p.itemCount) || 0,
orderNum: Number(p.orderNum) || 0,
}));
}, []);
const normalizePages = useCallback(
(pages: TreeNodeData[]): TreeNodeData[] => {
return pages.map((p) => ({
...p,
id: Number(p.id),
parentId: Number(p.parentId),
enabled: String(p.enabled ?? "0"),
visible: String(p.visible ?? "0"),
iconImage: Number(p.iconImage) || 0,
iconColor: Number(p.iconColor) || 0,
childCount: Number(p.childCount) || 0,
itemCount: Number(p.itemCount) || 0,
orderNum: Number(p.orderNum) || 0,
}));
},
[],
);
/**
* Load children of a page — same endpoint CatalogTree uses successfully.
@@ -227,11 +230,7 @@ export function SortableTree({
}
const node = nodes.get(id);
const existing = childrenMap.get(id);
if (
node &&
node.childCount > 0 &&
(!existing || existing.length === 0)
) {
if (node && node.childCount > 0 && (!existing || existing.length === 0)) {
dispatch({
type: "SET_LOADING_IDS",
ids: new Set([...loadingIds, id]),
@@ -591,9 +590,7 @@ export function SortableTree({
const handleDuplicate = async (id: number) => {
if (!canEdit) return;
try {
const res = await fetch(
`/api/admin/catalog/tree?pageId=${id}${catQs}`,
);
const res = await fetch(`/api/admin/catalog/tree?pageId=${id}${catQs}`);
if (!res.ok) throw new Error();
const { page } = await res.json();
const result =
@@ -662,7 +659,7 @@ export function SortableTree({
// Lazy-load each first-level node's children in background
for (const id of firstLevel) {
const node = nodes.get(id);
if (node && node.childCount > 0 && !(childrenMap.get(id)?.length)) {
if (node && node.childCount > 0 && !childrenMap.get(id)?.length) {
void loadChildren(id)
.then((pages) => {
dispatch({ type: "SET_CHILDREN", parentId: id, children: pages });
@@ -1132,7 +1129,6 @@ export function SortableTree({
</Button>
</div>
)}
</div>
);
}
+2 -2
View File
@@ -50,9 +50,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { adminFetch } from "@/lib/admin-fetch";
import { translateCaption } from "@/lib/catalog-translations";
import { cn } from "@/lib/utils";
import { adminFetch } from "@/lib/admin-fetch";
/* ─── Types ──────────────────────────────────────────────── */
@@ -514,7 +514,6 @@ export function CatalogTree({
function TreeItem({
node,
level,
// biome-ignore lint/correctness/noUnusedFunctionParameters: part of the public component API
isSearchResult,
defaultExpanded,
}: {
@@ -523,6 +522,7 @@ function TreeItem({
isSearchResult?: boolean;
defaultExpanded?: boolean;
}) {
void isSearchResult;
const {
expandSignal,
collapseSignal,
+1 -1
View File
@@ -4,8 +4,8 @@ import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { modalContentVariants, modalOverlayVariants } from "@/lib/motion";
import { cn } from "@/lib/utils";
export type ConfirmDialogProps = {
open: boolean;
+5 -1
View File
@@ -27,7 +27,11 @@ export function DashboardChart({
)}
</CardHeader>
<CardContent>
<div className="flex h-[200px] items-end gap-1" aria-label={title} role="img">
<div
className="flex h-[200px] items-end gap-1"
aria-label={title}
role="img"
>
{data.map((point) => (
<div
key={point.label}
+1 -1
View File
@@ -40,7 +40,7 @@ import {
} from "@/components/ui/table";
import { useDebounce } from "@/hooks/use-debounce";
import { PER_PAGE_OPTIONS } from "@/lib/admin-helpers";
import type { DataTableColumn, PaginatedResult } from "@/types";
import type { DataTableColumn, PaginatedResult } from "@/types/common";
/**
* One-click filter preset. The chip applies/clears the listed query-string
+11 -10
View File
@@ -1,8 +1,8 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import { uploadMedia, deleteMedia } from "@/actions/admin-media";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { deleteMedia, uploadMedia } from "@/actions/admin-media";
import { formatDate } from "@/lib/format-date";
type MediaFile = {
@@ -222,13 +222,13 @@ export function AdminMediaGrid() {
setSelected(f.name);
}
}}
className="group border rounded-[12px] overflow-hidden bg-[var(--admin-canvas)] cursor-pointer transition-all duration-150"
style={{
borderColor: isSelected
? "var(--admin-accent)"
: "color-mix(in srgb, var(--admin-text-muted) 14%, transparent)",
}}
>
className="group border rounded-[12px] overflow-hidden bg-[var(--admin-canvas)] cursor-pointer transition-all duration-150"
style={{
borderColor: isSelected
? "var(--admin-accent)"
: "color-mix(in srgb, var(--admin-text-muted) 14%, transparent)",
}}
>
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
@@ -246,7 +246,8 @@ export function AdminMediaGrid() {
{f.name}
</p>
<p className="text-[11px] m-0 text-[var(--admin-text-muted)]">
{formatSize(f.size)} · {formatDate(new Date(f.uploaded), "date")}
{formatSize(f.size)} ·{" "}
{formatDate(new Date(f.uploaded), "date")}
</p>
<div className="flex gap-1.5 mt-2">
<button
+4 -1
View File
@@ -54,7 +54,10 @@ export function PeakHoursHeatmap({ data }: { data: PeakHoursData }) {
</div>
{/* Rows */}
{days.map((day, dayIdx) => (
<div key={DAY_KEYS[dayIdx]} className="flex items-center gap-px mb-px">
<div
key={DAY_KEYS[dayIdx]}
className="flex items-center gap-px mb-px"
>
<span className="w-10 text-xs text-muted-foreground text-right pr-2 shrink-0">
{day}
</span>