Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ed6b16217 | ||
|
|
c6d22b3939 |
No files matched your search
@@ -30,9 +30,8 @@ needed to restore encrypted integration credentials.
|
||||
Select a Homarr backup ZIP. The browser previews entity counts, board names, and required database migrations before
|
||||
the archive is uploaded.
|
||||
|
||||
Restoring replaces the current SQLite database and cannot be undone. It also invalidates the current database session.
|
||||
After a successful management restore, Homarr waits for the restarted server and sends the administrator to sign in
|
||||
again. The first-run onboarding restore keeps its existing board destination.
|
||||
Restoring replaces the current SQLite database and cannot be undone. After a successful restore, the Next.js process
|
||||
restarts and the page reloads when Homarr is ready.
|
||||
|
||||
A backup can be restored on another Homarr instance. When its `SECRET_ENCRYPTION_KEY` differs, Homarr re-encrypts stored
|
||||
integration secrets for the target instance. Version metadata is used to apply compatible migrations.
|
||||
|
||||
@@ -204,12 +204,8 @@ export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: Dat
|
||||
}
|
||||
|
||||
onRestoreComplete?.();
|
||||
let destination: string | null = null;
|
||||
if (variant === "standalone" && homeBoardName) {
|
||||
destination = `/boards/${encodeURIComponent(homeBoardName)}`;
|
||||
} else if (variant === "card") {
|
||||
destination = "/auth/login";
|
||||
}
|
||||
const destination =
|
||||
variant === "standalone" && homeBoardName ? `/boards/${encodeURIComponent(homeBoardName)}` : null;
|
||||
startReadinessCheck(restartAfterMs, destination);
|
||||
}, [file, onRestoreComplete, startReadinessCheck, t, variant]);
|
||||
|
||||
@@ -321,7 +317,6 @@ export const DatabaseRestoreFlow = ({ variant = "card", onRestoreComplete }: Dat
|
||||
<RestoreProgressPanel
|
||||
active
|
||||
status={restoreStatus}
|
||||
reloginRequired={variant === "card"}
|
||||
onRetry={() => startReadinessCheck(0)}
|
||||
onReload={() => window.location.reload()}
|
||||
/>
|
||||
|
||||
@@ -11,18 +11,11 @@ export type RestoreProgressStatus = "restoring" | "restarting" | "timedOut";
|
||||
interface RestoreProgressPanelProps {
|
||||
active: boolean;
|
||||
status: RestoreProgressStatus;
|
||||
reloginRequired?: boolean;
|
||||
onRetry: () => void;
|
||||
onReload: () => void;
|
||||
}
|
||||
|
||||
export const RestoreProgressPanel = ({
|
||||
active,
|
||||
status,
|
||||
reloginRequired = false,
|
||||
onRetry,
|
||||
onReload,
|
||||
}: RestoreProgressPanelProps) => {
|
||||
export const RestoreProgressPanel = ({ active, status, onRetry, onReload }: RestoreProgressPanelProps) => {
|
||||
const t = useI18n("management.page.tool.backup.restore");
|
||||
const tCommon = useI18n("common");
|
||||
const reduceMotion = useReducedMotion();
|
||||
@@ -70,12 +63,6 @@ export const RestoreProgressPanel = ({
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
{reloginRequired && !isRestoring ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
{t("progress.relogin")}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{isTimedOut ? (
|
||||
<Group>
|
||||
<Button variant="light" onClick={onRetry} leftSection={<IconRefresh size={16} />}>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const createNextcloudCalendarServerUrl = (integrationUrl: string) => {
|
||||
const calendarServerUrl = new URL(integrationUrl);
|
||||
const integrationPath = calendarServerUrl.pathname.replace(/\/+$/, "");
|
||||
const davPath = "/remote.php/dav";
|
||||
|
||||
calendarServerUrl.pathname = integrationPath.endsWith(davPath)
|
||||
? `${integrationPath}/`
|
||||
: `${integrationPath}${davPath}/`;
|
||||
calendarServerUrl.search = "";
|
||||
calendarServerUrl.hash = "";
|
||||
|
||||
return calendarServerUrl.toString();
|
||||
};
|
||||
@@ -21,6 +21,7 @@ import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integ
|
||||
import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
|
||||
import type { Notification } from "../interfaces/notifications/notification-types";
|
||||
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
|
||||
import { createNextcloudCalendarServerUrl } from "./nextcloud-url";
|
||||
|
||||
// Register all existing timezones
|
||||
if (ICAL.TimezoneService.count === 0) {
|
||||
@@ -189,7 +190,7 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr
|
||||
|
||||
private async createCalendarClientAsync(agent?: Agent) {
|
||||
return new DAVClient({
|
||||
serverUrl: this.integration.url,
|
||||
serverUrl: createNextcloudCalendarServerUrl(this.integration.url),
|
||||
credentials: {
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Agent } from "node:https";
|
||||
import { DAVClient } from "tsdav";
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { NextcloudIntegration } from "../nextcloud.integration";
|
||||
|
||||
vi.mock("tsdav", () => ({ DAVClient: vi.fn() }));
|
||||
|
||||
interface NextcloudCalendarClientFactory {
|
||||
createCalendarClientAsync(agent: Agent): Promise<unknown>;
|
||||
}
|
||||
|
||||
describe("NextcloudIntegration", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(DAVClient).mockClear();
|
||||
});
|
||||
|
||||
test("uses a calendar server URL that preserves the integration subpath", async () => {
|
||||
const integration = new NextcloudIntegration({
|
||||
id: "integration-id",
|
||||
name: "Nextcloud",
|
||||
url: "https://example.com/nextcloud",
|
||||
externalUrl: null,
|
||||
decryptedSecrets: [
|
||||
{ kind: "username", value: "admin" },
|
||||
{ kind: "password", value: "password" },
|
||||
],
|
||||
});
|
||||
|
||||
await (integration as unknown as NextcloudCalendarClientFactory).createCalendarClientAsync({} as Agent);
|
||||
|
||||
expect(DAVClient).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
serverUrl: "https://example.com/nextcloud/remote.php/dav/",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createNextcloudCalendarServerUrl } from "../nextcloud-url";
|
||||
|
||||
describe("createNextcloudCalendarServerUrl", () => {
|
||||
test("uses the DAV endpoint for a root installation", () => {
|
||||
expect(createNextcloudCalendarServerUrl("https://cloud.example.com")).toBe(
|
||||
"https://cloud.example.com/remote.php/dav/",
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves the base path for a subpath installation", () => {
|
||||
expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud")).toBe(
|
||||
"https://example.com/nextcloud/remote.php/dav/",
|
||||
);
|
||||
});
|
||||
|
||||
test("does not duplicate a trailing slash", () => {
|
||||
expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud/")).toBe(
|
||||
"https://example.com/nextcloud/remote.php/dav/",
|
||||
);
|
||||
});
|
||||
|
||||
test("does not duplicate an existing DAV endpoint", () => {
|
||||
expect(createNextcloudCalendarServerUrl("https://example.com/nextcloud/remote.php/dav")).toBe(
|
||||
"https://example.com/nextcloud/remote.php/dav/",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -6456,8 +6456,7 @@
|
||||
},
|
||||
"progress": {
|
||||
"title": "Restoring database...",
|
||||
"restarting": "Restarting server",
|
||||
"relogin": "Restoring the database invalidated your current session. Homarr will send you to sign in when the server is ready."
|
||||
"restarting": "Restarting server"
|
||||
},
|
||||
"timeout": {
|
||||
"title": "Server did not come back online",
|
||||
|
||||
Reference in new issue
Block a user