Compare commits

..
Author SHA1 Message Date
ajnart 268c864801 fix: narrow PR to verified bug fixes 2026-08-22 10:52:15 +02:00
ajnart 3f9941f9c7 Merge remote-tracking branch 'origin/dev' into feat/pr-6510-polish 2026-08-22 10:47:15 +02:00
ajnart 6868e4b8d2 test(auth): assert complete membership set for invalid OIDC claim
Address CodeRabbit: query all memberships instead of findFirst so a
handler that preserves the existing row but adds a membership is caught.
2026-08-08 12:25:11 +02:00
ajnart cc4e11fce4 Merge remote-tracking branch 'origin/dev' into fix/three-xs-fixes
# Conflicts:
#	apps/nextjs/src/components/board/items/widget-context-menu.tsx
2026-08-08 12:07:10 +02:00
ajnart 71b986f249 fix: gate scrollbar resize guard on size, preserve parse causes
Address CodeRabbit review on #6510:
- gridstack: only suppress recalculation when the width delta is
  insignificant AND (non-dynamic OR height unchanged), so dynamic
  sections still rebuild on height changes
- beszel: propagate the response parse failure via { cause } in both
  error wrappers
- auth tests: cover invalid (non-string) OIDC groups claims
2026-08-08 11:35:42 +02:00
ajnart e0ebdd9047 test(auth): add regression test for single-string OIDC groups claim 2026-08-08 11:16:39 +02:00
Thomas Camlong b2a4dcad1c fix: revert ineffective Uptime-Kuma paused fix; harden Unraid CPU divisor
The Uptime-Kuma status-page API (toPublicJSON) does not emit a status
field on monitor objects, so the added status schema field and paused
early-return were dead code and did not resolve #6002. Paused monitors
stop producing heartbeats and the two consumed endpoints expose no
paused signal, so revert the change.

