Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
331925c197 | ||
|
|
0e51f745d3 | ||
|
|
d6f720d0e6 | ||
|
|
5324bf12d4 | ||
|
|
87cdcf28e0 | ||
|
|
441b181e79 | ||
|
|
eba639b59c | ||
|
|
da36283ad1 | ||
|
|
c050368782 | ||
|
|
82b50f0fd7 | ||
|
|
a0149d5894 | ||
|
|
8579fcbfc2 | ||
|
|
b6f3aba23e | ||
|
|
31e218295d | ||
|
|
aca3b5d861 |
No files matched your search
@@ -33,6 +33,7 @@ body:
|
|||||||
options:
|
options:
|
||||||
# The below comment is used to insert a new version with on-release.yml
|
# The below comment is used to insert a new version with on-release.yml
|
||||||
#NEXT_VERSION#
|
#NEXT_VERSION#
|
||||||
|
- 1.76.1
|
||||||
- 1.76.0
|
- 1.76.0
|
||||||
- 1.75.0
|
- 1.75.0
|
||||||
- 1.74.0
|
- 1.74.0
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## [1.76.2](https://github.com/homarr-labs/homarr/compare/v1.76.1...v1.76.2) (2026-08-27)
|
||||||
|
|
||||||
|
## [1.76.1](https://github.com/homarr-labs/homarr/compare/v1.76.0...v1.76.1) (2026-08-26)
|
||||||
|
|
||||||
## [1.76.0](https://github.com/homarr-labs/homarr/compare/v1.75.0...v1.76.0) (2026-08-21)
|
## [1.76.0](https://github.com/homarr-labs/homarr/compare/v1.75.0...v1.76.0) (2026-08-21)
|
||||||
|
|
||||||
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
|
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
---
|
---
|
||||||
title: 'App'
|
title: "App"
|
||||||
description: 'Embeds an app into the board.'
|
description: "Embeds an app into the board."
|
||||||
hide_title: true
|
hide_title: true
|
||||||
---
|
---
|
||||||
|
|
||||||
import { AddingWidget } from '@site/src/components/widgets/adding';
|
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||||
import { WidgetConfig } from '@site/src/components/widgets/configuration';
|
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||||
import { WidgetHeader } from '@site/src/components/widgets/header';
|
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||||
import { appWidget } from '.';
|
import { appWidget } from ".";
|
||||||
|
|
||||||
|
<WidgetHeader widget={appWidget} categories={["Links", "Apps"]} />
|
||||||
<WidgetHeader
|
|
||||||
widget={appWidget}
|
|
||||||
categories={['Links', 'Apps']}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The app widget allows you to add a link to an application or a website that you want to access quickly from your Homarr board.
|
The app widget allows you to add a link to an application or a website that you want to access quickly from your Homarr board.
|
||||||
It also supports a status check to indicate whether Homarr can reach the application or website.
|
It also supports a status check to indicate whether Homarr can reach the application or website.
|
||||||
|
In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
import githubApp from './img/github.png';
|
import githubApp from "./img/github.png";
|
||||||
import jellyseerrApp from './img/jellyseerr-description-tooltip.png';
|
import jellyseerrApp from "./img/jellyseerr-description-tooltip.png";
|
||||||
import piHoleApp from './img/pi-hole-ping-icon.png';
|
import piHoleApp from "./img/pi-hole-ping-icon.png";
|
||||||
|
|
||||||
<div className='flex gap-1'>
|
<div className="flex gap-1">
|
||||||
<img src={githubApp} alt="app github with-status-check" width={128 * 1.5} height={128 * 1.5} />
|
<img src={githubApp} alt="app github with-status-check" width={128 * 1.5} height={128 * 1.5} />
|
||||||
<img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} />
|
<img src={piHoleApp} alt="app pi-hole with-status-check status-check-icon" width={128 * 1.5} height={128 * 1.5} />
|
||||||
<img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} />
|
<img src={jellyseerrApp} alt="app jellyseerr with-description-tooltip" height={128 * 1.5} />
|
||||||
@@ -34,4 +31,5 @@ import piHoleApp from './img/pi-hole-ping-icon.png';
|
|||||||
<AddingWidget />
|
<AddingWidget />
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
<WidgetConfig configuration={appWidget.configuration} />
|
<WidgetConfig configuration={appWidget.configuration} />
|
||||||
@@ -1,29 +1,26 @@
|
|||||||
---
|
---
|
||||||
title: 'Bookmarks'
|
title: "Bookmarks"
|
||||||
description: 'Displays multiple app links'
|
description: "Displays multiple app links"
|
||||||
hide_title: true
|
hide_title: true
|
||||||
---
|
---
|
||||||
|
|
||||||
import { AddingWidget } from '@site/src/components/widgets/adding';
|
import { AddingWidget } from "@site/src/components/widgets/adding";
|
||||||
import { WidgetConfig } from '@site/src/components/widgets/configuration';
|
import { WidgetConfig } from "@site/src/components/widgets/configuration";
|
||||||
import { WidgetHeader } from '@site/src/components/widgets/header';
|
import { WidgetHeader } from "@site/src/components/widgets/header";
|
||||||
import { bookmarksWidget } from '.';
|
import { bookmarksWidget } from ".";
|
||||||
|
|
||||||
|
<WidgetHeader widget={bookmarksWidget} categories={["Links", "Apps"]} />
|
||||||
<WidgetHeader
|
|
||||||
widget={bookmarksWidget}
|
|
||||||
categories={['Links', 'Apps']}
|
|
||||||
/>
|
|
||||||
|
|
||||||
The bookmarks widget allows you to display multiple app links in a single widget.
|
The bookmarks widget allows you to display multiple app links in a single widget.
|
||||||
You can add links to your favorite applications, websites, or any other resources you want quick access to.
|
You can add links to your favorite applications, websites, or any other resources you want quick access to.
|
||||||
|
In view mode, right-clicking uses the browser's link menu. Homarr's widget menu remains available in edit mode.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
import gridIconsOnly from './img/grid-icons-only.png';
|
import gridIconsOnly from "./img/grid-icons-only.png";
|
||||||
import gridWithTitle from './img/grid-with-title.png';
|
import gridWithTitle from "./img/grid-with-title.png";
|
||||||
import verticalListAll from './img/vertical-list-all.png';
|
import verticalListAll from "./img/vertical-list-all.png";
|
||||||
import vericalListSimple from './img/vertical-list-simple.png';
|
import vericalListSimple from "./img/vertical-list-simple.png";
|
||||||
|
|
||||||
<div className='flex gap-1'>
|
<div className='flex gap-1'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col gap-1'>
|
||||||
@@ -33,12 +30,13 @@ import vericalListSimple from './img/vertical-list-simple.png';
|
|||||||
|
|
||||||
<img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} />
|
<img src={gridWithTitle} alt="bookmarks grid with-title" width={256 + 4} height={256 + 4} />
|
||||||
<img src={verticalListAll} alt="bookmarks vertical-list all-elements" width={128} height={256 + 4} />
|
<img src={verticalListAll} alt="bookmarks vertical-list all-elements" width={128} height={256 + 4} />
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
### Adding the widget
|
### Adding the widget
|
||||||
|
|
||||||
<AddingWidget />
|
<AddingWidget />
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
<WidgetConfig configuration={bookmarksWidget.configuration} />
|
<WidgetConfig configuration={bookmarksWidget.configuration} />
|
||||||
@@ -14,6 +14,7 @@ import { db } from "@homarr/db";
|
|||||||
|
|
||||||
import { getPackageVersion } from "~/versions/package-reader";
|
import { getPackageVersion } from "~/versions/package-reader";
|
||||||
import { extractMcpTools } from "../_extract-tools";
|
import { extractMcpTools } from "../_extract-tools";
|
||||||
|
import { serializeMcpToolResult } from "../_serialize-result";
|
||||||
|
|
||||||
const logger = createLogger({ module: "mcpRoute" });
|
const logger = createLogger({ module: "mcpRoute" });
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ const mcpHandler = createMcpHandler(
|
|||||||
const input = args && Object.keys(args).length > 0 ? args : undefined;
|
const input = args && Object.keys(args).length > 0 ? args : undefined;
|
||||||
const result = await procedure(input);
|
const result = await procedure(input);
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text" as const, text: JSON.stringify(result) }],
|
content: [{ type: "text" as const, text: serializeMcpToolResult(result) }],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = sanitizeErrorMessage(error);
|
const message = sanitizeErrorMessage(error);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { serializeMcpToolResult } from "./_serialize-result";
|
||||||
|
|
||||||
|
describe("serializeMcpToolResult", () => {
|
||||||
|
it("serializes objects as JSON strings", () => {
|
||||||
|
expect(serializeMcpToolResult({ boardId: "1" })).toBe('{"boardId":"1"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes primitive values", () => {
|
||||||
|
expect(serializeMcpToolResult("hello")).toBe('"hello"');
|
||||||
|
expect(serializeMcpToolResult(42)).toBe("42");
|
||||||
|
expect(serializeMcpToolResult(true)).toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes null as the string null", () => {
|
||||||
|
expect(serializeMcpToolResult(null)).toBe("null");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("serializes void results as the string null so MCP content text is always a string", () => {
|
||||||
|
expect(serializeMcpToolResult(undefined)).toBe("null");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function serializeMcpToolResult(result: unknown): string {
|
||||||
|
return JSON.stringify(result) ?? "null";
|
||||||
|
}
|
||||||
@@ -44,9 +44,13 @@ interface WidgetContextMenuProps {
|
|||||||
|
|
||||||
export const WidgetContextMenu = ({ item, definition, widgetStateRef, children }: WidgetContextMenuProps) => {
|
export const WidgetContextMenu = ({ item, definition, widgetStateRef, children }: WidgetContextMenuProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
|
const [isEditMode] = useEditMode();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
if (!session || !settings.enableRightClickOnWidgets) return <>{children}</>;
|
if (!session || !settings.enableRightClickOnWidgets) return <>{children}</>;
|
||||||
|
const isLinkWidget = item.kind === "app" || item.kind === "bookmarks";
|
||||||
|
// Keep the Homarr menu in edit mode so these widgets can still be edited or removed.
|
||||||
|
if (isLinkWidget && !isEditMode) return <>{children}</>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetContextMenuInner item={item} definition={definition} widgetStateRef={widgetStateRef} settings={settings}>
|
<WidgetContextMenuInner item={item} definition={definition} widgetStateRef={widgetStateRef} settings={settings}>
|
||||||
|
|||||||
@@ -301,6 +301,8 @@ interface UseCssVariableConfiguration {
|
|||||||
isDynamic: boolean;
|
isDynamic: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minimumResizeWidthDelta = 20;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook is used to configure the css variables for the gridstack
|
* This hook is used to configure the css variables for the gridstack
|
||||||
* Those css variables are used to define the size of the gridstack items
|
* Those css variables are used to define the size of the gridstack items
|
||||||
@@ -319,9 +321,30 @@ const useCssVariableConfiguration = ({
|
|||||||
columnCount,
|
columnCount,
|
||||||
isDynamic,
|
isDynamic,
|
||||||
}: UseCssVariableConfiguration) => {
|
}: UseCssVariableConfiguration) => {
|
||||||
|
const prevSizeRef = useRef<{ width: number; height: number } | null>(null);
|
||||||
|
|
||||||
const onResize = useCallback(() => {
|
const onResize = useCallback(() => {
|
||||||
if (!wrapperRef.current) return;
|
if (!wrapperRef.current) return;
|
||||||
if (!gridRef.current) return;
|
if (!gridRef.current) return;
|
||||||
|
|
||||||
|
const currentWidth = wrapperRef.current.clientWidth;
|
||||||
|
const currentHeight = wrapperRef.current.clientHeight;
|
||||||
|
const prevSize = prevSizeRef.current;
|
||||||
|
|
||||||
|
// Only skip recalculation when the width change is insignificant
|
||||||
|
// and, for dynamic sections, the height is unchanged. This prevents infinite
|
||||||
|
// feedback loops when the vertical scrollbar appears/disappears, causing
|
||||||
|
// clientWidth to oscillate by ~15px, while still reacting to height changes.
|
||||||
|
if (
|
||||||
|
prevSize !== null &&
|
||||||
|
Math.abs(currentWidth - prevSize.width) < minimumResizeWidthDelta &&
|
||||||
|
(!isDynamic || currentHeight === prevSize.height)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSizeRef.current = { width: currentWidth, height: currentHeight };
|
||||||
|
|
||||||
handleResizeChange(
|
handleResizeChange(
|
||||||
wrapperRef.current,
|
wrapperRef.current,
|
||||||
gridRef.current,
|
gridRef.current,
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ http {
|
|||||||
|
|
||||||
# Route websockets traffic to port 3001
|
# Route websockets traffic to port 3001
|
||||||
location /websockets {
|
location /websockets {
|
||||||
proxy_pass http://${HOSTNAME}:3001;
|
proxy_pass http://127.0.0.1:3001;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "Upgrade";
|
proxy_set_header Connection "Upgrade";
|
||||||
@@ -18,7 +18,7 @@ http {
|
|||||||
|
|
||||||
# Route all other traffic to port 3000
|
# Route all other traffic to port 3000
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://${HOSTNAME}:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homarr",
|
"name": "homarr",
|
||||||
"version": "1.76.0",
|
"version": "1.76.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env CI=true turbo build",
|
"build": "cross-env CI=true turbo build",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ async function importCookiesAsync() {
|
|||||||
|
|
||||||
function getBaseUrl() {
|
function getBaseUrl() {
|
||||||
if (typeof window !== "undefined") return window.location.origin;
|
if (typeof window !== "undefined") return window.location.origin;
|
||||||
return `http://${process.env.HOSTNAME ?? "localhost"}:3000`;
|
return "http://127.0.0.1:3000";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const trpcPath = "/api/trpc";
|
export const trpcPath = "/api/trpc";
|
||||||
|
|||||||
@@ -13,11 +13,6 @@ import { UndiciHttpAgent } from "@homarr/core/infrastructure/http";
|
|||||||
|
|
||||||
import type { TrustedCertificateHostname } from "../certificates/hostnames";
|
import type { TrustedCertificateHostname } from "../certificates/hostnames";
|
||||||
import { withTimeoutAsync } from "./timeout";
|
import { withTimeoutAsync } from "./timeout";
|
||||||
|
|
||||||
import packageJson from "../../../../../package.json";
|
|
||||||
|
|
||||||
export const getDefaultUserAgent = () => `Homarr/${packageJson.version} (+https://homarr.dev)`;
|
|
||||||
|
|
||||||
export const createCustomCheckServerIdentity = (
|
export const createCustomCheckServerIdentity = (
|
||||||
trustedHostnames: TrustedCertificateHostname[],
|
trustedHostnames: TrustedCertificateHostname[],
|
||||||
): typeof checkServerIdentity => {
|
): typeof checkServerIdentity => {
|
||||||
@@ -83,10 +78,6 @@ export const fetchWithTrustedCertificatesAsync = async (
|
|||||||
async (signal) =>
|
async (signal) =>
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
...fetchOptions,
|
...fetchOptions,
|
||||||
headers: {
|
|
||||||
"User-Agent": getDefaultUserAgent(),
|
|
||||||
...fetchOptions.headers,
|
|
||||||
},
|
|
||||||
signal,
|
signal,
|
||||||
dispatcher: agent,
|
dispatcher: agent,
|
||||||
}),
|
}),
|
||||||
@@ -97,10 +88,6 @@ export const fetchWithTrustedCertificatesAsync = async (
|
|||||||
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options ?? {};
|
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options ?? {};
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...fetchOptions,
|
...fetchOptions,
|
||||||
headers: {
|
|
||||||
"User-Agent": getDefaultUserAgent(),
|
|
||||||
...fetchOptions.headers,
|
|
||||||
},
|
|
||||||
dispatcher: agent,
|
dispatcher: agent,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -37,9 +37,16 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
|
|||||||
const systemInfo = await this.getSystemInformationAsync();
|
const systemInfo = await this.getSystemInformationAsync();
|
||||||
|
|
||||||
const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0);
|
const cpuUtilization = systemInfo.metrics.cpu.cpus.reduce((acc, val) => acc + val.percentTotal, 0);
|
||||||
const cpuCount = systemInfo.info.cpu.cores;
|
const cpuCount = systemInfo.metrics.cpu.cpus.length;
|
||||||
|
|
||||||
|
// Each entry in cpus[] is one logical CPU (thread). Dividing the summed per-thread
|
||||||
|
// utilization by the thread count (not physical cores) gives the correct average and
|
||||||
|
// avoids over-reporting on SMT systems. Guard against zero to avoid Infinity/NaN.
|
||||||
|
let cpuUtilizationNormalized = 0;
|
||||||
|
if (cpuCount > 0) {
|
||||||
|
cpuUtilizationNormalized = cpuUtilization / cpuCount;
|
||||||
|
}
|
||||||
|
|
||||||
// We use "info" object instead of the stats since this is the exact amount the kernel sees, which is what Unraid displays.
|
|
||||||
const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
|
const totalMemory = systemInfo.info.memory.layout.reduce((acc, layout) => layout.size + acc, 0);
|
||||||
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
|
const usedMemory = totalMemory * (systemInfo.metrics.memory.percentTotal / 100);
|
||||||
const uptime = dayjs(systemInfo.info.os.uptime);
|
const uptime = dayjs(systemInfo.info.os.uptime);
|
||||||
@@ -47,7 +54,7 @@ export class UnraidIntegration extends Integration implements ISystemHealthMonit
|
|||||||
return {
|
return {
|
||||||
version: systemInfo.info.os.release,
|
version: systemInfo.info.os.release,
|
||||||
cpuModelName: systemInfo.info.cpu.brand,
|
cpuModelName: systemInfo.info.cpu.brand,
|
||||||
cpuUtilization: cpuUtilization / cpuCount,
|
cpuUtilization: cpuUtilizationNormalized,
|
||||||
memUsedInBytes: usedMemory,
|
memUsedInBytes: usedMemory,
|
||||||
memAvailableInBytes: totalMemory - usedMemory,
|
memAvailableInBytes: totalMemory - usedMemory,
|
||||||
uptime: dayjs().diff(uptime, "seconds"),
|
uptime: dayjs().diff(uptime, "seconds"),
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
|
||||||
|
import { createServer } from "node:http";
|
||||||
|
import type { AddressInfo } from "node:net";
|
||||||
|
|
||||||
|
import { getServerStatistics, init } from "@immich/sdk";
|
||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||||
|
|
||||||
|
vi.mock("@homarr/core/infrastructure/certificates", () => ({
|
||||||
|
getAllTrustedCertificatesAsync: vi.fn(async () => []),
|
||||||
|
getTrustedCertificateHostnamesAsync: vi.fn(async () => []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("real @immich/sdk end-to-end through fetchWithTrustedCertificatesAsync", () => {
|
||||||
|
test("x-api-key set via init() reaches the server", async () => {
|
||||||
|
let receivedXApiKey: string | string[] | undefined;
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
receivedXApiKey = req.headers["x-api-key"];
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json" });
|
||||||
|
res.end(JSON.stringify({ usage: 1000, photos: 5, videos: 2 }));
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, resolve));
|
||||||
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
|
||||||
|
init({ baseUrl: `http://127.0.0.1:${port}/api`, apiKey: "sdk-configured-key" });
|
||||||
|
try {
|
||||||
|
const stats = await getServerStatistics({
|
||||||
|
fetch: fetchWithTrustedCertificatesAsync as unknown as typeof fetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(receivedXApiKey).toBe("sdk-configured-key");
|
||||||
|
expect(stats).toMatchObject({ usage: 1000, photos: 5, videos: 2 });
|
||||||
|
} finally {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
+6
-5
@@ -21,20 +21,18 @@ fi
|
|||||||
export AUTH_SECRET=$(openssl rand -base64 32)
|
export AUTH_SECRET=$(openssl rand -base64 32)
|
||||||
|
|
||||||
# Start nginx proxy
|
# Start nginx proxy
|
||||||
# 1. Replace the HOSTNAME in the nginx template file
|
# 1. Create the nginx configuration file from the template
|
||||||
# 2. Create the nginx configuration file from the template
|
# 2. Start the nginx server
|
||||||
# 3. Start the nginx server
|
|
||||||
# Only listen on IPv6 when the host actually has IPv6 configured (#4596).
|
# Only listen on IPv6 when the host actually has IPv6 configured (#4596).
|
||||||
# Otherwise nginx aborts with "socket() [::]:7575 failed (97: Address family
|
# Otherwise nginx aborts with "socket() [::]:7575 failed (97: Address family
|
||||||
# not supported by protocol)". Check HOMARR_DISABLE_IPV6 first so users can
|
# not supported by protocol)". Check HOMARR_DISABLE_IPV6 first so users can
|
||||||
# force IPv4-only even when the host supports IPv6, then probe for IPv6
|
# force IPv4-only even when the host supports IPv6, then probe for IPv6
|
||||||
# addresses via grep (not -s: proc files always report size 0).
|
# addresses via grep (not -s: proc files always report size 0).
|
||||||
export HOSTNAME
|
|
||||||
export NGINX_LISTEN_IPV6=''
|
export NGINX_LISTEN_IPV6=''
|
||||||
if [ "${HOMARR_DISABLE_IPV6:-false}" != "true" ] && grep -q . /proc/net/if_inet6 2>/dev/null; then
|
if [ "${HOMARR_DISABLE_IPV6:-false}" != "true" ] && grep -q . /proc/net/if_inet6 2>/dev/null; then
|
||||||
NGINX_LISTEN_IPV6='listen [::]:7575;'
|
NGINX_LISTEN_IPV6='listen [::]:7575;'
|
||||||
fi
|
fi
|
||||||
envsubst '${HOSTNAME} ${NGINX_LISTEN_IPV6}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
|
envsubst '${NGINX_LISTEN_IPV6}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
|
||||||
# Start services in the background and store their PIDs
|
# Start services in the background and store their PIDs
|
||||||
nginx -g 'daemon off;' &
|
nginx -g 'daemon off;' &
|
||||||
NGINX_PID=$!
|
NGINX_PID=$!
|
||||||
@@ -67,6 +65,9 @@ terminate() {
|
|||||||
|
|
||||||
trap terminate TERM INT
|
trap terminate TERM INT
|
||||||
|
|
||||||
|
# Next.js standalone uses HOSTNAME as its bind address. Docker's generated
|
||||||
|
# hostname can be unresolvable, so bind explicitly while nginx uses loopback.
|
||||||
|
export HOSTNAME=0.0.0.0
|
||||||
node apps/nextjs/server.js &
|
node apps/nextjs/server.js &
|
||||||
NEXTJS_PID=$!
|
NEXTJS_PID=$!
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user