From 32a51c1e60cb3b6f7f8b87006ccf800487df7f0d Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 6 Sep 2026 20:36:04 +0200 Subject: [PATCH] feat: announce Homarr v2 beta (#6793) * feat: announce Homarr v2 beta * chore: refine v2 beta announcement * feat: expand v2 beta guidance * chore: refine v2 beta resources * chore: simplify v2 beta modal * chore: improve v2 beta feedback links * fix: keep v2 modal above header * chore: sharpen v2 beta message * fix: clear header for v2 modal --- .github/ISSUE_TEMPLATE/v2_beta_feedback.yml | 131 ++++++ apps/docs/docs/getting-started/index.mdx | 6 + .../app/[locale]/boards/_layout-creator.tsx | 41 +- .../backup/_components/backup-export-card.tsx | 69 +--- .../backup/backup-export-button.tsx | 79 ++++ apps/nextjs/src/components/backup/index.ts | 1 + apps/nextjs/src/components/layout/header.tsx | 44 +- apps/nextjs/src/components/layout/shell.tsx | 4 +- .../layout/v2-beta-announcement/constants.ts | 7 + .../v2-beta-announcement.module.css | 94 +++++ .../v2-beta-announcement.tsx | 377 ++++++++++++++++++ .../v2-beta-dashboard-shell.tsx | 42 ++ packages/definitions/src/cookie.ts | 1 + 13 files changed, 799 insertions(+), 97 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/v2_beta_feedback.yml create mode 100644 apps/nextjs/src/components/backup/backup-export-button.tsx create mode 100644 apps/nextjs/src/components/layout/v2-beta-announcement/constants.ts create mode 100644 apps/nextjs/src/components/layout/v2-beta-announcement/v2-beta-announcement.module.css create mode 100644 apps/nextjs/src/components/layout/v2-beta-announcement/v2-beta-announcement.tsx create mode 100644 apps/nextjs/src/components/layout/v2-beta-announcement/v2-beta-dashboard-shell.tsx diff --git a/.github/ISSUE_TEMPLATE/v2_beta_feedback.yml b/.github/ISSUE_TEMPLATE/v2_beta_feedback.yml new file mode 100644 index 000000000..5d0b5b38d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/v2_beta_feedback.yml @@ -0,0 +1,131 @@ +name: Homarr v2 beta feedback +description: Report a specific bug, unexpected behavior, or improvement for the Homarr v2 beta +title: "feedback(v2): " +labels: ["needs triage"] +body: + - type: markdown + attributes: + value: | + Thanks for testing Homarr v2. It will become the default when it is ready, and every detailed report helps us get there. + + We cannot guess what you wanted from “this looks wrong.” Tell us exactly which feature is affected, what happened, why it is wrong for your use case, and what you expected instead. Add screenshots or a short recording whenever possible. + + **Not useful:** “The board looks wrong.” + + **Useful:** “The container changes from four columns to three after I move a widget at 1440 × 900 and 100% browser zoom. I expected it to keep four columns.” + + - type: dropdown + id: feedback-type + attributes: + label: Feedback type + options: + - Bug + - Upgrade or migration + - Usability or design + - Performance + - Other + validations: + required: true + + - type: input + id: version + attributes: + label: Homarr v2 version or image tag + description: Copy the version from the About page or provide the exact Docker image tag you tested. + placeholder: ghcr.io/homarr-labs/homarr-test:v2 + validations: + required: true + + - type: input + id: affected-area + attributes: + label: Affected page, feature, or widget + description: Name the exact page, widget, integration, container, sidebar, or action involved. + placeholder: "Board editor: moving a widget into a container" + validations: + required: true + + - type: textarea + id: behavior + attributes: + label: What happened, why is it wrong, and what should happen instead? + description: Describe the actual result, why it does not work for you, and the exact result you expected. + placeholder: | + What happened: + Why this is wrong for my use case: + What I expected instead: + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Exact steps to reproduce + description: Start from the page you opened and list every click, keypress, drag, setting, and value needed to reproduce it. + placeholder: | + 1. Open... + 2. Set... + 3. Drag or click... + 4. Observe... + validations: + required: true + + - type: textarea + id: visual-evidence + attributes: + label: Screenshots or screen recordings + description: Drag screenshots or a recording into this field. Include the full page for context and a close-up of the problem. Blur secrets and personal data. If you cannot add visuals, explain why. + placeholder: Drag screenshots or a recording here, or explain why none are available. + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Device and Homarr environment + description: Provide every detail that could affect the result. + placeholder: | + Device type and model: + Operating system and version: + Browser and version: + Installation method (Docker, Unraid, Kubernetes, other): + Database (SQLite, MySQL, PostgreSQL): + Reverse proxy and version, if used: + validations: + required: true + + - type: textarea + id: board-context + attributes: + label: Board and display context + description: Include the layout details even when you are not sure they matter. + placeholder: | + Display resolution: + Browser zoom: + Desktop or mobile layout: + Board column count: + Approximate number of items: + Container or sidebar nesting: + Relevant board or widget settings: + validations: + required: true + + - type: textarea + id: additional-information + attributes: + label: Logs and additional information + description: Add relevant browser console errors, Homarr logs, integration details, or anything else that may help. Remove secrets and personal information first. + + - type: checkboxes + id: checks + attributes: + label: Before submitting + options: + - label: I added screenshots or a recording where possible, or explained why I could not. + required: true + - label: I described both what happened and the exact result I expected. + required: true + - label: I searched the existing issues for the same feedback. + required: true + - label: I removed passwords, API keys, tokens, and other private information. + required: true diff --git a/apps/docs/docs/getting-started/index.mdx b/apps/docs/docs/getting-started/index.mdx index 4dc986658..9efaa54bc 100644 --- a/apps/docs/docs/getting-started/index.mdx +++ b/apps/docs/docs/getting-started/index.mdx @@ -11,6 +11,12 @@ tags: import Link from '@docusaurus/Link'; import { IconArrowRight } from '@tabler/icons-react'; +:::info Homarr v2 beta is available + +We've been working on Homarr's v2 update for a while now. There is no fixed release date yet, it will be ready when it's ready. We **need your help** and feedback to help us become confident enough to release it for everyone. Read [what is new](https://v2.preview.homarr.dev/blog/2026/09/03/homarr-2.0), browse the [v2 documentation](https://v2.preview.homarr.dev/docs/getting-started/) and [Workshop](https://v2.preview.homarr.dev/workshop), or try the [hosted preview](https://app-v2.preview.homarr.dev). If anything breaks or does not feel right, [tell us on Discord](https://discord.com/invite/aCsmEV5RgA) in the dedicated v2 channel for the fastest response, or [open a dedicated GitHub issue](https://github.com/homarr-labs/homarr/issues/new?template=v2_beta_feedback.yml). SQLite administrators can download a full backup directly from the beta announcement before changing images. + +::: + Before you try to install Homarr, please review if your device meets the requirements. Installing on an unsupported device may lead to unstable behaviour and no support will be provided. diff --git a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx index 24ebe3d03..bbe100e70 100644 --- a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx @@ -1,17 +1,21 @@ +import { createHash } from "node:crypto"; + import type { JSX, PropsWithChildren } from "react"; import { notFound, redirect } from "next/navigation"; -import { AppShellMain } from "@mantine/core"; +import { cookies } from "next/headers"; import { TRPCError } from "@trpc/server"; import { getRscUserSettingsAsync } from "@homarr/api/user-server"; import { auth } from "@homarr/auth/next"; import { BoardProvider } from "@homarr/boards/context"; import { EditModeProvider } from "@homarr/boards/edit-mode"; +import { dbEnv } from "@homarr/core/infrastructure/db/env"; import { createLogger } from "@homarr/core/infrastructure/logs"; +import { v2BetaAnnouncementCookieKey } from "@homarr/definitions"; -import { MainHeader } from "~/components/layout/header"; +import { MainHeaderContent } from "~/components/layout/header"; import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo"; -import { ClientShell } from "~/components/layout/shell"; +import { V2BetaDashboardShell } from "~/components/layout/v2-beta-announcement/v2-beta-dashboard-shell"; import { BoardTourGate } from "~/components/onboarding/board-tour-gate"; import { env } from "~/env"; import { getCurrentColorSchemeAsync } from "~/theme/color-scheme"; @@ -42,6 +46,7 @@ export const createBoardLayout = ({ }>) => { const resolvedParams = await params; const sessionPromise = auth(); + const cookiesPromise = cookies(); const initialBoardPromise = getInitialBoard(resolvedParams).then( (board) => ({ status: "fulfilled", board }) as const, (error: unknown) => ({ status: "rejected", error }) as const, @@ -58,11 +63,12 @@ export const createBoardLayout = ({ return false; } }); - const [session, initialBoardResult, colorScheme, shouldRunBoardTour] = await Promise.all([ + const [session, initialBoardResult, colorScheme, shouldRunBoardTour, cookieStore] = await Promise.all([ sessionPromise, initialBoardPromise, colorSchemePromise, shouldRunBoardTourPromise, + cookiesPromise, ]); if (initialBoardResult.status === "rejected") { const { error } = initialBoardResult; @@ -83,6 +89,11 @@ export const createBoardLayout = ({ throw error; } const initialBoard = initialBoardResult.board; + const viewerIdentity = session?.user.id ?? "anonymous"; + const viewerHash = createHash("sha256").update(viewerIdentity).digest("hex").slice(0, 16); + const dismissalCookieName = `${v2BetaAnnouncementCookieKey}.${viewerHash}`; + const isAnnouncementDismissed = cookieStore.get(dismissalCookieName)?.value === "dismissed"; + const canExportBackup = dbEnv.DRIVER === "better-sqlite3" && Boolean(session?.user.permissions.includes("admin")); return ( @@ -91,14 +102,20 @@ export const createBoardLayout = ({ - - } - actions={headerActions} - hasNavigation={false} - /> - {children} - + } + actions={headerActions} + hasNavigation={false} + /> + } + > + {children} + diff --git a/apps/nextjs/src/app/[locale]/manage/tools/backup/_components/backup-export-card.tsx b/apps/nextjs/src/app/[locale]/manage/tools/backup/_components/backup-export-card.tsx index dc49f71a8..8f4d7f06e 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/backup/_components/backup-export-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/backup/_components/backup-export-card.tsx @@ -1,49 +1,14 @@ "use client"; -import { useState } from "react"; -import { Alert, Button, Card, Group, Stack, Text, Title } from "@mantine/core"; -import { IconAlertTriangle, IconDatabaseExport, IconDownload } from "@tabler/icons-react"; +import { Card, Group, Stack, Text, Title } from "@mantine/core"; +import { IconDatabaseExport } from "@tabler/icons-react"; import { useScopedI18n } from "@homarr/translation/client"; -import { DangerousActionConfirmation } from "~/components/backup/dangerous-action-confirmation"; +import { BackupExportButton } from "~/components/backup"; export const BackupExportCard = () => { const t = useScopedI18n("management.page.tool.backup.export"); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [showConfirm, setShowConfirm] = useState(false); - - const handleExport = async () => { - setLoading(true); - setError(null); - try { - const response = await fetch("/api/backup/export"); - if (!response.ok) { - const errorMessage = await response.json().then( - (data) => data.error, - () => `Server returned ${response.status}`, - ); - throw new Error(errorMessage ?? t("error")); - } - - const blob = await response.blob(); - const url = URL.createObjectURL(blob); - const filename = - response.headers.get("Content-Disposition")?.match(/filename="(.+)"/)?.[1] ?? "homarr-backup.zip"; - - const a = document.createElement("a"); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); - setShowConfirm(false); - } catch (err) { - setError(err instanceof Error ? err.message : t("error")); - } finally { - setLoading(false); - } - }; return ( @@ -55,33 +20,7 @@ export const BackupExportCard = () => { {t("description")} - {error && ( - }> - {error} - - )} - - {showConfirm ? ( - } - cancelLabel={t("confirm.cancel")} - onConfirm={handleExport} - onCancel={() => setShowConfirm(false)} - disabled={loading} - color="orange" - /> - ) : ( - - - - )} + ); diff --git a/apps/nextjs/src/components/backup/backup-export-button.tsx b/apps/nextjs/src/components/backup/backup-export-button.tsx new file mode 100644 index 000000000..6116d0a16 --- /dev/null +++ b/apps/nextjs/src/components/backup/backup-export-button.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useState } from "react"; +import { Alert, Button, Group, Stack } from "@mantine/core"; +import { IconAlertTriangle, IconDownload } from "@tabler/icons-react"; + +import { useScopedI18n } from "@homarr/translation/client"; + +import { DangerousActionConfirmation } from "./dangerous-action-confirmation"; + +export const BackupExportButton = () => { + const t = useScopedI18n("management.page.tool.backup.export"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [showConfirm, setShowConfirm] = useState(false); + + const handleExport = async () => { + setLoading(true); + setError(null); + try { + const response = await fetch("/api/backup/export"); + if (!response.ok) { + const errorMessage = await response.json().then( + (data) => data.error, + () => `Server returned ${response.status}`, + ); + throw new Error(errorMessage ?? t("error")); + } + + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const filename = + response.headers.get("Content-Disposition")?.match(/filename="(.+)"/)?.[1] ?? "homarr-backup.zip"; + + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = filename; + anchor.click(); + URL.revokeObjectURL(url); + setShowConfirm(false); + } catch (cause) { + setError(cause instanceof Error ? cause.message : t("error")); + } finally { + setLoading(false); + } + }; + + return ( + + {error && ( + }> + {error} + + )} + + {showConfirm ? ( + } + cancelLabel={t("confirm.cancel")} + onConfirm={handleExport} + onCancel={() => setShowConfirm(false)} + disabled={loading} + color="orange" + /> + ) : ( + + + + )} + + ); +}; diff --git a/apps/nextjs/src/components/backup/index.ts b/apps/nextjs/src/components/backup/index.ts index accd1a935..1f6196125 100644 --- a/apps/nextjs/src/components/backup/index.ts +++ b/apps/nextjs/src/components/backup/index.ts @@ -1,3 +1,4 @@ +export { BackupExportButton } from "./backup-export-button"; export { DatabaseRestoreFlow } from "./database-restore-flow"; export { DangerousActionConfirmation } from "./dangerous-action-confirmation"; export type { BackupAnalysis, RestoreStep } from "./types"; diff --git a/apps/nextjs/src/components/layout/header.tsx b/apps/nextjs/src/components/layout/header.tsx index 2c46608c0..a512229d0 100644 --- a/apps/nextjs/src/components/layout/header.tsx +++ b/apps/nextjs/src/components/layout/header.tsx @@ -19,25 +19,31 @@ interface Props { export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => { return ( - - - {hasNavigation && } - - {logo ?? } - - - - - - - {actions} - - - - - - - + ); }; + +export const MainHeaderContent = ({ logo, actions, hasNavigation = true }: Props) => ( + <> + + + {hasNavigation && } + + {logo ?? } + + + + + + + {actions} + + + + + + + + +); diff --git a/apps/nextjs/src/components/layout/shell.tsx b/apps/nextjs/src/components/layout/shell.tsx index b51cf3220..01ef8b7e0 100644 --- a/apps/nextjs/src/components/layout/shell.tsx +++ b/apps/nextjs/src/components/layout/shell.tsx @@ -10,11 +10,13 @@ import { navigationCollapsedAtom } from "./header/burger"; interface ClientShellProps { hasHeader?: boolean; hasNavigation?: boolean; + headerHeight?: number; } export const ClientShell = ({ hasHeader = true, hasNavigation = true, + headerHeight = 60, children, }: PropsWithChildren) => { const collapsed = useAtomValue(navigationCollapsedAtom); @@ -23,7 +25,7 @@ export const ClientShell = ({ return ( " }, + { kind: "added", value: "+ WORKSHOP_API_URL: https://v2.preview.homarr.dev/ # Only during the beta" }, + { kind: "context", value: "" }, + { kind: "context", value: " volumes:" }, + { kind: "context", value: " homarr-v2:" }, +] as const; + +interface V2BetaAnnouncementProps { + canExportBackup: boolean; + dismissalCookieName: string; + onDismiss: () => void; +} + +export const V2BetaAnnouncement = ({ canExportBackup, dismissalCookieName, onDismiss }: V2BetaAnnouncementProps) => { + const { openModal } = useModalAction(V2BetaAnnouncementModal); + + const dismiss = () => { + setClientCookie(dismissalCookieName, "dismissed", { + maxAge: 60 * 60 * 24 * 7, + path: "/", + sameSite: "lax", + secure: window.location.protocol === "https:", + }); + onDismiss(); + }; + + return ( + + + + + We've been cooking, Homarr v2 beta is here + + + Custom Widgets & CSS workshop, new drag-and-drop, new container system, side-bars are back, new widgets + and so much more, come check it out and help us make v2 ready for everyone + + + + + + + + + + + + ); +}; + +interface V2BetaAnnouncementModalProps { + canExportBackup: boolean; +} + +const V2BetaAnnouncementModal = createModal(({ innerProps }) => { + return ( + + + + Help us get Homarr v2 ready + + We've been working on Homarr's v2 update for a while now. There is no fixed release date yet, it + will be ready when it's ready. We{" "} + + need your help + {" "} + and feedback to help us become confident enough to release it for everyone. + + + + + + } title="Workshop"> + Share and discover Custom Widgets and CSS made by the Homarr community. + + } title="Complete drag-and-drop rewrite"> + Move, resize, multi-select, and arrange your dashboard with a board editor rebuilt from the ground up. + + } title="Custom Widgets v2"> + We think you can make almost any widget with live previews, API requests, options, actions, and validation. + + } title="Assistant"> + Assistant can directly make a Custom Widget for you. Try giving it the API docs for your latest selfhosted + tool and asking it to build a custom widget for it. + + + + + + + + + } + aria-label="Test Homarr v2 on your server" + > + + Test Homarr v2 on your server + + Back up this instance, then update your Docker Compose file. + + + + + + + + + + } title="Your testing is essential"> + + Everyone uses Homarr in a different way. If anything breaks or does not feel right, the easiest way to get it + fixed as soon as possible is to{" "} + + tell us about it on Discord + {" "} + in our dedicated v2 channel. You can also{" "} + + open a dedicated GitHub issue + + {"."} Every report matters. + + + + + + + + + + + + + + + ); +}).withOptions({ + defaultTitle: "Homarr v2 beta", + size: 1000, + yOffset: 128, + zIndex: getDefaultZIndex("modal") + 2, +}); + +interface V2BetaInstallInstructionsProps { + canExportBackup: boolean; +} + +const V2BetaInstallInstructions = ({ canExportBackup }: V2BetaInstallInstructionsProps) => ( + + {canExportBackup ? ( + + + + + Back up this Homarr instance + + + Download a full backup before testing v2. The archive contains sensitive data, so keep it somewhere safe. + + + + + ) : ( + + Direct backup downloads are available here to Homarr administrators using SQLite. If you use MySQL or + PostgreSQL, back up the database with its own tools before testing v2. + + )} + + + Update your Docker Compose file + + Replace the current image and add the beta-only Workshop URL. Use the example below as a starting point. + + + + + + + + docker-compose.yml + + + + {composeDiffLines.map((line, index) => ( + + {line.value || " "} + + ))} + + + + + Replace <SECRET_KEY> with your existing encryption key. The Workshop URL is only required while + the beta uses the preview Workshop. + + +); + +interface FeatureCardProps { + icon: ReactNode; + title: string; + children: ReactNode; +} + +const FeatureCard = ({ icon, title, children }: FeatureCardProps) => ( + + + + {icon} + + + + {title} + + + {children} + + + + +); diff --git a/apps/nextjs/src/components/layout/v2-beta-announcement/v2-beta-dashboard-shell.tsx b/apps/nextjs/src/components/layout/v2-beta-announcement/v2-beta-dashboard-shell.tsx new file mode 100644 index 000000000..0fa2c0bbe --- /dev/null +++ b/apps/nextjs/src/components/layout/v2-beta-announcement/v2-beta-dashboard-shell.tsx @@ -0,0 +1,42 @@ +"use client"; + +import type { PropsWithChildren, ReactNode } from "react"; +import { useState } from "react"; +import { AppShellHeader, AppShellMain } from "@mantine/core"; + +import { ClientShell } from "../shell"; +import { V2BetaAnnouncement } from "./v2-beta-announcement"; + +interface V2BetaDashboardShellProps { + canExportBackup: boolean; + dismissalCookieName: string; + initiallyDismissed: boolean; + header: ReactNode; +} + +export const V2BetaDashboardShell = ({ + canExportBackup, + dismissalCookieName, + initiallyDismissed, + header, + children, +}: PropsWithChildren) => { + const [isAnnouncementVisible, setIsAnnouncementVisible] = useState(!initiallyDismissed); + const headerHeight = isAnnouncementVisible ? 112 : 60; + + return ( + + + {isAnnouncementVisible && ( + setIsAnnouncementVisible(false)} + /> + )} + {header} + + {children} + + ); +}; diff --git a/packages/definitions/src/cookie.ts b/packages/definitions/src/cookie.ts index 69cb24930..67ba5a36d 100644 --- a/packages/definitions/src/cookie.ts +++ b/packages/definitions/src/cookie.ts @@ -1,2 +1,3 @@ export const colorSchemeCookieKey = "homarr.color-scheme"; export const localeCookieKey = "homarr.locale"; +export const v2BetaAnnouncementCookieKey = "homarr.v2-beta-announcement";