Unraid CPU: divide by cpus.length (the array actually being averaged)
instead of info.cpu.threads, which is more robust if the two ever
disagree, while keeping the SMT over-reporting fix.
2026-08-03 17:41:24 +02:00
ajnart 751dd751e0 style: auto-fix lint and format 2026-08-03 15:31:09 +00:00
Thomas Camlong 7dd7d9280a chore: remove leaked docs sitemap entry from this PR 2026-08-03 17:30:00 +02:00
Thomas Camlong ebd8e00119 fix: scope app right-click bypass to view mode only 2026-08-03 17:21:38 +02:00
ajnart c51dbca9bc style: auto-fix lint and format 2026-08-03 15:17:53 +00:00
Thomas Camlong 60b0f59fe7 chore: remove board hotkeys from this PR (owned by #6393) 2026-08-03 17:16:50 +02:00
Thomas Camlong bc71cb7f16 chore: remove ISSUES.md from PR 2026-08-03 17:13:40 +02:00
Thomas Camlong 6e9136c5a0 chore: remove ISSUES.md from PR, merge BoardHotkeys into hook file 2026-08-03 17:11:04 +02:00
Thomas Camlong ef9ead60ba fix: address CodeRabbit review — guard OIDC group types and CPU division 2026-08-03 17:09:05 +02:00
ajnart ac8e0b1abe style: auto-fix lint and format 2026-08-03 15:04:34 +00:00
Thomas Camlong ce6a637220 fix: right-click apps, board keyboard shortcuts, scrollbar oscillation, Beszel JSON errors
- #6416: Skip widget context menu for app widgets (pass through native right-click)
- #6392: Add board keyboard shortcuts (mod+shift+arrows to cycle, mod+shift+1-9 for direct)
- #6080: Prevent GridStack scrollbar oscillation by thresholding resize events
- #6024: Add try-catch for Beszel response.json() calls in auth and testing paths
2026-08-03 17:03:24 +02:00
ajnart afe5f2b6b4 style: auto-fix lint and format 2026-08-03 14:52:40 +00:00
Thomas Camlong 7de2fc04ec fix: correct Unraid CPU calculation, OIDC single-group sync, and Uptime-Kuma paused status
- **Unraid CPU calculation** (#6082): Use thread count instead of core count
  when normalizing CPU utilization. Each cpus[] entry is one thread, so
  dividing by physical cores (cores) over-reports on SMT systems.

- **OIDC single-group sync** (#5716): Normalize the groups claim to always
  be an array. Some providers (e.g. Jumpcloud) send a single group as a
  plain string instead of an array, which caused the Array.isArray check
  to skip group synchronization entirely.

- **Uptime-Kuma paused status** (#6002): Add the status field to the
  monitor schema and check it before falling back to heartbeat inference.
  When a monitor is paused, heartbeats stop arriving, so the latest
  heartbeat may still show 'up' from before pausing. The status page
  returns status=2 for paused monitors, which this change now respects.
2026-08-03 16:51:21 +02:00
35 changed files with 578 additions and 1338 deletions

No files matched your search

-2
View File
@@ -33,8 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.76.2
- 1.76.1
- 1.76.0
- 1.75.0
- 1.74.0
-131
View File
@@ -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
@@ -68,6 +68,8 @@ jobs:
with:
node-version: 24.18.0
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
- name: Install dependencies
if: env.SKIP_RELEASE == 'false'
run: |
-4
View File
@@ -1,7 +1,3 @@
## [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.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
-6
View File
@@ -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.
@@ -2,9 +2,9 @@
<img src="https://raw.githubusercontent.com/homarr-labs/charts/refs/heads/main/charts/homarr/icon.svg" align="right" width="92" alt="homarr logo">
![Version: 8.28.1](https://img.shields.io/badge/Version-8.28.1-informational?style=flat)
![Version: 8.28.0](https://img.shields.io/badge/Version-8.28.0-informational?style=flat)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat)
![AppVersion: v1.76.1](https://img.shields.io/badge/AppVersion-v1.76.1-informational?style=flat)
![AppVersion: v1.76.0](https://img.shields.io/badge/AppVersion-v1.76.0-informational?style=flat)
A Helm chart to deploy homarr for Kubernetes
@@ -429,7 +429,7 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
| httproute.rules[0].matches[0].path.value | string | `"/"` | Path value to match |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| image.repository | string | `"ghcr.io/homarr-labs/homarr"` | Image repository |
| image.tag | string | `"v1.76.1"` | Overrides the image tag whose default is the chart appVersion |
| image.tag | string | `"v1.76.0"` | Overrides the image tag whose default is the chart appVersion |
| imagePullSecrets | list | `[]` | Secrets for Docker registry |
| ingress.annotations | object | `{}` | Ingress annotations |
| ingress.enabled | bool | `false` | Enable ingress |
@@ -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>
@@ -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>
);
@@ -14,7 +14,6 @@ import { db } from "@homarr/db";
import { getPackageVersion } from "~/versions/package-reader";
import { extractMcpTools } from "../_extract-tools";
import { serializeMcpToolResult } from "../_serialize-result";
const logger = createLogger({ module: "mcpRoute" });
@@ -204,7 +203,7 @@ const mcpHandler = createMcpHandler(
const input = args && Object.keys(args).length > 0 ? args : undefined;
const result = await procedure(input);
return {
content: [{ type: "text" as const, text: serializeMcpToolResult(result) }],
content: [{ type: "text" as const, text: JSON.stringify(result) }],
};
} catch (error) {
const message = sanitizeErrorMessage(error);
@@ -1,23 +0,0 @@
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");
});
});
@@ -1,3 +0,0 @@
export function serializeMcpToolResult(result: unknown): string {
return JSON.stringify(result) ?? "null";
}
@@ -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 -25
View File
@@ -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 />
</>
);
+1 -3
View File
@@ -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";
@@ -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&apos;ve been cooking, Homarr v2 beta is here
</Text>
<Text className={classes.subtitle} visibleFrom="sm" size="xs" truncate>
Custom Widgets &amp; 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&apos;ve been working on Homarr&apos;s v2 update for a while now. There is no fixed release date yet, it
will be ready when it&apos;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>&lt;SECRET_KEY&gt;</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>
);
};
+2 -2
View File
@@ -9,7 +9,7 @@ http {
# Route websockets traffic to port 3001
location /websockets {
proxy_pass http://127.0.0.1:3001;
proxy_pass http://${HOSTNAME}:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
@@ -18,7 +18,7 @@ http {
# Route all other traffic to port 3000
location / {
proxy_pass http://127.0.0.1:3000;
proxy_pass http://${HOSTNAME}:3000;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.76.2",
"version": "1.76.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
+1 -1
View File
@@ -43,7 +43,7 @@ async function importCookiesAsync() {
function getBaseUrl() {
if (typeof window !== "undefined") return window.location.origin;
return "http://127.0.0.1:3000";
return `http://${process.env.HOSTNAME ?? "localhost"}:3000`;
}
export const trpcPath = "/api/trpc";
@@ -13,6 +13,11 @@ import { UndiciHttpAgent } from "@homarr/core/infrastructure/http";
import type { TrustedCertificateHostname } from "../certificates/hostnames";
import { withTimeoutAsync } from "./timeout";
import packageJson from "../../../../../package.json";
export const getDefaultUserAgent = () => `Homarr/${packageJson.version} (+https://homarr.dev)`;
export const createCustomCheckServerIdentity = (
trustedHostnames: TrustedCertificateHostname[],
): typeof checkServerIdentity => {
@@ -78,6 +83,10 @@ export const fetchWithTrustedCertificatesAsync = async (
async (signal) =>
fetch(url, {
...fetchOptions,
headers: {
"User-Agent": getDefaultUserAgent(),
...fetchOptions.headers,
},
signal,
dispatcher: agent,
}),
@@ -88,6 +97,10 @@ export const fetchWithTrustedCertificatesAsync = async (
const { bodyTimeout: _bodyTimeout, dispatcher: _dispatcher, ...fetchOptions } = options ?? {};
return fetch(url, {
...fetchOptions,
headers: {
"User-Agent": getDefaultUserAgent(),
...fetchOptions.headers,
},
dispatcher: agent,
});
};
-1
View File
@@ -1,3 +1,2 @@
export const colorSchemeCookieKey = "homarr.color-scheme";
export const localeCookieKey = "homarr.locale";
export const v2BetaAnnouncementCookieKey = "homarr.v2-beta-announcement";
@@ -1,39 +0,0 @@
// @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();
}
});
});
+10 -10
View File
@@ -3163,8 +3163,8 @@
"noColumns": "Spalten in Elementen auswählen",
"noCommunications": "Kann keine Daten von der Integration laden",
"noItems": "",
"actionFailed": "Aktion fehlgeschlagen",
"actionFailedMessage": "Diese Aktion konnte nicht abgeschlossen werden. Bitte versuche es erneut."
"actionFailed": "",
"actionFailedMessage": ""
},
"items": {
"actions": {
@@ -3452,7 +3452,7 @@
"queued": ""
},
"type": {
"movie": "Film",
"movie": "",
"episode": ""
},
"empty": {
@@ -3812,7 +3812,7 @@
},
"resource": {
"routers": "",
"services": "Dienste",
"services": "",
"middlewares": ""
},
"status": {
@@ -4098,7 +4098,7 @@
"temp": "",
"battery": "",
"services": "Dienste",
"uptime": "Verfügbarkeit",
"uptime": "",
"agent": "Agent"
}
},
@@ -4173,12 +4173,12 @@
"chart": {
"cpu": {
"title": "CPU Auslastung",
"subtitle": "Durchschnittliche Systemweite CPU-Auslastung",
"subtitle": "",
"series": "CPU"
},
"memory": {
"title": "Speicherverbrauch",
"subtitle": "Genaue Auslastung zum erfassten Zeitpunkt",
"title": "",
"subtitle": "",
"series": "Belegt",
"cache": ""
},
@@ -4189,7 +4189,7 @@
},
"diskIO": {
"title": "Festplatten-E/A",
"subtitle": "Durchsatz des Root-Dateisystems",
"subtitle": "",
"read": "Lesen",
"write": "Schreiben"
},
@@ -6096,7 +6096,7 @@
}
},
"availability": {
"available": "Verfügbar",
"available": "",
"partiallyAvailable": "",
"processing": "",
"requested": "",
+21 -21
View File
@@ -3179,7 +3179,7 @@
"detailsTitle": ""
},
"downSpeed": {
"columnTitle": "Down",
"columnTitle": "",
"detailsTitle": ""
},
"index": {
@@ -3194,10 +3194,10 @@
"columnTitle": ""
},
"name": {
"columnTitle": "Job name"
"columnTitle": ""
},
"progress": {
"columnTitle": "Progress",
"columnTitle": "",
"detailsTitle": ""
},
"ratio": {
@@ -3217,7 +3217,7 @@
"detailsTitle": ""
},
"state": {
"columnTitle": "State",
"columnTitle": "",
"detailsTitle": ""
},
"time": {
@@ -3315,7 +3315,7 @@
}
},
"mediaRequests-requestList": {
"name": "Media Requests List",
"name": "",
"description": "",
"option": {
"linksTargetNewTab": {
@@ -3337,15 +3337,15 @@
"unknown": "",
"pending": "",
"processing": "",
"requested": "Requested",
"requested": "",
"partiallyAvailable": "",
"available": "",
"blacklisted": "Blacklisted",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
"approved": "Approved",
"approved": "",
"declined": "",
"failed": "",
"completed": ""
@@ -3358,7 +3358,7 @@
"option": {},
"titles": {
"stats": {
"main": "Media Stats",
"main": "",
"approved": "",
"pending": "",
"processing": "",
@@ -3402,15 +3402,15 @@
"panel": {
"statistics": {
"empty": "",
"transcodes": "Transcodes",
"transcodes": "",
"transcodesCount": "",
"healthChecksCount": "",
"filesCount": "",
"savedSpace": "",
"healthChecks": "Health checks",
"videoCodecs": "Codecs",
"videoContainers": "Containers",
"videoResolutions": "Resolutions"
"healthChecks": "",
"videoCodecs": "",
"videoContainers": "",
"videoResolutions": ""
},
"workers": {
"empty": "",
@@ -3620,7 +3620,7 @@
"option": {},
"card": {
"vpn": {
"countConnected": "{count} connected"
"countConnected": ""
}
},
"error": {
@@ -3637,14 +3637,14 @@
"name": ""
},
"wifi": {
"name": "Wi-Fi"
"name": ""
}
},
"users": {
"label": ""
},
"guests": {
"label": "Guests"
"label": ""
}
},
"option": {
@@ -3890,7 +3890,7 @@
"option": {
"cpu": "",
"memory": "",
"gpu": "GPU",
"gpu": "",
"network": ""
}
},
@@ -3908,7 +3908,7 @@
"cpu": "",
"memory": "",
"gpu": "",
"network": "NET",
"network": "",
"up": "",
"down": ""
}
@@ -4956,7 +4956,7 @@
"title": "",
"status": {
"idle": "",
"running": "Running",
"running": "",
"error": "",
"disabled": ""
},
@@ -5858,7 +5858,7 @@
}
},
"search": {
"placeholder": "Search for anything",
"placeholder": "",
"nothingFound": "",
"error": {
"fetch": ""
+6 -6
View File
@@ -730,15 +730,15 @@
"iconUrl": "URL del icono",
"url": "URL",
"authType": "Tipo de autenticación",
"headerName": "Nombre del parámetro de cabecera/consulta",
"headerName": "",
"method": "Método",
"requestBody": "Cuerpo de la Solicitud",
"requestBody": "",
"displayType": "Tipo de visualización",
"jsonPath": "JSON Path",
"jsonPathHint": "Usa la sintaxis JSONPath para extraer valores, por ejemplo, $.data.temperature o $.items[0].name",
"label": "Nombre",
"jsonPath": "",
"jsonPathHint": "",
"label": "",
"unit": "Unidad",
"mappings": "Asignaciones",
"mappings": "",
"tablePath": "",
"tablePathHint": "",
"columns": "",
+7 -7
View File
@@ -604,7 +604,7 @@
}
},
"app": {
"search": "Trouver une app",
"search": "Trouver une application",
"page": {
"list": {
"title": "Applications",
@@ -3358,7 +3358,7 @@
"option": {},
"titles": {
"stats": {
"main": "Stats des médias",
"main": "Statistiques des médias",
"approved": "Déjà approuvé",
"pending": "En attente de validation",
"processing": "En cours de traitement",
@@ -4118,7 +4118,7 @@
"internalServerError": ""
},
"history": "Historique",
"resolved": "Résolu",
"resolved": "Résolut",
"status": {
"triggered": "",
"ok": ""
@@ -4173,12 +4173,12 @@
"chart": {
"cpu": {
"title": "",
"subtitle": "Utilisation moyenne du CPU à l'échelle du système",
"subtitle": "",
"series": "CPU"
},
"memory": {
"title": "Utilisation Mémoire",
"subtitle": "Utilisation précise au moment enregistré",
"subtitle": "",
"series": "",
"cache": ""
},
@@ -4189,7 +4189,7 @@
},
"diskIO": {
"title": "",
"subtitle": "Débit du système de fichiers root",
"subtitle": "",
"read": "",
"write": ""
},
@@ -5766,7 +5766,7 @@
"label": "Médias"
},
"apps": {
"label": "Apps",
"label": "Applications",
"new": {
"label": "Nouveau"
},
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -3452,7 +3452,7 @@
"queued": ""
},
"type": {
"movie": "Film",
"movie": "",
"episode": ""
},
"empty": {
+2 -2
View File
@@ -4887,11 +4887,11 @@
"text": "Не індексувати сайт у пошукових системах і не показувати його в жодному результаті пошуку"
},
"noFollow": {
"title": "Не переходити",
"title": "Не переходити (No follow)",
"text": "Не переходьте за посиланнями під час індексації. Якщо вимкнути це, то сканери намагатимуться переходити за всіма посиланнями на Homarr."
},
"noTranslate": {
"title": "Не перекладати",
"title": "Не перекладати (No translate)",
"text": "Якщо мова сайту не є тією, яку користувач, імовірно, захоче прочитати, Google покаже посилання на переклад у результатах пошуку"
},
"noSiteLinksSearchBox": {
+5 -6
View File
@@ -21,18 +21,20 @@ fi
export AUTH_SECRET=$(openssl rand -base64 32)
# Start nginx proxy
# 1. Create the nginx configuration file from the template
# 2. Start the nginx server
# 1. Replace the HOSTNAME in the nginx template file
# 2. Create the nginx configuration file from the template
# 3. Start the nginx server
# Only listen on IPv6 when the host actually has IPv6 configured (#4596).
# Otherwise nginx aborts with "socket() [::]:7575 failed (97: Address family
# 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
# addresses via grep (not -s: proc files always report size 0).
export HOSTNAME
export NGINX_LISTEN_IPV6=''
if [ "${HOMARR_DISABLE_IPV6:-false}" != "true" ] && grep -q . /proc/net/if_inet6 2>/dev/null; then
NGINX_LISTEN_IPV6='listen [::]:7575;'
fi
envsubst '${NGINX_LISTEN_IPV6}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
envsubst '${HOSTNAME} ${NGINX_LISTEN_IPV6}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
# Start services in the background and store their PIDs
nginx -g 'daemon off;' &
NGINX_PID=$!
@@ -65,9 +67,6 @@ terminate() {
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 &
NEXTJS_PID=$!
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"beanbag420","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontentLine truncated
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"beanbag420","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontentLine truncated
+1 -1
View File
@@ -1 +1 @@
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/aaa1f083e04c572f953fde4563171e4a.jpeg"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"https://crowLine truncated
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/aaa1f083e04c572f953fde4563171e4a.jpeg"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14861042/medium/2d5c4e62613f03082f3e645fa92efd59.jpeg"},{"username":"djismgaming","avatarUrl":"https://crowLine truncated