chore(release): automatic release v1.77.1
This commit is contained in:
15 files changed
+126
-825
No files matched your search
@@ -33,6 +33,7 @@ body:
|
||||
options:
|
||||
# The below comment is used to insert a new version with on-release.yml
|
||||
#NEXT_VERSION#
|
||||
- 1.77.0
|
||||
- 1.76.2
|
||||
- 1.76.1
|
||||
- 1.76.0
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
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
|
||||
@@ -11,12 +11,6 @@ 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.
|
||||
|
||||
|
||||
@@ -178,35 +178,37 @@ export const BoardSettingsForm = ({ board, permissions, hasFullAccess, hideVisib
|
||||
const isPending = savePartialSettings.isPending || saveLayouts.isPending;
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<Stack gap="xl">
|
||||
<GeneralSettingsContent board={board} form={form} />
|
||||
<LayoutSettingsContent form={form} />
|
||||
<BackgroundSettingsContent form={form} />
|
||||
<ColorSettingsContent form={form} />
|
||||
<CustomCssSettingsContent form={form} />
|
||||
<BehaviorSettingsContent form={form} />
|
||||
<Stack gap="xl">
|
||||
<form onSubmit={form.onSubmit((values) => void handleSubmitAsync(values))}>
|
||||
<Stack gap="xl">
|
||||
<GeneralSettingsContent board={board} form={form} />
|
||||
<LayoutSettingsContent form={form} />
|
||||
<BackgroundSettingsContent form={form} />
|
||||
<ColorSettingsContent form={form} />
|
||||
<CustomCssSettingsContent form={form} />
|
||||
<BehaviorSettingsContent form={form} />
|
||||
|
||||
{hasFullAccess && (
|
||||
<SectionCard title={tSection("access.title")}>
|
||||
<BoardAccessSettings board={board} initialPermissions={permissions} />
|
||||
</SectionCard>
|
||||
)}
|
||||
{form.isDirty() && (
|
||||
<UnsavedChangesBar>
|
||||
<Button type="button" disabled={isPending} variant="default" onClick={handleDiscard}>
|
||||
{t("common.action.discard")}
|
||||
</Button>
|
||||
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</UnsavedChangesBar>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
|
||||
{hasFullAccess && <DangerZoneSettingsContent hideVisibility={hideVisibility} />}
|
||||
{hasFullAccess && (
|
||||
<SectionCard title={tSection("access.title")}>
|
||||
<BoardAccessSettings board={board} initialPermissions={permissions} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{form.isDirty() && (
|
||||
<UnsavedChangesBar>
|
||||
<Button type="button" disabled={isPending} variant="default" onClick={handleDiscard}>
|
||||
{t("common.action.discard")}
|
||||
</Button>
|
||||
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</UnsavedChangesBar>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
{hasFullAccess && <DangerZoneSettingsContent hideVisibility={hideVisibility} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import type { JSX, PropsWithChildren } from "react";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { cookies } from "next/headers";
|
||||
import { AppShellMain } from "@mantine/core";
|
||||
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 { MainHeaderContent } from "~/components/layout/header";
|
||||
import { MainHeader } from "~/components/layout/header";
|
||||
import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo";
|
||||
import { V2BetaDashboardShell } from "~/components/layout/v2-beta-announcement/v2-beta-dashboard-shell";
|
||||
import { ClientShell } from "~/components/layout/shell";
|
||||
import { BoardTourGate } from "~/components/onboarding/board-tour-gate";
|
||||
import { env } from "~/env";
|
||||
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
|
||||
@@ -46,7 +42,6 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
}>) => {
|
||||
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,
|
||||
@@ -63,12 +58,11 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const [session, initialBoardResult, colorScheme, shouldRunBoardTour, cookieStore] = await Promise.all([
|
||||
const [session, initialBoardResult, colorScheme, shouldRunBoardTour] = await Promise.all([
|
||||
sessionPromise,
|
||||
initialBoardPromise,
|
||||
colorSchemePromise,
|
||||
shouldRunBoardTourPromise,
|
||||
cookiesPromise,
|
||||
]);
|
||||
if (initialBoardResult.status === "rejected") {
|
||||
const { error } = initialBoardResult;
|
||||
@@ -89,11 +83,6 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
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 (
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
@@ -102,20 +91,14 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
<BoardMantineProvider defaultColorScheme={colorScheme}>
|
||||
<CustomCss />
|
||||
<BoardTourGate enabled={shouldRunBoardTour}>
|
||||
<V2BetaDashboardShell
|
||||
canExportBackup={canExportBackup}
|
||||
dismissalCookieName={dismissalCookieName}
|
||||
initiallyDismissed={isAnnouncementDismissed}
|
||||
header={
|
||||
<MainHeaderContent
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
actions={headerActions}
|
||||
hasNavigation={false}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</V2BetaDashboardShell>
|
||||
<ClientShell hasNavigation={false}>
|
||||
<MainHeader
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
actions={headerActions}
|
||||
hasNavigation={false}
|
||||
/>
|
||||
<AppShellMain>{children}</AppShellMain>
|
||||
</ClientShell>
|
||||
</BoardTourGate>
|
||||
</BoardMantineProvider>
|
||||
</EditModeProvider>
|
||||
|
||||
+65
-4
@@ -1,14 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconDatabaseExport } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { Alert, Button, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconDatabaseExport, IconDownload } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { BackupExportButton } from "~/components/backup";
|
||||
import { DangerousActionConfirmation } from "~/components/backup/dangerous-action-confirmation";
|
||||
|
||||
export const BackupExportCard = () => {
|
||||
const t = useScopedI18n("management.page.tool.backup.export");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<Card withBorder>
|
||||
@@ -20,7 +55,33 @@ export const BackupExportCard = () => {
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("description")}
|
||||
</Text>
|
||||
<BackupExportButton />
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertTriangle size={16} />}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{showConfirm ? (
|
||||
<DangerousActionConfirmation
|
||||
title={t("confirm.title")}
|
||||
warningTitle={t("confirm.warningTitle")}
|
||||
warningBody={t("confirm.warningBody")}
|
||||
typePrompt={t("confirm.typePrompt", { phrase: "I understand" })}
|
||||
submitLabel={t("confirm.submit")}
|
||||
submitIcon={<IconDownload size={16} />}
|
||||
cancelLabel={t("confirm.cancel")}
|
||||
onConfirm={handleExport}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
disabled={loading}
|
||||
color="orange"
|
||||
/>
|
||||
) : (
|
||||
<Group>
|
||||
<Button leftSection={<IconDownload size={16} />} loading={loading} onClick={() => setShowConfirm(true)}>
|
||||
{t("button")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
"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<string | null>(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 (
|
||||
<Stack gap="sm">
|
||||
{error && (
|
||||
<Alert color="red" icon={<IconAlertTriangle size={16} />}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{showConfirm ? (
|
||||
<DangerousActionConfirmation
|
||||
title={t("confirm.title")}
|
||||
warningTitle={t("confirm.warningTitle")}
|
||||
warningBody={t("confirm.warningBody")}
|
||||
typePrompt={t("confirm.typePrompt", { phrase: "I understand" })}
|
||||
submitLabel={t("confirm.submit")}
|
||||
submitIcon={<IconDownload size={16} />}
|
||||
cancelLabel={t("confirm.cancel")}
|
||||
onConfirm={handleExport}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
disabled={loading}
|
||||
color="orange"
|
||||
/>
|
||||
) : (
|
||||
<Group>
|
||||
<Button leftSection={<IconDownload size={16} />} loading={loading} onClick={() => setShowConfirm(true)}>
|
||||
{t("button")}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export { BackupExportButton } from "./backup-export-button";
|
||||
export { DatabaseRestoreFlow } from "./database-restore-flow";
|
||||
export { DangerousActionConfirmation } from "./dangerous-action-confirmation";
|
||||
export type { BackupAnalysis, RestoreStep } from "./types";
|
||||
@@ -19,31 +19,25 @@ interface Props {
|
||||
export const MainHeader = ({ logo, actions, hasNavigation = true }: Props) => {
|
||||
return (
|
||||
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
|
||||
<MainHeaderContent logo={logo} actions={actions} hasNavigation={hasNavigation} />
|
||||
<Group h="100%" gap="xl" px="md" justify="apart" wrap="nowrap">
|
||||
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
||||
{hasNavigation && <ClientBurger />}
|
||||
<UnstyledButton component={Link} href="/">
|
||||
{logo ?? <HomarrLogoWithTitle size="md" />}
|
||||
</UnstyledButton>
|
||||
</Group>
|
||||
<TourTarget id="board-search">
|
||||
<DesktopSearchInput />
|
||||
</TourTarget>
|
||||
<Group h="100%" align="center" justify="end" style={{ flex: 1 }} wrap="nowrap">
|
||||
{actions}
|
||||
<MobileSearchButton />
|
||||
<TourTarget id="board-user-menu">
|
||||
<UserButton />
|
||||
</TourTarget>
|
||||
</Group>
|
||||
</Group>
|
||||
<LazySpotlight />
|
||||
</AppShellHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export const MainHeaderContent = ({ logo, actions, hasNavigation = true }: Props) => (
|
||||
<>
|
||||
<Group h={60} gap="xl" px="md" justify="apart" wrap="nowrap">
|
||||
<Group h="100%" align="center" style={{ flex: 1 }} wrap="nowrap">
|
||||
{hasNavigation && <ClientBurger />}
|
||||
<UnstyledButton component={Link} href="/">
|
||||
{logo ?? <HomarrLogoWithTitle size="md" />}
|
||||
</UnstyledButton>
|
||||
</Group>
|
||||
<TourTarget id="board-search">
|
||||
<DesktopSearchInput />
|
||||
</TourTarget>
|
||||
<Group h="100%" align="center" justify="end" style={{ flex: 1 }} wrap="nowrap">
|
||||
{actions}
|
||||
<MobileSearchButton />
|
||||
<TourTarget id="board-user-menu">
|
||||
<UserButton />
|
||||
</TourTarget>
|
||||
</Group>
|
||||
</Group>
|
||||
<LazySpotlight />
|
||||
</>
|
||||
);
|
||||
@@ -10,13 +10,11 @@ import { navigationCollapsedAtom } from "./header/burger";
|
||||
interface ClientShellProps {
|
||||
hasHeader?: boolean;
|
||||
hasNavigation?: boolean;
|
||||
headerHeight?: number;
|
||||
}
|
||||
|
||||
export const ClientShell = ({
|
||||
hasHeader = true,
|
||||
hasNavigation = true,
|
||||
headerHeight = 60,
|
||||
children,
|
||||
}: PropsWithChildren<ClientShellProps>) => {
|
||||
const collapsed = useAtomValue(navigationCollapsedAtom);
|
||||
@@ -25,7 +23,7 @@ export const ClientShell = ({
|
||||
return (
|
||||
<AppShell
|
||||
{...backgroundProps}
|
||||
header={hasHeader ? { height: headerHeight } : undefined}
|
||||
header={hasHeader ? { height: 60 } : undefined}
|
||||
navbar={
|
||||
hasNavigation
|
||||
? {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const v2BetaBlogUrl = "https://v2.preview.homarr.dev/blog/2026/09/03/homarr-2.0";
|
||||
export const v2BetaDocsUrl = "https://v2.preview.homarr.dev/docs/getting-started/";
|
||||
export const v2BetaPreviewUrl = "https://app-v2.preview.homarr.dev";
|
||||
export const v2BetaWorkshopUrl = "https://v2.preview.homarr.dev/workshop";
|
||||
export const v2BetaFeedbackUrl = "https://github.com/homarr-labs/homarr/issues/new?template=v2_beta_feedback.yml";
|
||||
export const v2BetaDiscussionUrl = "https://github.com/homarr-labs/homarr/issues/6600";
|
||||
export const discordInviteUrl = "https://discord.com/invite/aCsmEV5RgA";
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
.banner {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
height: 52px;
|
||||
overflow: hidden;
|
||||
color: var(--mantine-color-white);
|
||||
background: var(--mantine-color-red-9);
|
||||
border-bottom: 1px solid rgb(255 255 255 / 18%);
|
||||
}
|
||||
|
||||
.banner::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
right: -0.35rem;
|
||||
bottom: -1.75rem;
|
||||
color: rgb(255 255 255 / 9%);
|
||||
font-size: 5.5rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
content: "v2";
|
||||
}
|
||||
|
||||
.copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.modalHero {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
color: var(--mantine-color-white);
|
||||
background: var(--mantine-color-red-9);
|
||||
}
|
||||
|
||||
.modalHero::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
right: -0.75rem;
|
||||
bottom: -1.5rem;
|
||||
color: rgb(255 255 255 / 8%);
|
||||
font-size: 12rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
content: "v2";
|
||||
}
|
||||
|
||||
.featureCard {
|
||||
height: 100%;
|
||||
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
}
|
||||
|
||||
.diffViewer {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diffHeader {
|
||||
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
|
||||
border-bottom: 1px solid var(--mantine-color-default-border);
|
||||
}
|
||||
|
||||
.diffCode {
|
||||
margin: 0;
|
||||
padding-block: var(--mantine-spacing-sm);
|
||||
overflow-x: auto;
|
||||
color: light-dark(var(--mantine-color-dark-8), var(--mantine-color-gray-2));
|
||||
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
|
||||
font-family: var(--mantine-font-family-monospace);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.diffLine_context,
|
||||
.diffLine_added,
|
||||
.diffLine_removed {
|
||||
display: block;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
padding-inline: var(--mantine-spacing-md);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.diffLine_added {
|
||||
color: light-dark(var(--mantine-color-green-9), var(--mantine-color-green-3));
|
||||
background: light-dark(var(--mantine-color-green-0), rgb(47 158 68 / 15%));
|
||||
}
|
||||
|
||||
.diffLine_removed {
|
||||
color: light-dark(var(--mantine-color-red-9), var(--mantine-color-red-3));
|
||||
background: light-dark(var(--mantine-color-red-0), rgb(250 82 82 / 15%));
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Accordion,
|
||||
Alert,
|
||||
Anchor,
|
||||
Box,
|
||||
Button,
|
||||
getDefaultZIndex,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconArrowsMove,
|
||||
IconBook2,
|
||||
IconBrandCss3,
|
||||
IconBrandDiscord,
|
||||
IconBrandGithub,
|
||||
IconDatabaseExport,
|
||||
IconExternalLink,
|
||||
IconHeartHandshake,
|
||||
IconRobot,
|
||||
IconRocket,
|
||||
IconTerminal2,
|
||||
IconWand,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { setClientCookie } from "@homarr/common";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
|
||||
import { BackupExportButton } from "~/components/backup";
|
||||
import {
|
||||
discordInviteUrl,
|
||||
v2BetaBlogUrl,
|
||||
v2BetaDiscussionUrl,
|
||||
v2BetaDocsUrl,
|
||||
v2BetaFeedbackUrl,
|
||||
v2BetaPreviewUrl,
|
||||
v2BetaWorkshopUrl,
|
||||
} from "./constants";
|
||||
import classes from "./v2-beta-announcement.module.css";
|
||||
|
||||
const diffLineClassNames = {
|
||||
added: classes.diffLine_added,
|
||||
context: classes.diffLine_context,
|
||||
removed: classes.diffLine_removed,
|
||||
} as const;
|
||||
|
||||
const composeDiffLines = [
|
||||
{ kind: "context", value: " services:" },
|
||||
{ kind: "context", value: " homarr:" },
|
||||
{ kind: "added", value: "+ image: ghcr.io/homarr-labs/homarr-test:v2" },
|
||||
{ kind: "removed", value: "- image: ghcr.io/homarr-labs/homarr:latest" },
|
||||
{ kind: "context", value: " ports:" },
|
||||
{ kind: "context", value: ' - "7575:7575"' },
|
||||
{ kind: "context", value: " volumes:" },
|
||||
{ kind: "context", value: " - homarr-v2:/appdata" },
|
||||
{ kind: "context", value: " environment:" },
|
||||
{ kind: "context", value: " SECRET_ENCRYPTION_KEY: <SECRET_KEY>" },
|
||||
{ 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 (
|
||||
<Box component="aside" className={classes.banner} aria-label="Homarr v2 beta announcement">
|
||||
<Group h="100%" justify="space-between" gap="sm" wrap="nowrap" px="md">
|
||||
<Box className={classes.copy}>
|
||||
<Text fw={750} size="sm" truncate>
|
||||
We've been cooking, Homarr v2 beta is here
|
||||
</Text>
|
||||
<Text className={classes.subtitle} visibleFrom="sm" size="xs" truncate>
|
||||
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
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Button variant="white" color="red" size="compact-sm" onClick={() => openModal({ canExportBackup })}>
|
||||
Explore v2
|
||||
</Button>
|
||||
<ActionIcon variant="subtle" color="white" onClick={dismiss} aria-label="Dismiss the v2 beta announcement">
|
||||
<IconX size={18} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface V2BetaAnnouncementModalProps {
|
||||
canExportBackup: boolean;
|
||||
}
|
||||
|
||||
const V2BetaAnnouncementModal = createModal<V2BetaAnnouncementModalProps>(({ innerProps }) => {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<Paper className={classes.modalHero} p={{ base: "lg", sm: "xl" }} radius="md">
|
||||
<Stack gap="sm" maw={760}>
|
||||
<Title order={2}>Help us get Homarr v2 ready</Title>
|
||||
<Text>
|
||||
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{" "}
|
||||
<Text span inherit fw={700}>
|
||||
need your help
|
||||
</Text>{" "}
|
||||
and feedback to help us become confident enough to release it for everyone.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<FeatureCard icon={<IconBrandCss3 size={21} />} title="Workshop">
|
||||
Share and discover Custom Widgets and CSS made by the Homarr community.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={<IconArrowsMove size={21} />} title="Complete drag-and-drop rewrite">
|
||||
Move, resize, multi-select, and arrange your dashboard with a board editor rebuilt from the ground up.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={<IconWand size={21} />} title="Custom Widgets v2">
|
||||
We think you can make almost any widget with live previews, API requests, options, actions, and validation.
|
||||
</FeatureCard>
|
||||
<FeatureCard icon={<IconRobot size={21} />} 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.
|
||||
</FeatureCard>
|
||||
</SimpleGrid>
|
||||
|
||||
<Accordion variant="separated" radius="md">
|
||||
<Accordion.Item value="installation">
|
||||
<Accordion.Control
|
||||
icon={
|
||||
<ThemeIcon variant="light" color="red" radius="md" size="lg">
|
||||
<IconTerminal2 size={20} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
aria-label="Test Homarr v2 on your server"
|
||||
>
|
||||
<Box>
|
||||
<Text fw={750}>Test Homarr v2 on your server</Text>
|
||||
<Text c="dimmed" size="xs">
|
||||
Back up this instance, then update your Docker Compose file.
|
||||
</Text>
|
||||
</Box>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<V2BetaInstallInstructions canExportBackup={innerProps.canExportBackup} />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
|
||||
<Alert variant="light" color="red" icon={<IconHeartHandshake size={22} />} title="Your testing is essential">
|
||||
<Text size="sm">
|
||||
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{" "}
|
||||
<Anchor
|
||||
href={discordInviteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
c="inherit"
|
||||
fz="sm"
|
||||
fw={700}
|
||||
underline="always"
|
||||
>
|
||||
tell us about it on Discord
|
||||
</Anchor>{" "}
|
||||
in our dedicated v2 channel. You can also{" "}
|
||||
<Anchor
|
||||
href={v2BetaFeedbackUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
c="inherit"
|
||||
fz="sm"
|
||||
fw={700}
|
||||
underline="always"
|
||||
>
|
||||
open a dedicated GitHub issue
|
||||
</Anchor>
|
||||
{"."} Every report matters.
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaPreviewUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
size="md"
|
||||
leftSection={<IconRocket size={18} />}
|
||||
>
|
||||
Try the hosted preview
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaWorkshopUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
size="md"
|
||||
leftSection={<IconBrandCss3 size={18} />}
|
||||
>
|
||||
Open Workshop
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaDocsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
size="md"
|
||||
leftSection={<IconBook2 size={18} />}
|
||||
>
|
||||
Read the v2 docs
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaBlogUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
size="md"
|
||||
rightSection={<IconExternalLink size={16} />}
|
||||
>
|
||||
See everything new
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={discordInviteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
size="md"
|
||||
leftSection={<IconBrandDiscord size={18} />}
|
||||
>
|
||||
Join us on Discord
|
||||
</Button>
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaDiscussionUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="default"
|
||||
size="md"
|
||||
rightSection={<IconExternalLink size={15} />}
|
||||
>
|
||||
Follow v2 development
|
||||
</Button>
|
||||
</SimpleGrid>
|
||||
|
||||
<Button
|
||||
component="a"
|
||||
href={v2BetaFeedbackUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="light"
|
||||
color="red"
|
||||
size="md"
|
||||
leftSection={<IconBrandGithub size={18} />}
|
||||
>
|
||||
Report anything that breaks or feels wrong
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle: "Homarr v2 beta",
|
||||
size: 1000,
|
||||
yOffset: 128,
|
||||
zIndex: getDefaultZIndex("modal") + 2,
|
||||
});
|
||||
|
||||
interface V2BetaInstallInstructionsProps {
|
||||
canExportBackup: boolean;
|
||||
}
|
||||
|
||||
const V2BetaInstallInstructions = ({ canExportBackup }: V2BetaInstallInstructionsProps) => (
|
||||
<Stack gap="lg">
|
||||
{canExportBackup ? (
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="sm">
|
||||
<Group gap="sm">
|
||||
<IconDatabaseExport size={22} />
|
||||
<Title order={4}>Back up this Homarr instance</Title>
|
||||
</Group>
|
||||
<Text c="dimmed" size="sm">
|
||||
Download a full backup before testing v2. The archive contains sensitive data, so keep it somewhere safe.
|
||||
</Text>
|
||||
<BackupExportButton />
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : (
|
||||
<Alert color="blue" title="Back up your database before continuing">
|
||||
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.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Stack gap="xs">
|
||||
<Title order={3}>Update your Docker Compose file</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Replace the current image and add the beta-only Workshop URL. Use the example below as a starting point.
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Paper withBorder radius="md" className={classes.diffViewer}>
|
||||
<Group gap="xs" px="md" py="sm" className={classes.diffHeader}>
|
||||
<IconTerminal2 size={17} />
|
||||
<Text fw={700} size="sm">
|
||||
docker-compose.yml
|
||||
</Text>
|
||||
</Group>
|
||||
<Box component="pre" className={classes.diffCode} aria-label="Docker Compose changes for Homarr v2 beta">
|
||||
{composeDiffLines.map((line, index) => (
|
||||
<Box component="span" className={diffLineClassNames[line.kind]} key={`${line.kind}-${index}`}>
|
||||
{line.value || " "}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
Replace <code><SECRET_KEY></code> with your existing encryption key. The Workshop URL is only required while
|
||||
the beta uses the preview Workshop.
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
interface FeatureCardProps {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const FeatureCard = ({ icon, title, children }: FeatureCardProps) => (
|
||||
<Paper className={classes.featureCard} withBorder p="md" radius="md">
|
||||
<Group gap="sm" align="flex-start" wrap="nowrap">
|
||||
<ThemeIcon variant="light" color="red" radius="md" size="lg">
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
<Box>
|
||||
<Text fw={750} size="sm">
|
||||
{title}
|
||||
</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
{children}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
@@ -1,42 +0,0 @@
|
||||
"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<V2BetaDashboardShellProps>) => {
|
||||
const [isAnnouncementVisible, setIsAnnouncementVisible] = useState(!initiallyDismissed);
|
||||
const headerHeight = isAnnouncementVisible ? 112 : 60;
|
||||
|
||||
return (
|
||||
<ClientShell hasNavigation={false} headerHeight={headerHeight}>
|
||||
<AppShellHeader maw="100vw" zIndex={201} style={{ overflowX: "hidden" }}>
|
||||
{isAnnouncementVisible && (
|
||||
<V2BetaAnnouncement
|
||||
canExportBackup={canExportBackup}
|
||||
dismissalCookieName={dismissalCookieName}
|
||||
onDismiss={() => setIsAnnouncementVisible(false)}
|
||||
/>
|
||||
)}
|
||||
{header}
|
||||
</AppShellHeader>
|
||||
<AppShellMain>{children}</AppShellMain>
|
||||
</ClientShell>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,2 @@
|
||||
export const colorSchemeCookieKey = "homarr.color-scheme";
|
||||
export const localeCookieKey = "homarr.locale";
|
||||
export const v2BetaAnnouncementCookieKey = "homarr.v2-beta-announcement";
|
||||
Reference in new issue
Block a user