Compare commits

...
Author SHA1 Message Date
Afonso Jorge Ramos 59f3033a63 fix(truenas): fall back when the dataset response fails validation 2026-08-20 14:29:44 +02:00
Afonso Jorge Ramos 9231611746 docs(truenas): clarify how storage values are derived 2026-08-20 14:23:12 +02:00
Afonso Jorge Ramos 79d28f97ce fix(truenas): keep reporting pools without usable dataset space 2026-08-20 14:23:08 +02:00
Thomas f1c49d1fbf fix(startup): only chown runtime paths for custom PUID/PGID (#6598) 2026-08-19 08:29:17 +02:00
Thomas 95c66c61eb fix(widgets): stop Health Monitoring system tab firing NOT_FOUND when Proxmox is mixed in (#6623) 2026-08-18 19:56:47 +02:00
Thomas ec2f8c6c2c fix(widgets): match OMV disk temperature via normalized partition names (#6615) (#6619)
OMV reports filesystem devices (sda1) without the /dev/ prefix, so the
partition-suffix normalization never applied and filesystem-to-SMART
matching returned no temperature. Strip partition suffixes from
unprefixed sd/vd, nvme and mmcblk names, and use the shared normalized
matcher in both the health monitoring and system disks widgets.
2026-08-18 19:39:27 +02:00
Thomas 05ba57c2fa chore: migrate cursor rules to portable agent skills + mantine MCP guidance
* chore: migrate cursor rules to portable agent skills and add mantine MCP guidance

Replace Cursor-only .cursor/rules/*.mdc with Agent Skills open-standard
SKILL.md files in .agents/skills/ (canonical), mirrored to .cursor/skills/
and .claude/skills/ so they work across Claude Code, Cursor, Codex, and
OpenCode. Add mantine MCP server guidance to AGENTS.md and un-ignore
.agents/skills/ in .gitignore.

* refactor: single-source agent skills at .agents/skills

Cursor, Codex, and OpenCode all read .agents/skills/ natively, so the
.cursor/skills/ mirror was redundant. Claude Code reads .claude/skills/,
which is now a symlink back to .agents/skills/ so the content lives once.
Deletes .cursor/skills/ and converts .claude/skills/ to a symlink.

* chore: ship mantine MCP server via project-scoped .mcp.json

Declares the keyless @mantine/mcp-server in .mcp.json (Claude Code
project-scope standard, committable) and documents MCP server setup and
the mantine entry in AGENTS.md.

* chore: address coderabbit security-review and markdown lint findings

Add explicit security-review guidance before exposing MCP tools and label
plain-text code fences to satisfy markdownlint MD040.
2026-08-16 16:27:51 +02:00
Thomas ddbf05524b feat(http): set custom User-Agent on outbound integration requests (#6606)
* feat(http): set custom User-Agent on outbound integration requests

Set a default 'User-Agent: Homarr/<version> (+https://homarr.dev)' header
on all outbound HTTP requests made via fetchWithTrustedCertificatesAsync,
so Homarr's integration traffic is distinguishable in reverse-proxy,
WAF, and security-monitoring logs instead of appearing as generic undici.

Closes #6604

* refactor(version): centralize Homarr version in @homarr/version package

Replace fragile deep-relative 'package.json' imports across the monorepo
with a single source of truth: the new @homarr/version leaf package reads
the version once from the root package.json and exports it as VERSION.

Consumers now import by package name incl. the new User-Agent header in
fetchWithTrustedCertificatesAsync (packages/core):
  - core, api (info router), analytics, request-handler (update checker)
This also removes all manually-maintained relative version imports.

* fix(http): normalize User-Agent injection with Headers

Object-spreading fetchOptions.headers breaks when callers pass a Headers
instance (spread yields no entries) or a tuple array (numeric keys), and is
case-sensitive. Build a Headers instance and set the default User-Agent only
when no case-insensitive User-Agent entry exists, via a shared helper in both
fetch paths.

Adds unit tests covering record, Headers, tuple-array, and casing variants.

* revert(version): undo version refactor and tests, keep single-file change

Keep the User-Agent change as a single edit to
packages/core/src/infrastructure/http/request.ts. Remove the @homarr/version
package, the consumer migrations, and the unit tests.
2026-08-16 16:09:15 +02:00
Thomas df438b4cd2 Merge pull request #6571 from homarr-labs/feat/fix-home-assistant-actions 2026-08-16 00:23:14 +02:00
Thomas a1f91eb385 Merge pull request #6567 from homarr-labs/feat/fix-truenas-free-space 2026-08-15 20:20:35 +02:00
Thomas 8d5ffb2dd9 feat(paseo): add worktree install script (#6599)
* feat(paseo): add worktree install script

* feat(paseo): inline worktree setup in paseo.json

* feat(paseo): add scripts and teardown to paseo.json

* feat(paseo): expand workspace scripts catalog

* feat(paseo): reset encryption key in worktree env
2026-08-15 12:29:52 +02:00
Thomas Camlong 56e6b23af6 test(e2e): remove flaky credentials onboarding test 2026-08-15 11:05:06 +02:00
Thomas Camlong 5488bf4d18 fix(truenas): use lowercase in operator for dataset query 2026-08-15 01:33:20 +02:00
homarr-crowdin[bot] 621ea07549 chore(lang): updated translations from crowdin (#6553) 2026-08-14 20:39:02 +02:00
homarr-update-contributors[bot] f64c01456b chore: update contributors (#6588) 2026-08-14 20:38:42 +02:00
homarr-releases[bot] 56198c3f02 chore: update bug report template (#6591) 2026-08-14 20:38:26 +02:00
Thomas ca1bb6f341 Merge pull request #6592 from homarr-labs/docs/update-helm-docs
docs(helm): update chart installation page
2026-08-14 20:38:10 +02:00
github-actions[bot] bf6b6663cd docs(helm): update chart installation page 2026-08-14 18:25:32 +00:00
Releases Homarr 5c2d86767f chore(release): 1.75.0 [skip ci] 2026-08-14 17:45:24 +00:00
homarr-releases[bot] 7c75642d4d chore(release): automatic release v1.75.0 2026-08-14 17:43:56 +00:00
Thomas Camlong 7652eb8c1c preserve smart home toggle response 2026-08-13 17:04:00 +02:00
Thomas Camlong e761c10f32 fix Home Assistant widget actions 2026-08-13 16:21:48 +02:00
Thomas 2513e8454f fix: open user menu instantly (#6568) 2026-08-13 11:44:10 +02:00
ajnart f6137b2dcc fix(truenas): guard empty dataset percentage 2026-08-12 18:24:33 +02:00
ajnart 425dd90871 fix(truenas): report usable dataset space 2026-08-12 18:19:07 +02:00
71 changed files with 5969 additions and 3985 deletions

No files matched your search

@@ -1,6 +1,6 @@
---
description: Comprehensive Homarr codebase architecture and conventions reference
alwaysApply: true
name: codebase-context
description: Comprehensive Homarr codebase architecture and conventions reference. Use when orienting in the monorepo, understanding package dependency layers, the three runtime services and ports, tRPC router structure, multi-database support, widget/integration/cron system architecture, routing, auth providers, env vars, or key patterns before writing code.
---
# Homarr Codebase Context
@@ -27,7 +27,7 @@ Homarr is an open-source, self-hosted dashboard for managing homelab services. I
## Repository Structure
```
```text
homarr/
├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000)
@@ -66,8 +66,6 @@ homarr/
│ ├── analytics/ # Server-side analytics (Umami)
│ ├── server-settings/ # Server setting keys/types
│ ├── settings/ # User-facing settings UI context
│ ├── old-import/ # Legacy Homarr import
│ ├── old-schema/ # Legacy Homarr zod schemas
│ └── cli/ # Node CLI for ops (brocli)
├── tooling/
│ ├── typescript/ # Base tsconfig
@@ -79,11 +77,11 @@ homarr/
## Three Runtime Services
| Service | Port | Role |
|---------|------|------|
| Next.js | 3000 | Main web app (SSR + API routes) |
| WebSocket | 3001 | tRPC subscriptions via `ws` |
| Tasks | 3002 | Cron job runner + management API via Fastify |
| Service | Port | Role |
| --------- | ---- | -------------------------------------------- |
| Next.js | 3000 | Main web app (SSR + API routes) |
| WebSocket | 3001 | tRPC subscriptions via `ws` |
| Tasks | 3002 | Cron job runner + management API via Fastify |
In Docker, nginx listens on **7575** and proxies to all three. The tasks service authenticates via `CRON_JOB_API_KEY` header.
@@ -112,6 +110,7 @@ In Docker, nginx listens on **7575** and proxies to all three. The tasks service
### Multi-Database Support
Drizzle schemas exist in three parallel implementations:
- `packages/db/schema/sqlite.ts`
- `packages/db/schema/mysql.ts`
- `packages/db/schema/postgresql.ts`
@@ -121,6 +120,7 @@ Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-pos
### Widget System
Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget:
- Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })`
- Uses `optionsBuilder.from(factory => ({ ... }))` for typed options
- Loads dynamically via `next/dynamic`
@@ -133,6 +133,7 @@ Integrations are defined in `packages/definitions/src/integration.ts` (`integrat
### Cron Job System
Jobs defined in `packages/cron-jobs/src/jobs/`. Each job:
- Uses `createCronJob` with a cron expression
- Publishes results to Redis channels
- Widget subscriptions consume those channels via tRPC subscriptions
@@ -160,6 +161,7 @@ Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `o
## Environment Variables
Key env vars (from `.env.example`):
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
- `DB_URL`: Database connection string
- `AUTH_SECRET`: NextAuth secret
@@ -191,16 +193,3 @@ Key env vars (from `.env.example`):
- Drag-and-drop via `@dnd-kit/*`
- `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`)
- `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom`
## Documentation (`apps/docs`)
Docusaurus 3 site at `apps/docs/`, package name `@homarr/docs`.
- Run with `pnpm dev:docs` from root
- Build with `pnpm turbo build --filter=@homarr/docs`
- Uses Tailwind CSS 3 for custom pages (not Mantine)
- Imports `@homarr/definitions` for type-safe integration/widget metadata
- When adding or modifying widgets, integrations, cron jobs, API routes, env vars, or user-facing features, you MUST also update corresponding docs in `apps/docs/docs/`
- Integration docs: `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts`
- Widget docs: `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts`
- Strict link checking: broken links/anchors cause build failure
@@ -1,3 +1,8 @@
---
name: datatable-migration
description: Playbook for migrating tables from mantine-react-table (MRT) to mantine-datatable. Use when converting any table component from mantine-react-table to mantine-datatable, working on DataTable column/sort/expansion/context-menu code, or implementing responsive column visibility, column persistence, or transparent tables in widgets.
---
# Migrating Tables from mantine-react-table to mantine-datatable
This document captures every pattern, fix, and decision from the downloads widget rework (PR #6430). Use it as a playbook when porting other tables.
@@ -7,10 +12,12 @@ This document captures every pattern, fix, and decision from the downloads widge
Current `mantine-react-table` consumers that could be migrated:
**Widgets (in `packages/widgets/`):**
- `docker/component.tsx` — Docker container table
- `media-server/component.tsx` — Media server sessions
**Admin pages (in `apps/nextjs/src/app/[locale]/manage/`):**
- `tools/docker/docker-table.tsx`
- `tools/tasks/_components/tasks-table.tsx`
- `tools/api/components/api-keys.tsx`
@@ -19,12 +26,12 @@ Current `mantine-react-table` consumers that could be migrated:
- `tools/kubernetes/` — 8 Kubernetes resource tables
**Modals:**
- `packages/modals-collection/src/search-engines/request-media-modal.tsx`
**Shared hook to remove once all migrated:**
- `packages/ui/src/hooks/use-translated-mantine-react-table.ts`
---
- `packages/ui/src/hooks/use-translated-mantine-react-table.ts`
## 1. Core Migration: mantine-react-table → mantine-datatable
@@ -67,6 +74,7 @@ const columns: DataTableColumn<MyRow>[] = [
```
Key differences:
- `accessorKey` → `accessor`
- `header` → `title`
- `size` → `width`
@@ -118,8 +126,6 @@ const sortedData = useMemo(() => {
<DataTable records={sortedData} sortStatus={sortStatus} onSortStatusChange={setSortStatus} />
```
---
## 2. Making Tables Transparent (Widget Context)
Widgets sit inside board cards that have user-controlled opacity. The table must be fully transparent so the card background shows through. mantine-datatable defaults to `var(--mantine-color-body)` which is opaque.
@@ -144,13 +150,21 @@ Create a CSS file alongside the component:
/* Hover: translucent instead of opaque */
.my-table .mantine-datatable-table tbody tr:hover {
background-color: color-mix(in srgb, var(--mantine-color-default-hover) 40%, transparent) !important;
background-color: color-mix(
in srgb,
var(--mantine-color-default-hover) 40%,
transparent
) !important;
}
/* Headers: frosted glass effect */
.my-table th {
white-space: nowrap;
background-color: color-mix(in srgb, var(--mantine-color-body) 60%, transparent) !important;
background-color: color-mix(
in srgb,
var(--mantine-color-body) 60%,
transparent
) !important;
backdrop-filter: blur(8px);
}
@@ -172,14 +186,16 @@ Create a CSS file alongside the component:
/* Row expansion area */
.my-table .mantine-datatable-row-expansion-cell {
background-color: color-mix(in srgb, var(--mantine-color-body) 40%, transparent) !important;
background-color: color-mix(
in srgb,
var(--mantine-color-body) 40%,
transparent
) !important;
}
```
Import it: `import "./styles.css";` and apply via `className="my-table"`.
---
## 3. Responsive Column Visibility
Use a lookup table of width breakpoints instead of media queries. The widget receives `width` as a prop.
@@ -195,11 +211,38 @@ interface SizeConfig {
}
const SIZE_BREAKPOINTS: { maxWidth: number; config: SizeConfig }[] = [
{ maxWidth: 300, config: { fontSize: "xs", iconSize: 12, cellPadding: 2, showSpeedColumns: false, showTimeColumn: false, showStateColumn: false } },
{ maxWidth: 500, config: { fontSize: "xs", iconSize: 14, cellPadding: 4, showSpeedColumns: false, showTimeColumn: true, showStateColumn: false } },
{
maxWidth: 300,
config: {
fontSize: "xs",
iconSize: 12,
cellPadding: 2,
showSpeedColumns: false,
showTimeColumn: false,
showStateColumn: false,
},
},
{
maxWidth: 500,
config: {
fontSize: "xs",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: false,
showTimeColumn: true,
showStateColumn: false,
},
},
];
const DEFAULT_SIZE_CONFIG: SizeConfig = { fontSize: "sm", iconSize: 14, cellPadding: 4, showSpeedColumns: true, showTimeColumn: true, showStateColumn: true };
const DEFAULT_SIZE_CONFIG: SizeConfig = {
fontSize: "sm",
iconSize: 14,
cellPadding: 4,
showSpeedColumns: true,
showTimeColumn: true,
showStateColumn: true,
};
function getSizeConfig(width: number): SizeConfig {
for (const { maxWidth, config } of SIZE_BREAKPOINTS) {
@@ -215,7 +258,9 @@ const size = useMemo(() => getSizeConfig(width), [width]);
Then use a visibility check lookup table for columns:
```typescript
const columnVisibilityChecks: Partial<Record<string, (ctx: ColumnContext) => boolean>> = {
const columnVisibilityChecks: Partial<
Record<string, (ctx: ColumnContext) => boolean>
> = {
upSpeed: (ctx) => ctx.hasTorrents && ctx.size.showSpeedColumns,
downSpeed: (ctx) => ctx.size.showSpeedColumns,
time: (ctx) => ctx.size.showTimeColumn,
@@ -230,8 +275,6 @@ function shouldIncludeColumn(accessor: string, ctx: ColumnContext): boolean {
}
```
---
## 4. Column Persistence (Drag/Resize → Server)
`useDataTableColumns` stores column order and widths in localStorage by default. For widgets, this must persist server-side so it works across devices.
@@ -263,15 +306,6 @@ Even hidden options need translation keys or the translation test fails:
"widget.mywidget.option.columnWidths.label": "Column widths"
```
### Old-import mappings
Add to `packages/old-import/src/widgets/options.ts`:
```typescript
columnOrder: () => undefined,
columnWidths: () => undefined,
```
### Dynamic storeKey
Use a key that includes `itemId` and sorted column names. This prevents cross-widget localStorage bleeding:
@@ -285,8 +319,13 @@ const storeKey = `mytable-${itemId ?? "preview"}-${[...options.columns].toSorted
On mount, seed `useDataTableColumns` with server-persisted values. Use a `hydrated` ref to prevent the persist effect from immediately writing defaults back:
```typescript
const { effectiveColumns, columnsOrder, columnsWidth, setColumnsOrder, setMultipleColumnWidths } =
useDataTableColumns<MyRow>({ key: storeKey, columns });
const {
effectiveColumns,
columnsOrder,
columnsWidth,
setColumnsOrder,
setMultipleColumnWidths,
} = useDataTableColumns<MyRow>({ key: storeKey, columns });
const lastStoreKey = useRef(storeKey);
const hydrated = useRef(false);
@@ -300,11 +339,22 @@ useEffect(() => {
if (savedOrder.length > 0) setColumnsOrder(savedOrder);
if (Object.keys(savedWidths).length > 0) {
setMultipleColumnWidths(
Object.entries(savedWidths).map(([accessor, w]) => ({ accessor, width: w })),
Object.entries(savedWidths).map(([accessor, w]) => ({
accessor,
width: w,
})),
);
}
requestAnimationFrame(() => { hydrated.current = true; });
}, [storeKey, savedOrder, savedWidths, setColumnsOrder, setMultipleColumnWidths]);
requestAnimationFrame(() => {
hydrated.current = true;
});
}, [
storeKey,
savedOrder,
savedWidths,
setColumnsOrder,
setMultipleColumnWidths,
]);
```
### Persist localStorage → server
@@ -317,22 +367,26 @@ const prevWidths = useRef(columnsWidth);
useEffect(() => {
if (!hydrated.current) return;
const orderChanged = JSON.stringify(columnsOrder) !== JSON.stringify(prevOrder.current);
const widthsChanged = JSON.stringify(columnsWidth) !== JSON.stringify(prevWidths.current);
const orderChanged =
JSON.stringify(columnsOrder) !== JSON.stringify(prevOrder.current);
const widthsChanged =
JSON.stringify(columnsWidth) !== JSON.stringify(prevWidths.current);
prevOrder.current = columnsOrder;
prevWidths.current = columnsWidth;
if (!orderChanged && !widthsChanged) return;
if (orderChanged) persistOption({ columnOrder: JSON.stringify(columnsOrder) });
if (orderChanged)
persistOption({ columnOrder: JSON.stringify(columnsOrder) });
if (widthsChanged) {
const widthMap: Record<string, number> = {};
for (const entry of columnsWidth) {
const key = Object.keys(entry)[0];
if (!key) continue;
const w = entry[key as keyof typeof entry];
if (typeof w === "number") widthMap[key] = w;
else if (typeof w === "string" && w.endsWith("px")) widthMap[key] = parseInt(w, 10);
const v = entry[key as keyof typeof entry];
if (typeof v === "number") widthMap[key] = v;
else if (typeof v === "string" && v.endsWith("px"))
widthMap[key] = parseInt(v, 10);
}
persistOption({ columnWidths: JSON.stringify(widthMap) });
}
@@ -341,8 +395,6 @@ useEffect(() => {
The `persistOption` helper calls both `setOptions` (local state) and `saveItemOptions` (server mutation).
---
## 5. Context Menu (Right-Click)
Do NOT install `mantine-contextmenu`. Use Mantine's `Menu` + `Portal` directly:
@@ -374,15 +426,41 @@ const handleContextMenu = useCallback(
The menu component uses `Portal` for correct z-index:
```tsx
function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose: () => void }) {
function RowContextMenu({
state,
onClose,
}: {
state: ContextMenuState;
onClose: () => void;
}) {
return (
<Portal>
<Menu opened onClose={onClose} closeOnItemClick={false} position="right-start" offset={0}>
<Menu
opened
onClose={onClose}
closeOnItemClick={false}
position="right-start"
offset={0}
>
<Menu.Target>
<Box style={{ position: "fixed", left: state.x, top: state.y, width: 0, height: 0 }} />
<Box
style={{
position: "fixed",
left: state.x,
top: state.y,
width: 0,
height: 0,
}}
/>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={() => { /* action */ onClose(); }}>Action</Menu.Item>
<Menu.Item
onClick={() => {
/* action */ onClose();
}}
>
Action
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Portal>
@@ -392,8 +470,6 @@ function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose:
**Key fix:** Use `closeOnItemClick={false}` if any menu item needs a two-step confirmation (like delete → confirm). Close the menu on scroll: `onScroll={() => { if (contextMenu) closeContextMenu(); }}`.
---
## 6. Row Expansion
```typescript
@@ -413,8 +489,6 @@ function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose:
**Critical:** Do NOT set `onRowClick` when using `rowExpansion` with `trigger: "click"`. They conflict and the expansion won't work.
---
## 7. Common Pitfalls & Fixes
### Status indicator dots
@@ -475,8 +549,11 @@ Always wrap in try/catch:
```typescript
function parseJsonOption<T>(value: string | undefined, fallback: T): T {
if (!value) return fallback;
try { return JSON.parse(value) as T; }
catch { return fallback; }
try {
return JSON.parse(value) as T;
} catch {
return fallback;
}
}
```
@@ -494,20 +571,19 @@ If the widget has configurable default sort, sync it:
```typescript
useEffect(() => {
setSortStatus({ columnAccessor: options.defaultSort, direction: defaultSortDirection });
setSortStatus({
columnAccessor: options.defaultSort,
direction: defaultSortDirection,
});
}, [options.defaultSort, defaultSortDirection]);
```
---
## 8. Accessibility
- All `ActionIcon` components need `aria-label`
- Filter toggles (client badges, status badges) need `aria-pressed={isActive}`
- `textSelectionDisabled` on DataTable prevents accidental text selection during drag/resize
---
## 9. Error Handling
- Every mutation needs an `onError` callback with `showErrorNotification`
@@ -518,12 +594,14 @@ useEffect(() => {
```typescript
const mutationOptions = {
onSettled: () => void utils.myQuery.invalidate(),
onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }),
onError: () =>
showErrorNotification({
title: t("errors.actionFailed"),
message: t("errors.actionFailedMessage"),
}),
};
```
---
## 10. i18n
- No hardcoded strings. Every user-visible string goes through `useScopedI18n`
@@ -534,8 +612,6 @@ const mutationOptions = {
- Errors: `t("errors.{errorKey}")`
- Hidden options still need `.label` translation keys
---
## 11. Edit Mode
Disable pointer events on the table during edit mode to prevent conflicts with the board's drag-and-drop:
@@ -550,8 +626,6 @@ if (isEditMode) rowContextMenuHandler = undefined;
<DataTable style={{ pointerEvents: tablePointerEvents }} />
```
---
## 12. DataTable Props Reference (Commonly Used)
```typescript
@@ -590,8 +664,6 @@ if (isEditMode) rowContextMenuHandler = undefined;
/>
```
---
## 13. Layout Stability
- Don't use `table-layout: fixed` — it fights mantine-datatable's resize logic
@@ -600,8 +672,6 @@ if (isEditMode) rowContextMenuHandler = undefined;
- Names should truncate at the cell boundary, not a character count, so the layout is stable regardless of content length
- `flexShrink: 0` on badges/icons prevents them from being squeezed by long names
---
## 14. Checklist for Each Migration
1. Replace imports (`mantine-react-table` → `mantine-datatable`)
@@ -610,13 +680,12 @@ if (isEditMode) rowContextMenuHandler = undefined;
4. Add CSS file for transparent backgrounds (if widget context)
5. Add responsive column visibility (if widget context)
6. Add hidden options + hydration logic (if column persistence needed)
7. Add old-import mappings for new options
8. Add translation keys (including hidden option labels)
9. Add context menu if row actions exist
10. Add row expansion if detail view exists
11. Add error handling on all mutations
12. Add accessibility attributes
13. Test at multiple widget widths
14. Update docs in `apps/docs/docs/widgets/` if applicable
15. Run `pnpm turbo typecheck` — catches old-import mismatches
16. Run translation spec — catches missing keys
7. Add translation keys (including hidden option labels)
8. Add context menu if row actions exist
9. Add row expansion if detail view exists
10. Add error handling on all mutations
11. Add accessibility attributes
12. Test at multiple widget widths
13. Update docs in `apps/docs/docs/widgets/` if applicable
14. Run `pnpm turbo typecheck`
15. Run translation spec — catches missing keys
@@ -0,0 +1,44 @@
---
name: documentation-sync
description: Keep Homarr documentation in sync with code changes. Use when modifying code that affects user-facing behavior, or when adding/modifying widgets, integrations, cron jobs, API routes, env vars, permissions, auth providers, or UI in apps/docs. Covers the docs/ mapping table, integration doc pattern, and widget doc pattern.
---
# Documentation Sync
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
## What triggers a doc update
| Code change | Doc location |
| ---------------------- | ----------------------------------------------------------- |
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
| Changed API surface | `apps/docs/docs/management/api.mdx` |
| New/changed env vars | `apps/docs/docs/advanced/` |
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
| UI/UX changes | Relevant getting-started or management pages |
| New cron job | `apps/docs/docs/management/tasks.mdx` |
| Changed permissions | `apps/docs/docs/management/users.mdx` |
## Integration doc pattern
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
- Import metadata from `@homarr/definitions` where possible
## Widget doc pattern
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
## Development
- Run docs locally: `pnpm dev:docs` from root
- Build docs: `pnpm turbo build --filter=@homarr/docs`
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
- Docusaurus strict mode throws on broken links — fix any broken references before committing
@@ -1,13 +1,14 @@
---
description: How to expose tRPC procedures as MCP tools for AI clients
globs: packages/api/src/router/**/*.ts
alwaysApply: false
name: mcp-integration
description: Expose tRPC procedures as MCP tools for AI clients. Use when modifying procedures in packages/api/src/router, adding a new tRPC procedure or router, or deciding what to expose via Homarr's MCP interface. Covers .meta({ mcp }), router registration in packages/api/src/mcp.ts, input schema rules, and what to expose vs skip.
---
# MCP Integration Guide
# MCP Integration
Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr.
Exposing a procedure makes it callable by any AI client with an API key. Before adding `.meta({ mcp })`, review what the tool exposes: authorization requirements, sensitive data, tenant isolation, auditability, and whether the action is destructive. Prefer exposing read-only queries over mutations unless the mutation is safe and permission-scoped. When in doubt, ask for a security review before shipping a new MCP tool.
## Adding MCP to a procedure
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
@@ -16,7 +17,7 @@ Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure cha
// Read-only tool (query)
myProcedure: protectedProcedure
.meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } })
.input(z.object({ limit: z.number().default(50) }))
.input(z.object({ limit: z.number().default(10) }))
.query(async ({ ctx, input }) => { ... })
// Write tool (mutation)
@@ -38,23 +39,27 @@ The `.meta()` call must come **before** `.input()` and `.query()`/`.mutation()`
The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr.
**Must include:**
- What the tool returns or does
- Which integrations/services it works with (if applicable)
- How to get required IDs (e.g., "Use integration_all to get the integrationId")
- Permission requirements (if non-obvious)
**Good:**
```
```text
"Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all"
```
**Bad:**
```
```text
"Get calendar events"
```
**For tools that return permission fields**, explain what they mean:
```
```text
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
```
@@ -77,11 +82,13 @@ export const mcpRouter = createTRPCRouter({
## What to expose vs. what to skip
**Expose:**
- Data queries (list, search, get by ID)
- Actions users would ask an AI to do (toggle, create, delete, approve)
- Status checks (health, stats, summaries)
**Skip:**
- Subscriptions (WebSocket-only, not supported by MCP)
- Internal procedures (session management, onboarding steps)
- File upload/download procedures
+1
View File
@@ -0,0 +1 @@
../.agents/skills
-50
View File
@@ -1,50 +0,0 @@
---
description: Keep documentation in sync with code changes
globs:
- packages/definitions/src/**
- packages/widgets/src/**
- packages/integrations/src/**
- packages/api/src/**
- apps/nextjs/src/**
- apps/docs/**
---
# Documentation Sync Rule
When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`.
## What triggers a doc update
| Code change | Doc location |
|---|---|
| New integration | `apps/docs/docs/integrations/<slug>/index.mdx` + `index.ts` |
| New widget | `apps/docs/docs/widgets/<slug>/index.mdx` + `index.ts` |
| Changed API surface | `apps/docs/docs/management/api.mdx` |
| New/changed env vars | `apps/docs/docs/advanced/` |
| New CLI commands | `apps/docs/docs/advanced/command-line/` |
| Changed auth providers | `apps/docs/docs/advanced/` SSO pages |
| UI/UX changes | Relevant getting-started or management pages |
| New cron job | `apps/docs/docs/management/tasks.mdx` |
| Changed permissions | `apps/docs/docs/management/users.mdx` |
## Integration doc pattern
Each integration has a folder under `apps/docs/docs/integrations/<slug>/`:
- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path
- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets`
- Import metadata from `@homarr/definitions` where possible
## Widget doc pattern
Each widget has a folder under `apps/docs/docs/widgets/<slug>/`:
- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration
- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding`
## Development
- Run docs locally: `pnpm dev:docs` from root
- Build docs: `pnpm turbo build --filter=@homarr/docs`
- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata
- Docusaurus strict mode throws on broken links — fix any broken references before committing
+1
View File
@@ -33,6 +33,7 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.75.0
- 1.74.0
- 1.73.0
- 1.72.0
+2 -1
View File
@@ -71,7 +71,8 @@ apps/nextjs/public/images/background.png
# personal agent config
.opencode
.agents/
.agents/*
!.agents/skills/
skills-lock.json
# docusaurus
+8
View File
@@ -0,0 +1,8 @@
{
"mcpServers": {
"mantine": {
"command": "npx",
"args": ["-y", "@mantine/mcp-server"]
}
}
}
+10
View File
@@ -84,6 +84,16 @@ When modifying user-facing code, you MUST also update the corresponding document
- Lint: oxlint (not ESLint)
- Format: oxfmt (not Prettier)
- UI: Mantine (not Tailwind) — Tailwind is only used in docs app
- Mantine: use the `mantine` MCP server (configured in `.mcp.json`; tools `get_item_doc` / `get_item_props` / `search_docs`) for current v9 APIs before writing component code. Prefer built-in primitives (`Combobox`/`useCombobox` for selects, the polymorphic `component` prop, `@mantine/hooks`) over hand-rolled equivalents. Check `packages/ui/` for existing conventions first.
## MCP servers
`.mcp.json` at the repo root declares project-scoped MCP servers (the standard Claude Code layout; other tools can import the same `mcpServers` block). See the `mantine` entry there for the Mantine v9 server, and the upstream docs at https://mantine.dev/guides/llms/ for per-tool setup (OpenCode: `.opencode/opencode.json`, Codex: `.codex/config.toml`). Only keyless server definitions belong in `.mcp.json`; anything needing secrets stays in local dotfiles.
## Agent Skills
Portable agent skills live in `.agents/skills/` (the Agent Skills open standard). Cursor, Codex, and OpenCode read `.agents/skills/` natively. Claude Code does not scan it, so `.claude/skills/` is a symlink back to `.agents/skills/`. Install skills at `.agents/skills/<name>/SKILL.md` — that single copy is used everywhere. See `.agents/skills/` for available skills (documentation-sync, mcp-integration, codebase-context, datatable-migration).
- Icons: @tabler/icons-react
- Docs app can import from `@homarr/definitions` for shared types
- Run `pnpm dev:cli -- dev` to browse local `homarr:*` images and remote PR images.
+2
View File
@@ -1,3 +1,5 @@
## [1.75.0](https://github.com/homarr-labs/homarr/compare/v1.74.0...v1.75.0) (2026-08-14)
## [1.74.0](https://github.com/homarr-labs/homarr/compare/v1.73.0...v1.74.0) (2026-08-07)
## [1.73.0](https://github.com/homarr-labs/homarr/compare/v1.72.0...v1.73.0) (2026-07-31)
@@ -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.26.0](https://img.shields.io/badge/Version-8.26.0-informational?style=flat)
![Version: 8.27.0](https://img.shields.io/badge/Version-8.27.0-informational?style=flat)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat)
![AppVersion: v1.74.0](https://img.shields.io/badge/AppVersion-v1.74.0-informational?style=flat)
![AppVersion: v1.75.0](https://img.shields.io/badge/AppVersion-v1.75.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.74.0"` | Overrides the image tag whose default is the chart appVersion |
| image.tag | string | `"v1.75.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 |
@@ -27,6 +27,8 @@ import Alert from '@theme/Admonition';
]}
/>
The storage values shown by these widgets come from each pool's root ZFS dataset, so they already account for RAIDZ parity overhead and dataset limits and match what `zfs list` and the TrueNAS web interface report. When a pool cannot report its dataset space, Homarr falls back to the pool's physical values, which read higher on RAIDZ.
### Adding the integration
<AddingIntegration />
@@ -43,6 +43,7 @@ import temperature from './img/temperature.png';
### Interactions
- Toggle entity state by clicking on the entity (only when clickable enabled).
- After a successful toggle, the displayed state refreshes immediately.
### Adding the widget
@@ -64,4 +65,4 @@ import retrieveEntityId from './img/retrieve-entity-id.png';
<div>
<img src={retrieveEntityId} alt="homeassistant retrieve-entity-id" width={256 * 2} />
</div>
</div>
@@ -47,4 +47,7 @@ import withDisplayname from './img/with-displayname.png';
<AddingWidget />
### Configuration
Use the Home Assistant entity ID (for example, `automation.turn_on_lights`) in the **Automation ID** field, not the automation's internal ID.
<WidgetConfig configuration={smartHomeExecuteAutomationWidget.configuration} />
@@ -1,38 +1,17 @@
"use client";
import type { ComponentProps, ReactNode } from "react";
import { Suspense, use, useEffect, useState } from "react";
import { Box, Loader, UnstyledButton, useMantineColorScheme } from "@mantine/core";
import type { ReactNode } from "react";
import { useEffect, useState } from "react";
import { UnstyledButton, useMantineColorScheme } from "@mantine/core";
import { useHotkeys } from "@mantine/hooks";
import { ErrorBoundary } from "react-error-boundary";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { hotkeys } from "@homarr/definitions";
import { UserAvatarMenu } from "~/components/user-avatar-menu";
import { UpdateIndicator } from "./update";
type UserAvatarMenuModule = typeof import("~/components/user-avatar-menu");
type UserAvatarMenuProps = ComponentProps<UserAvatarMenuModule["UserAvatarMenu"]>;
let userAvatarMenuPromise: Promise<UserAvatarMenuModule> | undefined;
const loadUserAvatarMenu = () => {
if (userAvatarMenuPromise) return userAvatarMenuPromise;
const promise = import("~/components/user-avatar-menu");
userAvatarMenuPromise = promise;
void promise.catch(() => {
if (userAvatarMenuPromise === promise) userAvatarMenuPromise = undefined;
});
return promise;
};
const preloadUserAvatarMenu = () => void loadUserAvatarMenu().catch(() => undefined);
const LazyUserAvatarMenu = (props: UserAvatarMenuProps) => {
const { UserAvatarMenu } = use(loadUserAvatarMenu());
return <UserAvatarMenu {...props} />;
};
interface UserButtonClientProps {
avatar: ReactNode;
isAdmin: boolean;
@@ -41,8 +20,6 @@ interface UserButtonClientProps {
export const UserButtonClient = ({ avatar, isAdmin, isDockerEnabled }: UserButtonClientProps) => {
const [canCheckForUpdates, setCanCheckForUpdates] = useState(false);
const [isMenuMounted, setIsMenuMounted] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const session = useSession();
const { toggleColorScheme } = useMantineColorScheme();
useHotkeys([[hotkeys.toggleColorScheme, toggleColorScheme]]);
@@ -66,60 +43,14 @@ export const UserButtonClient = ({ avatar, isAdmin, isDockerEnabled }: UserButto
staleTime: 60 * 60 * 1_000,
});
const visibleUpdates = isCurrentSessionAdmin ? availableUpdates : undefined;
const openMenu = () => {
setIsMenuMounted(true);
setIsMenuOpen(true);
};
const renderButton = (onClick: () => void, isLoading = false) => (
<UnstyledButton
onClick={onClick}
onFocus={preloadUserAvatarMenu}
onMouseEnter={preloadUserAvatarMenu}
onPointerDown={preloadUserAvatarMenu}
aria-busy={isLoading || undefined}
disabled={isLoading}
>
<UpdateIndicator availableUpdates={visibleUpdates} disabled={!isCurrentSessionAdmin}>
<Box pos="relative" style={{ display: "inline-flex" }}>
<Box opacity={isLoading ? 0.35 : 1}>{avatar}</Box>
{isLoading && (
<Loader
aria-label="Loading user menu"
size={18}
pos="absolute"
top="50%"
left="50%"
style={{ transform: "translate(-50%, -50%)" }}
/>
)}
</Box>
</UpdateIndicator>
</UnstyledButton>
);
const button = renderButton(openMenu);
const loadingButton = renderButton(() => undefined, true);
if (!isMenuMounted) return button;
return (
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) =>
renderButton(() => {
resetErrorBoundary();
openMenu();
})
}
>
<Suspense fallback={loadingButton}>
<LazyUserAvatarMenu
availableUpdates={visibleUpdates}
isDockerEnabled={isDockerEnabled}
opened={isMenuOpen}
onOpenChange={setIsMenuOpen}
>
{button}
</LazyUserAvatarMenu>
</Suspense>
</ErrorBoundary>
<UserAvatarMenu availableUpdates={visibleUpdates} isDockerEnabled={isDockerEnabled}>
<UnstyledButton>
<UpdateIndicator availableUpdates={visibleUpdates} disabled={!isCurrentSessionAdmin}>
{avatar}
</UpdateIndicator>
</UnstyledButton>
</UserAvatarMenu>
);
};
@@ -21,17 +21,9 @@ interface UserAvatarMenuProps {
children: ReactNode;
availableUpdates?: RouterOutputs["updateChecker"]["getAvailableUpdates"];
isDockerEnabled?: boolean;
opened: boolean;
onOpenChange: (opened: boolean) => void;
}
export const UserAvatarMenu = ({
children,
availableUpdates,
isDockerEnabled,
opened,
onOpenChange,
}: UserAvatarMenuProps) => {
export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled }: UserAvatarMenuProps) => {
const t = useScopedI18n("common.userAvatar.menu");
const session = useSession();
@@ -48,7 +40,7 @@ export const UserAvatarMenu = ({
return (
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
<Menu width={300} withinPortal keepMounted opened={opened} onChange={onOpenChange}>
<Menu width={300} withinPortal keepMounted>
<Menu.Dropdown>
<AvailableUpdatesMenuItem availableUpdates={availableUpdates} />
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
-36
View File
@@ -7,42 +7,6 @@ import { createHomarrContainer } from "./shared/create-homarr-container";
import { createSqliteDbFileAsync } from "./shared/e2e-db";
describe("Onboarding", () => {
test("Credentials onboarding should be successful", async () => {
// Arrange
const { db, localMountPath } = await createSqliteDbFileAsync();
const homarrContainer = await createHomarrContainer({
mounts: {
"/appdata": localMountPath,
},
}).start();
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
const actions = new OnboardingActions(page, db);
const assertions = new OnboardingAssertions(page, db);
try {
// Act
await page.goto(`http://${homarrContainer.getHost()}:${homarrContainer.getMappedPort(7575)}`);
await actions.startOnboardingAsync("scratch");
await actions.processUserStepAsync({
username: "admin",
password: "Comp(exP4sswOrd",
confirmPassword: "Comp(exP4sswOrd",
});
await actions.processSettingsStepAsync();
await actions.processIntegrationsStepAsync();
// Assert
await assertions.assertFinishStepVisibleAsync();
await assertions.assertUserAndAdminGroupInsertedAsync("admin");
await assertions.assertDbOnboardingStepAsync("finish");
} finally {
await Promise.all([browser.close(), homarrContainer.stop()]);
}
}, 60_000);
test("External provider onboarding setup should be successful", async () => {
// Arrange
const { db, localMountPath } = await createSqliteDbFileAsync();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.74.0",
"version": "1.75.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
+16 -1
View File
@@ -1,3 +1,4 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
@@ -41,6 +42,13 @@ export const smartHomeRouter = createTRPCRouter({
const client = await createIntegrationAsync(integration);
const success = await client.triggerToggleAsync(input.entityId);
if (!success) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Home Assistant failed to toggle the entity",
});
}
return success;
}),
executeAutomation: protectedProcedure
@@ -55,6 +63,13 @@ export const smartHomeRouter = createTRPCRouter({
.input(z.object({ automationId: z.string() }))
.mutation(async ({ ctx: { integration }, input }) => {
const client = await createIntegrationAsync(integration);
await client.triggerAutomationAsync(input.automationId);
const success = await client.triggerAutomationAsync(input.automationId);
if (!success) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Home Assistant failed to trigger the automation",
});
}
}),
});
@@ -14,6 +14,10 @@ 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 => {
@@ -79,6 +83,10 @@ export const fetchWithTrustedCertificatesAsync = async (
async (signal) =>
fetch(url, {
...fetchOptions,
headers: {
"User-Agent": getDefaultUserAgent(),
...fetchOptions.headers,
},
signal,
dispatcher: agent,
}),
@@ -89,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,
});
};
@@ -134,7 +134,10 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
*/
private async postAsync(path: `/api/${string}`, body: Record<string, string>) {
return await fetchWithTrustedCertificatesAsync(this.url(path), {
headers: this.getAuthHeaders(),
headers: {
...this.getAuthHeaders(),
"Content-Type": "application/json",
},
body: JSON.stringify(body),
method: "POST",
});
@@ -59,10 +59,12 @@ const ws = vi.hoisted(() => {
}
const result = responder(payload.method as string, (payload.params as unknown[] | undefined) ?? []);
// A responder returning an Error mimics a method the server rejects, e.g. an unsupported filter.
const body = result instanceof Error ? { error: { message: result.message } } : { result };
const envelope =
payload.jsonrpc === "2.0"
? { jsonrpc: "2.0", id: payload.id, result }
: { msg: "result", id: payload.id, result };
? { jsonrpc: "2.0", id: payload.id, ...body }
: { msg: "result", id: payload.id, ...body };
this.respond(JSON.stringify(envelope));
}
@@ -91,7 +93,10 @@ const ws = vi.hoisted(() => {
};
});
const logs = vi.hoisted(() => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }));
vi.mock("ws", () => ({ WebSocket: ws.FakeWebSocket }));
vi.mock("@homarr/core/infrastructure/logs", () => ({ createLogger: () => logs }));
vi.mock("@homarr/core/infrastructure/certificates", () => ({
getAllTrustedCertificatesAsync: vi.fn().mockResolvedValue([]),
getTrustedCertificateHostnamesAsync: vi.fn().mockResolvedValue([]),
@@ -148,6 +153,14 @@ const happyResponder = (method: string) => {
];
case "pool.query":
return [{ name: "tank", status: "ONLINE", healthy: true, free: 500, size: 1000, allocated: 500 }];
case "pool.dataset.query":
return [
{
id: "tank",
used: { parsed: 200 },
available: { parsed: 300 },
},
];
case "interface.query":
return [{ id: "eth0", name: "eth0" }];
case "reporting.netdata_get_data":
@@ -159,6 +172,11 @@ const happyResponder = (method: string) => {
const emptyAggregations = () => ({ min: {}, mean: {}, max: {} });
// `pool.query` reports 500 allocated of a 1000 byte pool, leaving 500 free.
const physicalFallbackFileSystem = [{ deviceName: "tank", available: "500", used: "500", percentage: 50 }];
const ERROR_MESSAGE_LIMIT = 200;
let integrationCounter = 0;
const createIntegration = (decryptedSecrets: IntegrationSecret[]) =>
new TrueNasIntegration({
@@ -180,6 +198,7 @@ describe("TrueNasIntegration", () => {
ws.constructedUrls.length = 0;
ws.failures.length = 0;
ws.setResponder(happyResponder);
logs.warn.mockClear();
});
test("fetches and maps system info over the JSON-RPC API", async () => {
@@ -191,6 +210,18 @@ describe("TrueNasIntegration", () => {
// JSON-RPC does not use the legacy "connect" session handshake.
expect(ws.sentPayloads.some((payload) => payload.msg === "connect")).toBe(false);
const datasetPayload = ws.sentPayloads.find((payload) => payload.method === "pool.dataset.query");
expect(datasetPayload).toMatchObject({
params: [
[["id", "in", ["tank"]]],
{
extra: {
properties: ["used", "available"],
},
},
],
});
expect(result).toMatchObject({
version: "TrueNAS-SCALE-24.10",
cpuModelName: "Intel Xeon",
@@ -204,12 +235,131 @@ describe("TrueNasIntegration", () => {
loadAverage: null,
gpu: [],
network: { up: 20_000, down: 10_000 },
// `available` is the free space left on the pool (free = 500), not its total size.
fileSystem: [{ deviceName: "tank", available: "500", used: "500", percentage: 50 }],
// `available` and `used` come from the root dataset, not the physical pool.
fileSystem: [{ deviceName: "tank", available: "300", used: "200", percentage: 40 }],
smart: [{ deviceName: "tank", healthy: true, overallStatus: "ONLINE", temperature: null }],
});
});
test("falls back to physical pool space when the root dataset is not returned", async () => {
ws.setResponder((method) => (method === "pool.dataset.query" ? [] : happyResponder(method)));
const integration = createIntegration(credentials);
const result = await integration.getSystemInfoAsync();
expect(result.fileSystem).toMatchObject(physicalFallbackFileSystem);
// Dropping the pool would also hide its health status.
expect(result.smart).toMatchObject([{ deviceName: "tank", healthy: true, overallStatus: "ONLINE" }]);
});
test("falls back to physical pool space when the root dataset reports no usable values", async () => {
ws.setResponder((method) =>
method === "pool.dataset.query"
? [{ id: "tank", used: { parsed: null }, available: null }]
: happyResponder(method),
);
const integration = createIntegration(credentials);
const result = await integration.getSystemInfoAsync();
expect(result.fileSystem).toMatchObject(physicalFallbackFileSystem);
});
test("falls back to physical pool space when the root dataset omits the requested properties", async () => {
// Datasets that cannot report space, e.g. while locked, come back without `used` and `available`.
ws.setResponder((method) =>
method === "pool.dataset.query" ? [{ id: "tank", locked: true }] : happyResponder(method),
);
const integration = createIntegration(credentials);
const result = await integration.getSystemInfoAsync();
expect(result.fileSystem).toMatchObject(physicalFallbackFileSystem);
});
test("falls back to physical pool space when the dataset response fails schema validation", async () => {
ws.setResponder((method) =>
method === "pool.dataset.query" ? { unexpected: "not an array" } : happyResponder(method),
);
const integration = createIntegration(credentials);
const result = await integration.getSystemInfoAsync();
expect(result.fileSystem).toMatchObject(physicalFallbackFileSystem);
// A malformed payload must not take the rest of the system info down with it.
expect(result).toMatchObject({ cpuTemp: 45, memAvailableInBytes: 6_000_000_000, uptime: 3600 });
});
test("falls back to physical pool space when the dataset query is rejected", async () => {
ws.setResponder((method) =>
method === "pool.dataset.query"
? new Error("[EINVAL] filters: Value error, Invalid operation: in")
: happyResponder(method),
);
const integration = createIntegration(credentials);
const result = await integration.getSystemInfoAsync();
expect(result.fileSystem).toMatchObject(physicalFallbackFileSystem);
// A rejected dataset query must not take the rest of the system info down with it.
expect(result).toMatchObject({ cpuTemp: 45, memAvailableInBytes: 6_000_000_000, uptime: 3600 });
});
test("logs the truncated rejection reason rather than the generic wrapper message", async () => {
// TrueNAS answers a rejected method with its Python traceback, which runs to several kilobytes.
ws.setResponder((method) =>
method === "pool.dataset.query"
? new Error(`[EINVAL] query-filters ${"x".repeat(5000)}`)
: happyResponder(method),
);
const integration = createIntegration(credentials);
await integration.getSystemInfoAsync();
const logged = logs.warn.mock.calls.find(([message]) => String(message).includes("Could not retrieve"));
const reason = String((logged?.[1] as { error?: string } | undefined)?.error);
// The decorator would otherwise surface only "An unknown error occured while executing Integration method".
expect(reason).toContain("[EINVAL] query-filters");
expect(reason).toHaveLength(ERROR_MESSAGE_LIMIT + 1); // truncated, plus the ellipsis
});
test("keeps reporting a pool that is unhealthy but has no usable dataset space", async () => {
ws.setResponder((method) => {
if (method === "pool.query")
return [
{ name: "tank", status: "ONLINE", healthy: true, free: 500, size: 1000, allocated: 500 },
{ name: "backup", status: "DEGRADED", healthy: false, free: 10, size: 100, allocated: 90 },
];
if (method === "pool.dataset.query") return [{ id: "tank", used: { parsed: 200 }, available: { parsed: 300 } }];
return happyResponder(method);
});
const integration = createIntegration(credentials);
const result = await integration.getSystemInfoAsync();
expect(result.smart).toMatchObject([
{ deviceName: "tank", healthy: true, overallStatus: "ONLINE" },
{ deviceName: "backup", healthy: false, overallStatus: "DEGRADED" },
]);
expect(result.fileSystem).toMatchObject([
{ deviceName: "tank", available: "300", used: "200", percentage: 40 },
{ deviceName: "backup", available: "10", used: "90", percentage: 90 },
]);
});
test("reports zero usage for an empty root dataset", async () => {
ws.setResponder((method) =>
method === "pool.dataset.query"
? [{ id: "tank", used: { parsed: 0 }, available: { parsed: 0 } }]
: happyResponder(method),
);
const integration = createIntegration(credentials);
const result = await integration.getSystemInfoAsync();
expect(result.fileSystem[0]?.percentage).toBe(0);
});
test("authenticates with an API key when one is configured", async () => {
const integration = createIntegration([{ kind: "apiKey", value: "api-key-123" }]);
@@ -15,6 +15,20 @@ const logger = createLogger({ module: "trueNasIntegration" });
const NETWORK_MULTIPLIER = 100;
const ERROR_MESSAGE_LIMIT = 200;
/**
* The error decorator replaces a rejected request with a generic message and moves the actionable
* reason to `cause`. TrueNAS puts its middleware traceback in there, several kilobytes of it, so the
* reason is truncated before it reaches a log line that repeats on every poll.
*/
const describeRequestError = (error: unknown) => {
const cause = error instanceof Error ? error.cause : undefined;
const reason = cause instanceof Error ? cause.message : error instanceof Error ? error.message : String(error);
return reason.length > ERROR_MESSAGE_LIMIT ? `${reason.slice(0, ERROR_MESSAGE_LIMIT)}…` : reason;
};
export class TrueNasIntegration extends Integration implements ISystemHealthMonitoringIntegration {
private client?: TrueNasClient;
@@ -49,9 +63,10 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni
memUsedInBytes,
fileSystem: datasets.map((dataset) => ({
deviceName: dataset.name,
available: `${dataset.free}`, // free space left on the pool
used: `${dataset.allocated}`,
percentage: (dataset.allocated / dataset.size) * 100,
available: `${dataset.available}`,
used: `${dataset.used}`,
percentage:
dataset.used + dataset.available === 0 ? 0 : (dataset.used / (dataset.used + dataset.available)) * 100,
})),
availablePkgUpdates: 0,
network: {
@@ -107,9 +122,10 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni
.map((pool) => {
if (pool.allocated !== null && pool.size !== null && pool.free !== null) {
return {
...pool,
name: pool.name,
status: pool.status,
healthy: pool.healthy,
allocated: pool.allocated,
size: pool.size,
free: pool.free,
};
}
@@ -117,12 +133,76 @@ export class TrueNasIntegration extends Integration implements ISystemHealthMoni
return null;
})
.filter((pool) => pool !== null);
logger.debug("Retrieved pools", {
url: this.integration.url,
totalCount: result.length,
activeCount: activePools.length,
});
return activePools;
if (activePools.length === 0) return [];
const usableSpaceByPool = await this.getUsableSpaceByPoolAsync(activePools.map((pool) => pool.name));
return activePools.map((pool) => {
const usableSpace = usableSpaceByPool.get(pool.name);
if (!usableSpace) {
logger.warn("Falling back to physical pool space, the root dataset reported no usable space", {
url: this.integration.url,
pool: pool.name,
});
}
return {
name: pool.name,
status: pool.status,
healthy: pool.healthy,
used: usableSpace?.used ?? pool.allocated,
available: usableSpace?.available ?? pool.free,
};
});
}
/**
* Returns the usable space of each pool's root ZFS dataset, keyed by pool name. Unlike the physical
* `pool.query` values, these account for RAIDZ parity overhead and dataset limits, matching `zfs list`.
* See https://github.com/homarr-labs/homarr/issues/6566
*
* A pool is left out when its usable space is unavailable, so the caller keeps reporting the pool with
* its physical values instead of dropping it, which would also hide its health status:
* - request rejected → method or `extra.properties` unsupported, or the API key lacks the dataset role
* - response rejected → the payload does not match the expected shape
* - dataset missing → the pool's root dataset was not returned by the query
* - property absent → the dataset does not report `used` / `available`, e.g. while locked
*/
private async getUsableSpaceByPoolAsync(poolNames: string[]) {
const usableSpaceByPool = new Map<string, { used: number; available: number }>();
try {
const datasets = await this.requestAsync("pool.dataset.query", [
[["id", "in", poolNames]],
{
extra: {
properties: ["used", "available"],
},
},
]);
for (const dataset of await poolDatasetSchema.parseAsync(datasets)) {
const used = dataset.used?.parsed;
const available = dataset.available?.parsed;
if (typeof used !== "number" || typeof available !== "number") continue;
usableSpaceByPool.set(dataset.id, { used, available });
}
} catch (error) {
logger.warn("Could not retrieve root dataset space, continuing with physical pool space", {
url: this.integration.url,
error: describeRequestError(error),
});
}
return usableSpaceByPool;
}
/**
@@ -263,6 +343,19 @@ const poolSchema = z.array(
}),
);
// Properties are absent or unparsed for datasets that do not report them, e.g. while locked,
// so they are optional here and the pool falls back to its physical values instead of failing
// the whole request, which would also drop cpu, memory and network data
const poolDatasetPropertySchema = z.object({ parsed: z.number().min(0).nullish() }).nullish();
const poolDatasetSchema = z.array(
z.object({
id: z.string(),
used: poolDatasetPropertySchema,
available: poolDatasetPropertySchema,
}),
);
const reportingNetDataSchema = z.array(
z.object({
name: z.string(),
@@ -0,0 +1,48 @@
// @vitest-environment node
import { beforeEach, describe, expect, test, vi } from "vitest";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { HomeAssistantIntegration } from "../src";
vi.mock("@homarr/core/infrastructure/http", () => ({
fetchWithTrustedCertificatesAsync: vi.fn(),
}));
const mockFetch = vi.mocked(fetchWithTrustedCertificatesAsync);
const integration = new HomeAssistantIntegration({
id: "home-assistant",
name: "Home Assistant",
url: "http://home-assistant.local:8123",
externalUrl: null,
decryptedSecrets: [{ kind: "apiKey", value: "secret" }],
});
describe("HomeAssistantIntegration actions", () => {
beforeEach(() => {
mockFetch.mockReset();
mockFetch.mockResolvedValue({ ok: true } as never);
});
test.each([
["toggle", () => integration.triggerToggleAsync("light.desk"), "/api/services/homeassistant/toggle", "light.desk"],
[
"automation",
() => integration.triggerAutomationAsync("automation.turn_on_lights"),
"/api/services/automation/trigger",
"automation.turn_on_lights",
],
])("sends the %s service call as JSON", async (_action, trigger, path, entityId) => {
await expect(trigger()).resolves.toBe(true);
expect(mockFetch).toHaveBeenCalledWith(new URL(`http://home-assistant.local:8123${path}`), {
headers: {
Authorization: "Bearer secret",
"Content-Type": "application/json",
},
body: JSON.stringify({ entity_id: entityId }),
method: "POST",
});
});
});
@@ -18,4 +18,6 @@ export const smartHomeEntityStateRequestHandler = createIntegrationRequestHandle
return result.data.state;
},
// The widget already polls every 30 seconds and refetches after toggles.
cacheTtlMs: 0,
});
+48 -2
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "What's Up Docker",
"description": "Controla quins contenidors Docker tenen actualitzacions d'imatges disponibles."
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "aquí",
"iconPicker": {
"label": "URL de la icona",
"header": "Escriviu un nom o objectes per filtrar icones... Homarr cercarà entre {countIcons} icones per a vosaltres.",
"headerLoading": "",
"description": "Cerca a la biblioteca, enganxa l'URL d'una imatge o puja un fitxer.",
"header": "Cerca {countIcons} imatges per nom",
"headerLoading": "Cerca imatges per nom",
"clear": "Esborra la imatge",
"uploadImage": "Puja una imatge",
"results": "Resultats de la cerca d'imatges",
"localImages": "Les teves imatges",
"svgIcons": "Icones SVG",
"otherImages": "Altres imatges",
"urlLoading": "Comprovant la imatge…",
"urlReady": "La imatge està a punt per utilitzar",
"urlError": "No s'ha pogut carregar aquesta imatge. Comprova l'URL i torna-ho a provar.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "Mostra quants contenidors supervisats tenen actualitzacions disponibles.",
"title": "What's Up Docker",
"monitored": "Monitoritzats",
"updatesAvailable": "{count, plural, one {# actualització disponible} other {# actualitzacions disponibles}}",
"updateAvailable": "Actualització disponible",
"openReleaseNotes": "Obre les notes de versió",
"option": {
"showTitle": {
"label": "Mostra el títol"
},
"showRing": {
"label": "Mostra l'anell d'actualització"
},
"layout": {
"label": "Disposició",
"option": {
"horizontal": {
"label": "Horitzontal"
},
"vertical": {
"label": "Vertical"
}
}
},
"showUpdateList": {
"label": "Mostra la llista d'actualitzacions de contenidors",
"description": "Llista cada contenidor monitoritzat que té una actualització d'imatge disponible"
}
}
},
"dnsHoleSummary": {
"name": "Resum del forat DNS",
"description": "Mostra el resum del vostre forat DNS",
+189 -143
View File
@@ -1018,11 +1018,11 @@
"releasesProvider": "发布",
"notifications": "通知",
"firewall": "防火墙",
"timetable": "时刻表",
"timetable": "时间表",
"photoService": "照片",
"documents": "文档",
"notes": "笔记",
"mediaMonitoring": "监控",
"mediaMonitoring": "媒体监控",
"speedtest": "速度测试",
"analytics": "分析",
"vpn": "VPN",
@@ -1310,8 +1310,8 @@
"newLabel": "新建私钥"
},
"slug": {
"label": "状态页面标识",
"newLabel": "新的状态页面标识"
"label": "状态页标识",
"newLabel": "新状态页标识"
}
}
},
@@ -1322,7 +1322,7 @@
},
"audiobookshelf": {
"name": "Audiobookshelf",
"description": "自托管有声书和播客服务器"
"description": "自托管有声读物和播客服务器"
},
"navidrome": {
"name": "Navidrome",
@@ -1330,13 +1330,13 @@
},
"uptimeKuma": {
"name": "Uptime Kuma",
"description": "自托管运行时间监控,带有公开状态页",
"description": "自托管运行时间监控,支持公开状态页",
"secret": {
"slug": {
"description": "公开状态页的标识。如果未设置,默认为 \"default\"。"
"description": "您公开状态页的标识。如未设置,默认值为“default”。"
},
"apiKey": {
"description": "用于经过身份验证的 /metrics 访问的可选 API 密钥"
"description": "用于访问 /metrics 认证接口的可选 API 密钥"
}
}
},
@@ -1344,9 +1344,13 @@
"name": "Traefik",
"description": "云原生反向代理和负载均衡器"
},
"wud": {
"name": "What's Up Docker",
"description": "监控您哪些 Docker 容器有可用的镜像更新"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "用于归档和组织扫描文档的文档管理系统"
"description": "用于归档和整理扫描文档的文档管理系统"
},
"patchmon": {
"name": "PatchMon",
@@ -1398,7 +1402,7 @@
}
},
"noResults": {
"title": "未上传媒体"
"title": "暂无上传的媒体"
}
},
"common": {
@@ -1437,8 +1441,18 @@
"here": "这里",
"iconPicker": {
"label": "图标 URL",
"description": "搜索库,粘贴图片URL,或上传文件。",
"header": "输入名称或对象来筛选图标… Homarr 将为您搜索 {countIcons} 图标。",
"headerLoading": "输入以搜索图标…",
"clear": "清除图像",
"uploadImage": "上传图片",
"results": "图像搜索结果",
"localImages": "你的映像",
"svgIcons": "SVG 图标",
"otherImages": "其他映像",
"urlLoading": "",
"urlReady": "图像已准备好使用",
"urlError": "无法加载此图像。请检查URL,然后重试。",
"noResults": "没有与“{search}”匹配的图标。",
"error": "无法加载图标。"
},
@@ -1663,7 +1677,7 @@
"app": "编辑应用"
},
"app": {
"propagationNotice": "对此应用的更改将应用于所有面板中使用它的所有位置。"
"propagationNotice": "对此应用的更改将应用于所有面板中使用该应用的位置。"
},
"advancedOptions": {
"label": "高级选项",
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "显示有多少个已监控的容器有可用更新。",
"title": "What's Up Docker",
"monitored": "已监控",
"updatesAvailable": "{count, plural, one {# 个更新可用} other {# 个更新可用}}",
"updateAvailable": "有可用更新",
"openReleaseNotes": "打开发布说明",
"option": {
"showTitle": {
"label": "显示标题"
},
"showRing": {
"label": "显示更新进度环"
},
"layout": {
"label": "布局",
"option": {
"horizontal": {
"label": "水平"
},
"vertical": {
"label": "垂直"
}
}
},
"showUpdateList": {
"label": "显示容器更新列表",
"description": "列出每个有镜像更新可用的已监控容器"
}
}
},
"dnsHoleSummary": {
"name": "DNS Hole 摘要",
"description": "显示 DNS Hole 的摘要",
@@ -2486,7 +2532,7 @@
},
"ups": {
"name": "UPS",
"description": "通过 NUT 服务器监控 UPS 设备的状态",
"description": "通过 NUT 服务器监控您的 UPS 设备状态",
"option": {
"showBattery": {
"label": "显示电量"
@@ -3784,35 +3830,35 @@
"option": {
"showArtists": {
"label": "显示艺术家",
"description": "显示音乐库中艺术家的总数 (Navidrome)"
"description": "显示音乐库中的艺术家总数(Navidrome)"
},
"showAlbums": {
"label": "显示专辑",
"description": "显示音乐库中专辑的总数 (Navidrome)"
"description": "显示音乐库中的专辑总数(Navidrome)"
},
"showSongs": {
"label": "显示歌曲",
"description": "显示音乐库中歌曲的总数 (Navidrome)"
"description": "显示音乐库中的歌曲总数(Navidrome)"
},
"showLibraryCount": {
"label": "显示库数量",
"description": "显示实例中的库数量 (Audiobookshelf)"
"label": "显示媒体库数量",
"description": "显示实例中的媒体库数量(Audiobookshelf)"
},
"showAudiobooks": {
"label": "显示有声书",
"description": "显示所有库中的有声书总数 (Audiobookshelf)"
"label": "显示有声读物",
"description": "显示所有媒体库中的有声读物总数(Audiobookshelf)"
},
"showPodcasts": {
"label": "显示播客",
"description": "显示所有库中的播客总数 (Audiobookshelf)"
"description": "显示所有媒体库中的播客总数(Audiobookshelf)"
},
"showListeningTime": {
"label": "显示收听时间",
"description": "显示您的总收听时间 (Audiobookshelf)"
"label": "显示收听时长",
"description": "显示总收听时长(Audiobookshelf)"
},
"showActiveSessions": {
"label": "显示活跃会话",
"description": "显示当前活跃的播放会话数量 (Audiobookshelf)"
"description": "显示当前活跃播放会话的数量(Audiobookshelf)"
},
"compactMode": {
"label": "紧凑模式",
@@ -3822,8 +3868,8 @@
"artists": "艺术家",
"albums": "专辑",
"songs": "歌曲",
"libraryCount": "库",
"audiobooks": "有声书",
"libraryCount": "媒体库",
"audiobooks": "有声读物",
"podcasts": "播客",
"listeningTime": "收听时长",
"activeSessions": "活跃会话",
@@ -3923,10 +3969,10 @@
},
"beszelSystemTable": {
"name": "Beszel 系统(表格)",
"description": "所有 Beszel 监控系统的表格视图,具有可排序列和状态指示器",
"description": "所有 Beszel 监控系统的表格视图,包含可排序列和状态指示器",
"option": {
"statusFilter": {
"label": "状态过滤器"
"label": "状态筛选"
},
"sortBy": {
"label": "排序方式"
@@ -4057,17 +4103,17 @@
}
},
"beszelAlerts": {
"name": "Beszel 警报",
"description": "查看 Beszel 警报配置和历史记录",
"name": "Beszel 告警",
"description": "查看 Beszel 告警配置和历史记录",
"option": {
"showHistory": {
"label": "显示警报历史"
"label": "显示告警历史"
},
"maxHistoryItems": {
"label": "最大历史记录条数"
}
},
"empty": "未配置警报",
"empty": "未配置告警",
"error": {
"internalServerError": "无法连接到 Beszel。请检查您的集成设置。"
},
@@ -4079,15 +4125,15 @@
}
},
"beszelSystemStats": {
"name": "Beszel 系统状态",
"description": "CPU、内存、磁盘、网络和 Docker 容器指标的时间序列图表",
"name": "Beszel 系统统计",
"description": "CPU、内存、磁盘、网络和 Docker 容器指标的时序图表",
"option": {
"systemId": {
"label": "系统",
"description": "选择要显示统计信息的 Beszel 系统"
},
"timePeriod": {
"label": "时间段"
"label": "时间范围"
},
"showCpu": {
"label": "显示 CPU 图表"
@@ -4127,12 +4173,12 @@
"chart": {
"cpu": {
"title": "CPU 使用率",
"subtitle": "平均系统 CPU 利用率",
"subtitle": "系统整体 CPU 平均使用率",
"series": "CPU"
},
"memory": {
"title": "内存使用率",
"subtitle": "记录时的精确利用率",
"subtitle": "记录时间点的精确使用率",
"series": "已用",
"cache": "缓存/缓冲区"
},
@@ -4154,17 +4200,17 @@
},
"network": {
"title": "带宽",
"subtitle": "公共接口的网络流量",
"sent": "已发送",
"subtitle": "公网接口的网络流量",
"sent": "发送",
"recv": "已接收"
},
"dockerCpu": {
"title": "Docker CPU 使用率",
"subtitle": "容器的平均 CPU 利用率"
"subtitle": "容器的平均 CPU 使用率"
},
"dockerMemory": {
"title": "Docker 内存使用率",
"subtitle": "Docker 容器的内存使用"
"subtitle": "Docker 容器的内存使用率"
},
"dockerNetwork": {
"title": "Docker 网络 I/O",
@@ -4572,7 +4618,7 @@
"docker": "Docker",
"kubernetes": "Kubernetes",
"logs": "日志",
"api": "API",
"api": "API 与 MCP",
"certificates": "证书",
"tasks": "任务",
"backup": "备份"
@@ -4690,19 +4736,19 @@
"firstDayOfWeek": "一周的第一天",
"accessibility": "无障碍服务",
"onboardingTours": {
"title": "入门指南",
"description": "重置入门指南,以便在下次访问管理中心或面板时再次看到它们。",
"reset": "重置入门指南",
"title": "新手指南",
"description": "重置新手指南,以便您下次访问管理区域或面板时再次看到它们。",
"reset": "重置新手指南",
"resetSuccess": {
"title": "指南已重置",
"message": "入门指南将在您下次访问时再次显示"
"title": "新手指南已重置",
"message": "新手指南将在您下次访问时再次显示"
},
"status": {
"completed": "已完成",
"notStarted": "未开始"
},
"name": {
"management": "管理中心",
"management": "管理",
"dashboard": "面板"
}
}
@@ -4829,7 +4875,7 @@
"analytics": {
"title": "分析",
"general": {
"title": "发送匿名分析",
"title": "协助我们改进 Homarr",
"text": "这有助于我们了解用户实际使用哪些功能,从而决定将开发精力投入到哪里。我们会收集诸如您拥有多少小组件或集成、Homarr 版本以及使用的数据库类型等信息。仅此而已——不包含个人信息、URL、名称,也不含任何可能识别您本人或您个人配置的内容。"
}
},
@@ -5033,84 +5079,84 @@
"mcp": {
"label": "AI / MCP",
"title": "AI 集成",
"subtitle": "模型上下文协议 (MCP) 服务器",
"description": "将 AI 助手连接到您的 Homarr 实例,通过自然语言控制整个家庭实验室。在 Overseerr 上搜索和请求媒体,管理 Docker 容器,切换 Pi-hole,检查服务器健康状况,控制 Home Assistant 设备等——一切通过您的 AI 助手完成。",
"noApiKey": "您需要一个 API 密钥来与 MCP 服务器进行身份验证。",
"subtitle": "模型上下文协议(MCP)服务器",
"description": "将 AI 助手连接到您的 Homarr 实例,通过自然语言控制您的整个家庭实验室。在 Overseerr 上搜索和请求媒体资源,管理 Docker 容器,切换 Pi-hole,检查服务器健康状态,控制 Home Assistant 设备等——全部通过您的 AI 助手完成。",
"noApiKey": "您需要 API 密钥才能通过 MCP 服务器进行认证。",
"createApiKeyButton": "创建 API 密钥",
"endpoint": {
"title": "MCP 端点"
},
"authentication": {
"title": "认证",
"intro": "您可以使用 API 密钥或 OAuth 与 MCP 服务器进行身份验证。两种方法授予相同的访问级别。",
"intro": "您可以使用 API 密钥或 OAuth 对 MCP 服务器进行认证。两种方法授予的访问权限级别相同。",
"apiKey": {
"title": "API 密钥 (手动)",
"description": "在 MCP 客户端配置中传递带有完整 API 密钥的 ApiKey 标头。"
"title": "API 密钥(手动)",
"description": "在 MCP 客户端配置中通过 ApiKey 标头传递您的完整 API 密钥。"
},
"oauth": {
"title": "OAuth (浏览器登录)",
"description": "支持 OAuth 的客户端(如 Claude Code、opencode 或 Cursor)可以通过打开浏览器窗口进行身份验证。在客户端中运行 auth 命令,在浏览器中登录 Homarr,即可连接——API 密钥会自动创建。",
"hint": "OAuth 令牌在身份验证选项卡中显示为常规 API 密钥。您可以随时撤销它们。"
"title": "OAuth(浏览器登录)",
"description": "支持 OAuth 的客户端(如 Claude Code、opencode 或 Cursor)可通过打开浏览器窗口进行认证。在客户端中运行 auth 命令,在浏览器中登录 Homarr,即可完成连接——API 密钥将自动创建。",
"hint": "OAuth 令牌在“认证”标签页中显示为常规 API 密钥。您可以随时撤销它们。"
},
"permissions": "API 密钥决定了 AI 可用的权限——它与密钥所有者具有相同的访问级别。"
"permissions": "PI 密钥决定了 AI 可用的权限——它的访问级别与密钥所有者相同。"
},
"clientConfig": {
"title": "客户端配置",
"streamableHttp": {
"recommended": "推荐",
"title": "可流式 HTTP",
"worksWith": "适用于: Cursor, Claude Desktop, VS Code Copilot, Windsurf, Claude Code",
"addToConfig": "添加到您的 MCP 客户端配置文件:",
"cursor": "{name}: 项目或全局设置中的 {file}",
"title": "可流式传输的 HTTP",
"worksWith": "适用于:Cursor、Claude Desktop、VS Code Copilot、Windsurf、Claude Code",
"addToConfig": "添加到您的 MCP 客户端配置文件中:",
"cursor": "{name}:项目或全局设置中的 {file}",
"claudeDesktop": "{name}: {file}",
"claudeCode": "{name}: {file} 或使用 {command}",
"vsCodeCopilot": "{name}: 工作区设置中的 {file}",
"claudeCode": "{name}:{file} 或使用 {command}",
"vsCodeCopilot": "{name}:工作区设置中的 {file}",
"replaceHint": "将占位符替换为您在创建时收到的 API 密钥。"
},
"stdio": {
"title": "stdio (通过 mcp-remote 桥接的旧客户端)",
"description": "仅支持 stdio 传输的客户端。需要本地安装 {dependency} (Node.js):"
"title": "stdio(通过 mcp-remote 桥接的旧版客户端)",
"description": "适用于仅支持 stdio 传输的客户端。需要本地安装 {dependency}(Node.js):"
}
},
"testConnection": {
"title": "测试您的连接",
"description": "在终端中运行此命令以验证端点是否可达以及 API 密钥是否有效:",
"hint": "成功响应将返回 MCP 服务器功能。如果出现错误,请检查 JSON 响应以获取提示。"
"description": "在终端中运行以下命令,以验证端点可访问且您的 API 密钥有效:",
"hint": "成功响应将返回 MCP 服务器的能力信息。如果看到错误,请检查 JSON 响应中的提示。"
},
"availableTools": {
"title": "可用工具",
"description": "{count} 个工具可用。展开类别以查看各个工具及其描述。",
"description": "{count} 个工具可用。展开分类可查看各个工具及其描述。",
"columnType": "类型",
"columnName": "工具",
"columnDescription": "描述"
},
"troubleshooting": {
"title": "故障排除",
"title": "故障排查",
"missingHeader": {
"title": "\"缺少 ApiKey 标头\"",
"description": "您的客户端未发送 {header} 标头。验证 MCP 配置中的 {section} 部分是否正确。某些客户端在配置更改后需要重启。"
"title": "“缺少 ApiKey 标头”",
"description": "您的客户端未发送 {header} 标头。请检查 MCP 配置中的 {section} 部分是否正确。某些客户端在更改配置后需要重启。"
},
"invalidFormat": {
"title": "\"无效的 ApiKey 格式\"",
"description": "确保您复制了完整的 API 密钥,而不仅仅是部分。"
"title": "“ApiKey 格式无效”",
"description": "请确保您复制了完整的 API 密钥,而非仅复制了其中一部分。"
},
"invalidKey": {
"title": "\"无效的 API 密钥\"",
"description": "找不到密钥 ID 或令牌错误。API 密钥仅在创建时可见一次。如果您丢失了它,请删除旧密钥并创建新密钥。"
"title": "“API 密钥无效”",
"description": "未找到该密钥 ID,或令牌有误。API 密钥仅在创建时可见一次。如已丢失,请删除旧密钥并创建新密钥。"
},
"connectionRefused": {
"title": "连接被拒绝 / 超时",
"description": "确保 URL 可从您的 AI 客户端访问。如果 Homarr 在反向代理或 Docker 后运行,请使用外部 URL (不是 {localhost})。验证端口是否正确 (默认: Docker 为 7575)。"
"description": "请确保您的 AI 客户端能够访问该 URL。如果 Homarr 运行在反向代理后或 Docker 中,请使用外部 URL(而非 {localhost})。请确认端口是否正确(Docker 默认端口为 7575)。"
},
"toolsNotAppearing": {
"title": "工具未出现",
"description": "某些工具需要配置特定的集成(例如 Docker、Overseerr)。工具列表取决于 Homarr 实例中的可用内容。"
"title": "工具未显示",
"description": "部分工具需要先配置特定的集成(例如 Docker、Overseerr)。工具列表取决于您的 Homarr 实例中可用的功能。"
}
},
"copy": "复制",
"oneClick": {
"title": "一键安装",
"subtitle": "使用 OAuth — 首次使用时在浏览器中进行身份验证",
"subtitle": "使用 OAuth 认证——首次使用时在浏览器中进行身份验证",
"vsCode": "在 VS Code 中安装",
"cursor": "在 Cursor 中安装",
"copyUrl": "复制 MCP URL",
@@ -5123,16 +5169,16 @@
},
"backup": {
"title": "完整数据库备份",
"metaTitle": "备份与恢复",
"metaTitle": "备份 & 恢复",
"export": {
"title": "导出数据库",
"description": "下载包含所有设置、用户、面板和加密集成密钥的 SQLite 数据库的完整备份。备份打包为 ZIP 文件,附带加密密钥和元数据。",
"button": "下载备份",
"description": "下载 SQLite 数据库的完整备份,包含所有设置、用户、面板和加密的集成密钥。备份文件以 ZIP 格式打包,内含加密密钥和元数据。",
"button": "备份下载",
"error": "导出失败",
"confirm": {
"title": "敏感数据导出",
"warningTitle": "此导出包含您的整个数据库。",
"warningBody": "备份包括所有用户、集成密钥(加密)、API 密钥、面板配置和应用数据。将导出的文件安全存储,不要分享。",
"warningTitle": "此导出包含您的完整数据库。",
"warningBody": "备份包含所有用户、集成密钥(已加密)、API 密钥、面板配置和应用数据。请妥善保管导出的文件,切勿分享。",
"typePrompt": "输入 \"{phrase}\" 以继续",
"submit": "导出数据库",
"cancel": "取消"
@@ -5140,23 +5186,23 @@
},
"import": {
"title": "恢复数据库",
"description": "上传先前导出的备份 ZIP 以恢复您的数据库。这将替换您的整个数据库。如果备份是使用不同的加密密钥创建的,密钥将自动重新加密。",
"warning": "恢复备份将完全替换您当前的数据库并重启应用程序。此操作无法撤销。",
"description": "上传之前导出的备份 ZIP 文件以恢复数据库。此操作将替换整个数据库。如果备份使用的加密密钥不同,密钥将自动重新加密。",
"warning": "恢复备份将完全替换当前数据库并重启应用。此操作不可撤销。",
"dropzone": {
"title": "将备份 ZIP 拖到此处或点击浏览",
"description": "仅接受 Homarr SQLite 备份的 .zip 文件"
"title": "将备份 ZIP 文件拖放到此处,或点击浏览",
"description": "仅接受来自 Homarr SQLite 备份的 .zip 文件"
},
"changeFile": "更改文件",
"button": "恢复数据库",
"success": {
"title": "恢复完成",
"message": "数据库已成功恢复。应用程序正在重启……"
"message": "数据库已成功恢复。应用正在重启……"
},
"error": "导入失败"
},
"preview": {
"title": "备份内容",
"exported": "导出时间:{date}",
"exported": "导出于 {date}",
"loading": "正在分析备份文件……",
"errorTitle": "预览错误",
"entity": "实体",
@@ -5172,13 +5218,13 @@
"search_engine": "搜索引擎"
},
"analyzeError": "分析备份文件失败",
"invalidMissingMetadata": "无效备份:缺少 metadata.json",
"invalidMissingDb": "无效备份:缺少 db.sqlite"
"invalidMissingMetadata": "备份无效:缺少 metadata.json 文件",
"invalidMissingDb": "备份无效:缺少 db.sqlite 文件"
},
"restore": {
"dropzone": {
"title": "将备份 ZIP 拖到此处",
"description": "接受 Homarr SQLite 备份的 .zip 文件(最大 256 MB)"
"title": "将备份 ZIP 文件拖放到此处",
"description": "仅接受来自 Homarr SQLite 备份的 .zip 文件(最大 256 MB)"
},
"analyzing": "正在分析备份……",
"changeFile": "更改文件",
@@ -5187,7 +5233,7 @@
"analysisError": "分析错误",
"preview": {
"title": "备份内容",
"exported": "导出时间:{date}",
"exported": "导出于 {date}",
"entity": "实体",
"count": "数量",
"boards": "面板",
@@ -5200,43 +5246,43 @@
"media": "媒体",
"search_engine": "搜索引擎"
},
"migrationsApplied": "已应用 {count} 个迁移",
"migrationsAppliedPlural": "已应用 {count} 个迁移",
"migrationsOlderVersion": "此备份来自较旧的 Homarr 版本。已应用 {count} 个架构迁移以使其更新。",
"migrationsOlderVersionPlural": "此备份来自较旧的 Homarr 版本。已应用 {count} 个架构迁移以使其更新。",
"migrationsUpToDate": "数据库架构已是最新(已应用 {count} 个迁移)",
"upToDate": "已更新",
"migrationsPending": "{count} 个待处理"
"migrationsApplied": "已应用 {count} 项迁移",
"migrationsAppliedPlural": "已应用 {count} 项迁移",
"migrationsOlderVersion": "此备份来自较旧版本的 Homarr。已应用 {count} 项数据库迁移以将其更新至最新版本。",
"migrationsOlderVersionPlural": "此备份来自较旧版本的 Homarr。已应用 {count} 项数据库迁移以将其更新至最新版本。",
"migrationsUpToDate": "数据库架构已为最新(已应用 {count} 项迁移)",
"upToDate": "已是最新",
"migrationsPending": "{count} 项待处理"
},
"migration": {
"title": "正在应用迁移 ({current}/{total})",
"title": "正在应用迁移({current}/{total})",
"migrationLabel": "迁移 {index}"
},
"confirm": {
"title": "破坏性操作",
"warningTitle": "这将永久替换您的整个数据库。",
"warningBody": "所有当前面板、用户、应用、集成、设置和会话都将丢失。此操作无法撤销。应用将在恢复后重启。",
"warningBody": "所有现有面板、用户、应用、集成、设置和会话都将丢失。此操作不可撤销。恢复完成后应用将重启。",
"typePrompt": "输入 \"{phrase}\" 以继续",
"cancel": "取消",
"submit": "恢复数据库"
},
"progress": {
"title": "正在恢复数据库……",
"extracting": "提取备份存档",
"migrating": "应用数据库迁移",
"encrypting": "重新加密集成密钥",
"swapping": "替换数据库文件",
"restarting": "重启服务器"
"extracting": "正在解压备份压缩包",
"migrating": "正在应用数据库迁移",
"encrypting": "正在重新加密集成密钥",
"swapping": "正在替换数据库文件",
"restarting": "正在重启服务器"
},
"complete": {
"title": "恢复完成",
"message": "数据库已成功恢复。服务器正在重启——请几秒钟后刷新页面。"
"message": "数据库已成功恢复。服务器正在重启——请稍后刷新页面。"
},
"failed": {
"title": "恢复失败"
},
"invalidMissingMetadata": "无效备份:缺少 metadata.json",
"invalidMissingDb": "无效备份:缺少 db.sqlite",
"invalidMissingMetadata": "备份无效:缺少 metadata.json 文件",
"invalidMissingDb": "备份无效:缺少 db.sqlite 文件",
"analyzeError": "分析备份失败"
}
}
@@ -5918,14 +5964,14 @@
"title": "偏好设置",
"children": {
"detail": {
"title": "选择要更改的偏好",
"backHint": "按 Backspace 或使用返回按钮返回",
"title": "选择要更改的偏好设置",
"backHint": "按 Backspace 键或使用返回按钮返回",
"backAction": "返回"
}
},
"notification": {
"error": {
"message": "无法更新偏好"
"message": "无法更新偏好设置"
}
},
"option": {
@@ -5938,11 +5984,11 @@
},
"board": {
"none": "无",
"unavailable": "当前选择(不可用)"
"unavailable": "当前已选(不可用)"
},
"searchEngine": {
"none": "无",
"unavailable": "当前选择(不可用)"
"unavailable": "当前已选(不可用)"
},
"colorScheme": {
"label": "配色方案",
@@ -5977,42 +6023,42 @@
}
},
"ddgBangs": {
"label": "启用 DuckDuckGo bangs",
"label": "启用 DuckDuckGo Bang 快捷搜索",
"children": {
"detail": {
"title": "启用 DuckDuckGo bangs"
"title": "启用 DuckDuckGo Bang 快捷搜索"
}
}
},
"firstDayOfWeek": {
"label": "一周的第一天",
"label": "每周的第一天",
"children": {
"detail": {
"title": "选择一周的第一天"
"title": "选择每周的第一天"
}
}
},
"homeBoardId": {
"label": "主面板",
"label": "默认面板",
"children": {
"detail": {
"title": "选择您的主面板"
"title": "选择您的默认面板"
}
}
},
"mobileHomeBoardId": {
"label": "移动端主面板",
"label": "移动端默认面板",
"children": {
"detail": {
"title": "选择您的移动端主面板"
"title": "选择您的移动端默认面板"
}
}
},
"pingIconsEnabled": {
"label": "为 ping 使用图标",
"label": "使用图标表示 ping 状态",
"children": {
"detail": {
"title": "为 ping 使用图标"
"title": "使用图标表示 ping 状态"
}
}
},
@@ -6032,16 +6078,16 @@
}
},
"media": {
"help": "搜索电影或节目以请求",
"help": "搜索要请求的电影或剧集",
"requestMovie": "请求电影",
"requestSeries": "请求电视节目",
"openIn": "在 {kind} 中打开",
"group": {
"title": "电影和节目"
"title": "电影和剧集"
},
"action": {
"search": {
"label": "搜索电影或节目",
"label": "搜索电影或剧集",
"description": "开始输入以搜索您的媒体请求集成",
"disabled": {
"noIntegration": "添加 Overseerr、Jellyseerr 或 Seerr 集成以启用媒体搜索。",
@@ -6456,39 +6502,39 @@
"manage": {
"welcome": {
"title": "欢迎使用 Homarr!",
"description": "这是您的控制中心。您可以在此管理面板、应用、集成、用户和服务器设置。让我们浏览每个部分。"
"description": "这是您的控制中心。您可以在这里管理面板、应用、集成、用户和服务器设置。让我们逐一了解每个部分。"
},
"boardsList": {
"title": "您的面板",
"description": "这里是您的所有面板。每个面板都有自己的布局、背景和访问设置。您可以查看每个面板的创建者以及它是公开的还是私有的。"
"description": "这里是您的所有面板。每个面板都有自己的布局、背景和访问设置。您可以查看每个面板的创建者以及它是公开还是私有的。"
},
"boardsCreate": {
"title": "创建面板",
"description": "点击此处创建新面板或从以前的 Homarr 安装中导入。每个面板都有自己可共享的 URL。"
"description": "点击此处创建新面板,或从之前的 Homarr 安装中导入。每个面板都有独立的可分享链接。"
},
"appsList": {
"title": "您的应用",
"description": "这些是您添加的所有应用——指向自托管服务的链接。每个应用都有图标、可选的健康检查和面板上的状态徽章。"
"description": "这些是您已添加的所有应用——指向您自托管服务的链接。每个应用都配有图标、可选的健康检查,并在面板上显示状态标记。"
},
"appsCreate": {
"title": "添加应用",
"description": "在此注册新服务。为其命名、URL 和图标——它将作为磁贴显示在您的面板上,并带有实时状态监控。"
"description": "在此处注册新服务。设置名称、URL 和图标后,它将以磁贴形式出现在您的面板上,并带有实时状态监控。"
},
"integrationsList": {
"title": "您的集成",
"description": "这些是连接到 Homarr 的服务——Sonarr、Radarr、Plex、Jellyfin 及 50 多种其他服务。所有凭据在静态时均加密。"
"description": "这些是已连接到 Homarr 的服务——Sonarr、Radarr、Plex、Jellyfin 以及 50 多种其他服务。所有凭证均以加密方式存储。"
},
"integrationsCreate": {
"title": "添加集成",
"description": "在此连接新服务。选择类型,输入 URL 和 API 密钥——Homarr 在保存前会验证连接。"
"description": "在此处连接新服务。选择类型,输入 URL 和 API 密钥——Homarr 会在保存前验证连接。"
},
"usersList": {
"title": "您的用户",
"description": "查看所有有访问权限的人员。将用户组织到组中,并控制谁可以查看或编辑每个面板。"
"description": "查看所有拥有访问权限的用户。将用户归入不同组别,并控制谁可以查看或编辑每个面板。"
},
"usersCreate": {
"title": "创建用户",
"description": "在此添加新用户账户。您也可以使用邀请链接方便地引入。导览到此结束——您已准备就绪!"
"description": "在此处添加新用户账号。您也可以使用邀请链接简化入门流程。导览已结束——您已准备就绪!"
}
},
"board": {
@@ -6502,15 +6548,15 @@
},
"settings": {
"title": "面板设置",
"description": "更改此面板的名称、列数、背景、颜色以及访问权限。每个面板可以是公开或私有的。"
"description": "更改此面板的名称、列数、背景、颜色以及访问权限。每个面板均可设为公开或私有。"
},
"switcher": {
"title": "切换面板",
"description": "在面板间跳转。为不同目的创建独立的面板——媒体、网络、按用户或任何您喜欢的。"
"description": "在不同面板之间跳转。您可以为不同用途创建独立的面板——媒体、网络、按用户划分,或任何您喜欢的分类。"
},
"userMenu": {
"title": "您的个人资料",
"description": "更改您的配色方案、语言、主面板和偏好。您也可以从此处跳转到管理中心。"
"description": "更改您的配色方案、语言、默认面板和偏好设置。您也可以从这里跳转到管理区域。"
}
}
}
+48 -2
View File
@@ -1344,6 +1344,10 @@
"name": "crwdns5753:0crwdne5753:0",
"description": "crwdns5755:0crwdne5755:0"
},
"wud": {
"name": "crwdns6193:0crwdne6193:0",
"description": "crwdns6195:0crwdne6195:0"
},
"paperlessNgx": {
"name": "crwdns5003:0crwdne5003:0",
"description": "crwdns5005:0crwdne5005:0"
@@ -1437,8 +1441,18 @@
"here": "crwdns552:0crwdne552:0",
"iconPicker": {
"label": "crwdns554:0crwdne554:0",
"header": "crwdns556:0{countIcons}crwdne556:0",
"headerLoading": "crwdns5681:0crwdne5681:0",
"description": "crwdns6225:0crwdne6225:0",
"header": "crwdns6227:0{countIcons}crwdne6227:0",
"headerLoading": "crwdns6229:0crwdne6229:0",
"clear": "crwdns6231:0crwdne6231:0",
"uploadImage": "crwdns6233:0crwdne6233:0",
"results": "crwdns6235:0crwdne6235:0",
"localImages": "crwdns6237:0crwdne6237:0",
"svgIcons": "crwdns6239:0crwdne6239:0",
"otherImages": "crwdns6241:0crwdne6241:0",
"urlLoading": "crwdns6243:0crwdne6243:0",
"urlReady": "crwdns6245:0crwdne6245:0",
"urlError": "crwdns6247:0crwdne6247:0",
"noResults": "crwdns5683:0{search}crwdne5683:0",
"error": "crwdns5685:0crwdne5685:0"
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "crwdns6197:0crwdne6197:0",
"description": "crwdns6199:0crwdne6199:0",
"title": "crwdns6201:0crwdne6201:0",
"monitored": "crwdns6203:0crwdne6203:0",
"updatesAvailable": "crwdns6205:0count={count}crwdne6205:0",
"updateAvailable": "crwdns6207:0crwdne6207:0",
"openReleaseNotes": "crwdns6209:0crwdne6209:0",
"option": {
"showTitle": {
"label": "crwdns6211:0crwdne6211:0"
},
"showRing": {
"label": "crwdns6213:0crwdne6213:0"
},
"layout": {
"label": "crwdns6215:0crwdne6215:0",
"option": {
"horizontal": {
"label": "crwdns6217:0crwdne6217:0"
},
"vertical": {
"label": "crwdns6219:0crwdne6219:0"
}
}
},
"showUpdateList": {
"label": "crwdns6221:0crwdne6221:0",
"description": "crwdns6223:0crwdne6223:0"
}
}
},
"dnsHoleSummary": {
"name": "crwdns768:0crwdne768:0",
"description": "crwdns770:0crwdne770:0",
+48 -2
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Co je Docker nahoru",
"description": "Sledujte, které z vašich kontejnerů v Dockeru mají k dispozici aktualizace obrázků"
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "zde",
"iconPicker": {
"label": "URL ikony",
"header": "",
"headerLoading": "",
"description": "Vyhledejte knihovnu, vložte URL obrázku, nebo nahrajte soubor.",
"header": "Hledat {countIcons} obrázky podle jména",
"headerLoading": "Hledat obrázky podle jména",
"clear": "Vymazat obrázek",
"uploadImage": "Nahrát obrázek",
"results": "Výsledky vyhledávání obrázků",
"localImages": "Vaše obrázky",
"svgIcons": "SVG ikony",
"otherImages": "Ostatní obrázky",
"urlLoading": "Kontrola obrázku…",
"urlReady": "Obrázek je připraven k použití",
"urlError": "Tento obrázek nelze načíst. Zkontrolujte URL a zkuste to znovu.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Co je Docker nahoru",
"description": "Zobrazit, kolik sledovaných kontejnerů je k dispozici aktualizace.",
"title": "Co je Docker nahoru",
"monitored": "Monitorováno",
"updatesAvailable": "{count, plural, one {K dispozici je # aktualizace} other {# dostupných aktualizací}}",
"updateAvailable": "K dispozici je aktualizace",
"openReleaseNotes": "Otevřít poznámky k vydání",
"option": {
"showTitle": {
"label": "Zobrazit název"
},
"showRing": {
"label": "Zobrazit vyzvánění aktualizace"
},
"layout": {
"label": "Rozložení",
"option": {
"horizontal": {
"label": "Horizontální"
},
"vertical": {
"label": "Vertikální"
}
}
},
"showUpdateList": {
"label": "Zobrazit seznam aktualizací kontejneru",
"description": "Zobrazí seznam každého monitorovaného kontejneru, který má k dispozici aktualizaci obrázku"
}
}
},
"dnsHoleSummary": {
"name": "",
"description": "Zobrazí shrnutí vaší DNS Hole",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Hvad Er Opad Docker",
"description": "Overvåg hvilke af dine Docker-containere der er tilgængelige billedopdateringer"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Dokumenthåndteringssystem til arkivering og organisering af scannede dokumenter"
@@ -1437,8 +1441,18 @@
"here": "her",
"iconPicker": {
"label": "Ikon-URL",
"description": "Søg i biblioteket, indsæt en billed-URL, eller upload en fil.",
"header": "Indtast navn eller objekter for at filtrere for ikoner... Homarr vil søge gennem {countIcons} ikoner for dig.",
"headerLoading": "",
"headerLoading": "Søg billeder efter navn",
"clear": "Ryd billede",
"uploadImage": "Upload billede",
"results": "Billede søgeresultater",
"localImages": "Dine billeder",
"svgIcons": "SVG ikoner",
"otherImages": "Andre billeder",
"urlLoading": "Kontrollerer billede…",
"urlReady": "Billedet er klar til brug",
"urlError": "Dette billede kunne ikke indlæses. Tjek URL'en og prøv igen.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Hvad Er Opad Docker",
"description": "Vis hvor mange overvågede containere der er tilgængelige opdateringer.",
"title": "Hvad Er Opad Docker",
"monitored": "Overvåget",
"updatesAvailable": "{count, plural, one {# opdatering tilgængelig} other {# opdateringer tilgængelige}}",
"updateAvailable": "Opdatering tilgængelig",
"openReleaseNotes": "Åbn udgivelsesnoter",
"option": {
"showTitle": {
"label": "Vis titel"
},
"showRing": {
"label": "Vis opdateringsring"
},
"layout": {
"label": "Layout",
"option": {
"horizontal": {
"label": "Horisontal"
},
"vertical": {
"label": "Vertikal"
}
}
},
"showUpdateList": {
"label": "Vis liste over container-opdatering",
"description": "Lister hver overvåget beholder, der har en billedopdatering tilgængelig"
}
}
},
"dnsHoleSummary": {
"name": "DNS Hole Oversigt",
"description": "Viser oversigten over dit DNS-hul",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "",
"description": ""
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "Hier",
"iconPicker": {
"label": "",
"description": "",
"header": "Geben Sie Namen oder Objekte ein, um nach Symbolen zu filtern... Homarr sucht nach {countIcons} Icons für Sie.",
"headerLoading": "",
"clear": "",
"uploadImage": "",
"results": "",
"localImages": "",
"svgIcons": "",
"otherImages": "",
"urlLoading": "",
"urlReady": "",
"urlError": "",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "",
"description": "",
"title": "",
"monitored": "",
"updatesAvailable": "",
"updateAvailable": "",
"openReleaseNotes": "",
"option": {
"showTitle": {
"label": ""
},
"showRing": {
"label": ""
},
"layout": {
"label": "Ansicht",
"option": {
"horizontal": {
"label": ""
},
"vertical": {
"label": "Vertikal"
}
}
},
"showUpdateList": {
"label": "",
"description": ""
}
}
},
"dnsHoleSummary": {
"name": "DNS Hole Zusammenfassung",
"description": "Zeigt die Zusammenfassung deines DNS Holes an",
+105 -59
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "Cloud-nativer Reverse-Proxy und Lastverteiler"
},
"wud": {
"name": "Was ist Docker hoch",
"description": "Überwachen Sie, welche Docker-Container Bildupdates verfügbar sind"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Dokumentenverwaltungssystem zum Archivieren und Organisieren gescannter Dokumente"
@@ -1437,8 +1441,18 @@
"here": "Hier",
"iconPicker": {
"label": "Icon URL",
"description": "Suchen Sie die Bibliothek, fügen Sie eine Bild-URL ein oder laden Sie eine Datei hoch.",
"header": "Geben Sie Namen oder Objekte ein, um nach Symbolen zu filtern... Homarr sucht nach {countIcons} Icons für Sie.",
"headerLoading": "Tippe um die Symbole zu durchsuchen…",
"clear": "Bild löschen",
"uploadImage": "Bild hochladen",
"results": "Bildsuchergebnisse",
"localImages": "Ihre Bilder",
"svgIcons": "SVG-Symbole",
"otherImages": "Andere Bilder",
"urlLoading": "Bild wird überprüft…",
"urlReady": "Bild ist bereit zur Verwendung",
"urlError": "Dieses Bild konnte nicht geladen werden. Überprüfen Sie die URL und versuchen Sie es erneut.",
"noResults": "Keine Symbole stimmen mit \"{search}\" überein.",
"error": "Konnte Symbole nicht laden."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Was ist Docker hoch",
"description": "Zeigt an, wie viele überwachte Container über Updates verfügen.",
"title": "Was ist Docker hoch",
"monitored": "Überwacht",
"updatesAvailable": "{count, plural, one {# Update verfügbar} other {# Updates verfügbar}}",
"updateAvailable": "Update verfügbar",
"openReleaseNotes": "Versionshinweise öffnen",
"option": {
"showTitle": {
"label": "Titel anzeigen"
},
"showRing": {
"label": "Update-Ring anzeigen"
},
"layout": {
"label": "Ansicht",
"option": {
"horizontal": {
"label": "Horizontal"
},
"vertical": {
"label": "Vertikal"
}
}
},
"showUpdateList": {
"label": "Container-Update-Liste anzeigen",
"description": "Listet jeden überwachten Container auf, der eine Bildaktualisierung zur Verfügung hat"
}
}
},
"dnsHoleSummary": {
"name": "DNS Hole Zusammenfassung",
"description": "Zeigt die Zusammenfassung deines DNS Holes an",
@@ -2739,10 +2785,10 @@
},
"showBitrate": {
"label": "Zeigt die Bitrate an",
"description": ""
"description": "Zeige die aktuelle Wiedergabe-Bitrate neben dem Ort an"
},
"showLocation": {
"label": "",
"label": "Standort anzeigen",
"description": "Zeigt an, ob der Stream über das LAN oder WAN läuft"
}
},
@@ -2758,8 +2804,8 @@
"status": "Status",
"directPlay": "Direkte Wiedergabe",
"directStream": "Direkter Stream",
"transcodeVideo": "",
"transcodeAudio": "",
"transcodeVideo": "Video wird transkodiert",
"transcodeAudio": "Audio wird transkodiert",
"transcoding": "Transkodierung",
"paused": "Pausiert",
"remaining": "{minutes} Min. verbleibend",
@@ -2966,8 +3012,8 @@
}
},
"showOsVersion": {
"label": "",
"description": ""
"label": "Betriebssystem-Version anzeigen",
"description": "Füge die Betriebssystemversion jedem Verteilungsetikett hinzu, sofern verfügbar"
},
"showComplianceHero": {
"label": "",
@@ -4057,11 +4103,11 @@
}
},
"beszelAlerts": {
"name": "",
"description": "",
"name": "Beszel-Alarme",
"description": "Zeige Beszels Alarmeinstellungen und -verlauf",
"option": {
"showHistory": {
"label": ""
"label": "Alarmverlauf anzeigen"
},
"maxHistoryItems": {
"label": ""
@@ -4072,9 +4118,9 @@
"internalServerError": ""
},
"history": "Verlauf",
"resolved": "",
"resolved": "Gelöst",
"status": {
"triggered": "",
"triggered": "Ausgelöst",
"ok": "Ok"
}
},
@@ -4083,43 +4129,43 @@
"description": "",
"option": {
"systemId": {
"label": "",
"label": "System",
"description": ""
},
"timePeriod": {
"label": ""
"label": "Zeitraum"
},
"showCpu": {
"label": ""
"label": "CPU-Diagramm anzeigen"
},
"showMemory": {
"label": ""
"label": "RAM-Diagramm anzeigen"
},
"showDisk": {
"label": ""
"label": "Festplatten-Diagramm anzeigen"
},
"showDiskIO": {
"label": ""
"label": "Festplatten-E/A-Diagramm anzeigen"
},
"showNetwork": {
"label": ""
"label": "Netzwerk-Diagramm anzeigen"
},
"showDockerCpu": {
"label": ""
"label": "Docker-CPU-Diagramm anzeigen"
},
"showDockerMemory": {
"label": ""
"label": "Docker-RAM-Diagramm anzeigen"
},
"showDockerNetwork": {
"label": ""
"label": "Docker-Netzwerk-Diagramm anzeigen"
}
},
"selectSystem": "System auswählen",
"empty": {
"noSystems": ""
"noSystems": "Keine Systeme gefunden"
},
"error": {
"internalServerError": "",
"internalServerError": "Keine Verbindung zu Beszel möglich. Überprüfe deine Integrations-Einstellungen.",
"liveConnectionFailed": "",
"switchToHistorical": "",
"systemNotFound": ""
@@ -4139,36 +4185,36 @@
"disk": {
"title": "Festplattenbelegung",
"subtitle": "",
"series": ""
"series": "Root"
},
"diskIO": {
"title": "",
"title": "Festplatten-E/A",
"subtitle": "",
"read": "",
"write": ""
"read": "Lesen",
"write": "Schreiben"
},
"storage": {
"title": "",
"subtitle": "",
"series": ""
"title": "Speicher",
"subtitle": "Festplattenverbrauch pro Dateisystem",
"series": "Belegt"
},
"network": {
"title": "",
"subtitle": "",
"sent": "",
"recv": ""
"title": "Bandbreite",
"subtitle": "Netzwerkverkehr öffentlicher Schnittstellen",
"sent": "Gesendet",
"recv": "Empfangen"
},
"dockerCpu": {
"title": "",
"subtitle": ""
"title": "Docker-CPU-Nutzung",
"subtitle": "Durchschnittliche CPU-Nutzung der Container"
},
"dockerMemory": {
"title": "",
"subtitle": ""
"title": "Docker-RAM-Nutzung",
"subtitle": "RAM-Nutzung der Docker-Container"
},
"dockerNetwork": {
"title": "",
"subtitle": ""
"title": "Docker-Netzwerk-E/A",
"subtitle": "Netzwerkverkehr der Docker-Container"
}
}
}
@@ -4545,7 +4591,7 @@
"text": "Konnte die angeforderte Ressource nicht finden"
},
"error": {
"title": "",
"title": "Diese Seite konnte nicht geladen werden",
"text": "",
"digest": ""
},
@@ -5110,7 +5156,7 @@
"copy": "",
"oneClick": {
"title": "",
"subtitle": "",
"subtitle": "OAuth wird verwendet. Authentifiziere dich bei der ersten Nutzung im Browser",
"vsCode": "In VS Code installieren",
"cursor": "In Cursor installieren",
"copyUrl": "MCP URL kopieren",
@@ -5130,7 +5176,7 @@
"button": "Sicherung herunterladen",
"error": "Export fehlgeschlagen",
"confirm": {
"title": "",
"title": "Export sensibler Daten",
"warningTitle": "Dieser Export enthält Ihre gesamte Datenbank.",
"warningBody": "Das Backup umfasst alle Benutzer, Integrationsgeheimnisse (verschlüsselt), API-Keys, Board-Konfigurationen und App-Daten. Speichern Sie die exportierte Datei sicher ab und teilen Sie diese nicht.",
"typePrompt": "Geben Sie \"{phrase}\" ein, um fortzufahren",
@@ -5140,8 +5186,8 @@
},
"import": {
"title": "Datenbank wiederherstellen",
"description": "",
"warning": "",
"description": "Lade ein zuvor exportiertes Backup-ZIP‑File hoch, um deine Datenbank wiederherzustellen. Damit ersetzt du deine gesamte jetzige Datenbank. Falls das Backup mit einem anderen Verschlüsselungs-Key erstellt wurde, werden vertrauliche Daten (Secrets) automatisch neu verschlüsselt.",
"warning": "Ein Backup wiederherzustellen, wird deine jetzige Datenbank komplett ersetzen und das Programm neu starten. Diese Aktion kann nicht rückgängig gemacht werden.",
"dropzone": {
"title": "Ziehen Sie Ihre Backup-ZIP hierher oder klicken Sie zum Durchsuchen",
"description": "Nur .zip Dateien aus einer Homarr SQLite-Sicherung werden akzeptiert"
@@ -6332,10 +6378,10 @@
"header": {
"season": "Staffel",
"episodes": "Episoden",
"status": ""
"status": "Status"
},
"status": {
"requested": ""
"requested": "Angefragt"
}
},
"button": {
@@ -6448,18 +6494,18 @@
}
},
"onboardingTour": {
"learnMore": "",
"next": "",
"prev": "",
"done": "",
"skip": "",
"learnMore": "Lese die Dokumentation",
"next": "Weiter",
"prev": "Zurück",
"done": "Fertig",
"skip": "Überspringe die Einführung",
"manage": {
"welcome": {
"title": "",
"description": ""
"title": "Willkommen bei Homarr!",
"description": "Das ist dein Kontrollzentrum. Von hier steuerst du Boards, Apps, Integrationen, Benutzer und die Servereinstellungen. Lass uns eine Einführung in jeden Bereich machen."
},
"boardsList": {
"title": "",
"title": "Deine Boards",
"description": ""
},
"boardsCreate": {
@@ -6493,15 +6539,15 @@
},
"board": {
"welcome": {
"title": "",
"description": ""
"title": "Das ist dein Board",
"description": "Ein Drag-and-drop-Dashboard, wo du Apps und Widgets anordnen kannst. Du kannst jederzeit Strg+K drücken, um nach Apps, Boards und Aktionen zu suchen."
},
"editMode": {
"title": "Bearbeitungsmodus",
"description": ""
"description": "Drücke hier, um in den Bearbeitungsmodus zu kommen. Dort kannst du Widgets, Apps und Abschnitte hinzufügen, welche du durch anschließendes Ziehen neu anordnen und an den Ecken in der Größe anpassen kannst."
},
"settings": {
"title": "",
"title": "Board-Einstellungen",
"description": ""
},
"switcher": {
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Τι Είναι Το Docker",
"description": "Παρακολούθηση ποια από τα εμπορευματοκιβώτια Docker σας έχουν διαθέσιμες ενημερώσεις εικόνας"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Σύστημα διαχείρισης εγγράφων για αρχειοθέτηση και οργάνωση σαρωμένων εγγράφων"
@@ -1437,8 +1441,18 @@
"here": "εδώ",
"iconPicker": {
"label": "",
"description": "Αναζήτηση στη βιβλιοθήκη, επικόλληση ενός URL εικόνας ή μεταφόρτωση αρχείου.",
"header": "",
"headerLoading": "",
"headerLoading": "Αναζήτηση εικόνων κατά όνομα",
"clear": "Καθαρισμός εικόνας",
"uploadImage": "Μεταφόρτωση εικόνας",
"results": "Αποτελέσματα αναζήτησης εικόνας",
"localImages": "Οι εικόνες σας",
"svgIcons": "Εικονίδια SVG",
"otherImages": "Άλλες εικόνες",
"urlLoading": "",
"urlReady": "Η εικόνα είναι έτοιμη προς χρήση",
"urlError": "Δεν ήταν δυνατή η φόρτωση αυτής της εικόνας. Ελέγξτε τη διεύθυνση URL και προσπαθήστε ξανά.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Τι Είναι Το Docker",
"description": "Δείτε πόσες παρακολουθήσεις δοχείων έχουν διαθέσιμες ενημερώσεις.",
"title": "Τι Είναι Το Docker",
"monitored": "Ελέγχτηκε",
"updatesAvailable": "{count, plural, one {# διαθέσιμη ενημέρωση} other {# διαθέσιμες ενημερώσεις}}",
"updateAvailable": "Διαθέσιμη ενημέρωση",
"openReleaseNotes": "Άνοιγμα σημειώσεων έκδοσης",
"option": {
"showTitle": {
"label": "Εμφάνιση τίτλου"
},
"showRing": {
"label": "Εμφάνιση κουδουνίσματος ενημέρωσης"
},
"layout": {
"label": "Διάταξη",
"option": {
"horizontal": {
"label": "Οριζόντια"
},
"vertical": {
"label": "Κατακόρυφα"
}
}
},
"showUpdateList": {
"label": "Εμφάνιση λίστας ενημερώσεων περιέκτη",
"description": "Εμφανίζει κάθε δοχείο που παρακολουθείται και διαθέτει διαθέσιμη ενημέρωση εικόνας"
}
}
},
"dnsHoleSummary": {
"name": "",
"description": "",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "Cloud-native reverse proxy and load balancer"
},
"wud": {
"name": "",
"description": ""
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Document management system for archiving and organizing scanned documents"
@@ -1437,8 +1441,18 @@
"here": "here",
"iconPicker": {
"label": "Icon URL",
"description": "",
"header": "Please enter a name or object to filter for the icon. Homarr will search throughout {countIcons} different icons to find the right one.",
"headerLoading": "Type to search icons…",
"clear": "",
"uploadImage": "",
"results": "",
"localImages": "",
"svgIcons": "",
"otherImages": "",
"urlLoading": "",
"urlReady": "",
"urlError": "",
"noResults": "No icons match \"{search}\".",
"error": "Couldn't load icons."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "",
"description": "",
"title": "",
"monitored": "",
"updatesAvailable": "",
"updateAvailable": "",
"openReleaseNotes": "",
"option": {
"showTitle": {
"label": "Show title"
},
"showRing": {
"label": ""
},
"layout": {
"label": "Layout",
"option": {
"horizontal": {
"label": "Horizontal"
},
"vertical": {
"label": "Vertical"
}
}
},
"showUpdateList": {
"label": "",
"description": ""
}
}
},
"dnsHoleSummary": {
"name": "DNS Hole Summary",
"description": "Displays the summary of your DNS Hole",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Qué hay de nuevo Docker",
"description": "Controlar cuál de sus contenedores Docker tiene actualizaciones de imagen disponibles"
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "aquí",
"iconPicker": {
"label": "URL del icono",
"description": "Busca en la biblioteca, pega una URL de imagen o sube un archivo.",
"header": "Escribe nombre u objetos para filtrar iconos... Homarr buscará a través de iconos {countIcons} por ti.",
"headerLoading": "",
"headerLoading": "Buscar imágenes por nombre",
"clear": "Borrar imagen",
"uploadImage": "Subir imagen",
"results": "Resultados de búsqueda de imágenes",
"localImages": "Tus imágenes",
"svgIcons": "Iconos SVG",
"otherImages": "Otras imágenes",
"urlLoading": "Comprobando imagen…",
"urlReady": "La imagen está lista para usar",
"urlError": "Esta imagen no se pudo cargar. Compruebe la URL e inténtelo de nuevo.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Qué hay de nuevo Docker",
"description": "Mostrar cuántos contenedores vistos tienen actualizaciones disponibles.",
"title": "Qué hay de nuevo Docker",
"monitored": "Monitoreado",
"updatesAvailable": "{count, plural, one {# actualización disponible} other {# actualizaciones disponibles}}",
"updateAvailable": "Actualización disponible",
"openReleaseNotes": "Notas de la versión abierta",
"option": {
"showTitle": {
"label": "Mostrar título"
},
"showRing": {
"label": "Mostrar anillo de actualización"
},
"layout": {
"label": "Diseño",
"option": {
"horizontal": {
"label": "Horizontal"
},
"vertical": {
"label": "Vertical"
}
}
},
"showUpdateList": {
"label": "Mostrar lista de actualización del contenedor",
"description": "Lista cada contenedor supervisado que tiene una actualización de imagen disponible"
}
}
},
"dnsHoleSummary": {
"name": "Resumen del agujero DNS",
"description": "Muestra el resumen de tu agujero DNS",
File diff suppressed because it is too large. Load diff
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "Pilvinatiivi käänteinen välityspalvelin ja kuormantasaaja"
},
"wud": {
"name": "Mikä Ylös Docker",
"description": "Tarkkaile missä Docker-säiliöissä on saatavilla kuvapäivityksiä"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Asiakirjojen hallintajärjestelmä skannattujen dokumenttien arkistointiin ja järjestelyyn"
@@ -1437,8 +1441,18 @@
"here": "",
"iconPicker": {
"label": "Kuvakkeen URL-osoite",
"description": "Etsi kirjastosta, liitä kuvan URL-osoite tai lataa tiedosto.",
"header": "Suodata kuvakkeita syöttämällä nimi tai objekti... Homarr etsii {countIcons} kuvakkeesta.",
"headerLoading": "Kirjoita hakeaksesi kuvakkeita…",
"clear": "Tyhjennä kuva",
"uploadImage": "Lataa kuva",
"results": "Haun tulokset",
"localImages": "Omat kuvat",
"svgIcons": "SVG kuvakkeet",
"otherImages": "Muut kuvat",
"urlLoading": "",
"urlReady": "Kuva on valmis käytettäväksi",
"urlError": "Kuvaa ei voitu ladata. Tarkista URL-osoite ja yritä uudelleen.",
"noResults": "Ei kuvakkeita haulla \"{search}\".",
"error": "Kuvakkeiden lataaminen epäonnistui."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Mikä Ylös Docker",
"description": "Näytä kuinka monta katsottua astiaa on saatavilla päivityksiä.",
"title": "Mikä Ylös Docker",
"monitored": "Valvottu",
"updatesAvailable": "{count, plural, one {# päivitys saatavilla} other {# päivitystä saatavilla}}",
"updateAvailable": "Päivitys saatavilla",
"openReleaseNotes": "Avaa julkaisutiedot",
"option": {
"showTitle": {
"label": "Näytä otsikko"
},
"showRing": {
"label": "Näytä päivitysrengas"
},
"layout": {
"label": "Asettelu",
"option": {
"horizontal": {
"label": "Vaaka"
},
"vertical": {
"label": "Pysty"
}
}
},
"showUpdateList": {
"label": "Näytä kontin päivityslista",
"description": "Luettele jokaisen seuratun säiliön joka on saatavilla päivityksessä"
}
}
},
"dnsHoleSummary": {
"name": "",
"description": "",
+55 -9
View File
@@ -673,7 +673,7 @@
},
"action": {
"select": {
"label": "Sélectionner l'app",
"label": "Sélectionner une app",
"notFound": "Aucune app trouvée",
"search": "Rechercher une application",
"noResults": "Pas de résultats",
@@ -739,7 +739,7 @@
"label": "Étiquette",
"unit": "Unité",
"mappings": "Correspondances",
"tablePath": "",
"tablePath": "Chemin du tableau JSON",
"tablePathHint": "",
"columns": "Colonnes",
"header": "En-tête",
@@ -786,7 +786,7 @@
"valuePath": "",
"maxPath": "",
"goodValues": "",
"color": ""
"color": "bleu"
},
"cardStyleOption": {
"filled": "",
@@ -851,10 +851,10 @@
"customJsx": "JSX personnalisé"
},
"secret": {
"apiKey": "",
"apiKey": "Clé API / jeton",
"username": "Nom d'utilisateur",
"password": "Mot de passe",
"savedPlaceholder": ""
"savedPlaceholder": "••••••• (sauvegardé, laisser vide pour garder)"
},
"preview": {
"title": "Aperçu",
@@ -1291,10 +1291,10 @@
},
"patchmonApiKey": {
"label": "",
"newLabel": ""
"newLabel": "Nouvelle clé API"
},
"patchmonApiSecret": {
"label": "",
"label": "Clé secrète d'API",
"newLabel": ""
},
"githubAppId": {
@@ -1329,7 +1329,7 @@
"description": ""
},
"uptimeKuma": {
"name": "",
"name": "Uptime Kuma",
"description": "",
"secret": {
"slug": {
@@ -1341,9 +1341,13 @@
}
},
"traefik": {
"name": "",
"name": "Traefik",
"description": ""
},
"wud": {
"name": "Qu'est ce que Docker",
"description": "Surveiller lequel de vos conteneurs Docker a des mises à jour d'images disponibles"
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "ici",
"iconPicker": {
"label": "URL de l'icône :",
"description": "Recherchez dans la bibliothèque, collez une URL d'image, ou téléchargez un fichier.",
"header": "Tapez le nom ou les objets pour filtrer les icônes... Homarr recherchera les icônes {countIcons} pour vous.",
"headerLoading": "Tapez pour rechercher les icônes…",
"clear": "Effacer l'image",
"uploadImage": "Charger une image",
"results": "Résultats de la recherche d'images",
"localImages": "Vos images",
"svgIcons": "Icônes SVG",
"otherImages": "Autres images",
"urlLoading": "Vérification de l'image…",
"urlReady": "L'image est prête à être utilisée",
"urlError": "Cette image n'a pas pu être chargée. Vérifiez l'URL et réessayez.",
"noResults": "Aucune icône ne correspond à \"{search}\".",
"error": "Impossible de charger les icônes."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Qu'est ce que Docker",
"description": "Afficher combien de conteneurs surveillés ont des mises à jour disponibles.",
"title": "Qu'est ce que Docker",
"monitored": "Surveillé",
"updatesAvailable": "{count, plural, one {# mise à jour disponible} other {# mises à jour disponibles}}",
"updateAvailable": "Mise à jour disponible",
"openReleaseNotes": "Ouvrir les notes de version",
"option": {
"showTitle": {
"label": "Afficher le titre"
},
"showRing": {
"label": "Afficher l'anneau de mise à jour"
},
"layout": {
"label": "Mise en page",
"option": {
"horizontal": {
"label": "Horizontale"
},
"vertical": {
"label": "Vertical"
}
}
},
"showUpdateList": {
"label": "Afficher la liste de mise à jour du conteneur",
"description": "Liste chaque conteneur surveillé qui a une mise à jour d'image disponible"
}
}
},
"dnsHoleSummary": {
"name": "Résumé du puit DNS",
"description": "Affiche le résumé de votre puit DNS",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "",
"description": ""
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "כאן",
"iconPicker": {
"label": "קישור של הסמל",
"description": "",
"header": "הקלד שם או אובייקטים לסינון עבור אייקונים... Homarr יחפש עבורך סמלים של {countIcons} .",
"headerLoading": "",
"clear": "",
"uploadImage": "",
"results": "",
"localImages": "התמונות שלך",
"svgIcons": "",
"otherImages": "תמונות אחרות",
"urlLoading": "",
"urlReady": "",
"urlError": "",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "",
"description": "",
"title": "",
"monitored": "",
"updatesAvailable": "",
"updateAvailable": "",
"openReleaseNotes": "",
"option": {
"showTitle": {
"label": ""
},
"showRing": {
"label": ""
},
"layout": {
"label": "פריסה",
"option": {
"horizontal": {
"label": "אופקי"
},
"vertical": {
"label": "אנכי"
}
}
},
"showUpdateList": {
"label": "",
"description": ""
}
}
},
"dnsHoleSummary": {
"name": "סיכום חור אבטחה DNS",
"description": "מציג את הסיכום חור אבטחת DNS שלך",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "What's Up Docker",
"description": "Pratite koji od vaših Docker kontejnera imaju dostupna ažuriranja slika."
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "ovdje",
"iconPicker": {
"label": "URL ikone",
"description": "Pretražite biblioteku, zalijepite URL slike ili prenesite datoteku.",
"header": "Upišite naziv ili objekte za filtriranje ikona... Homarr će pretražiti {countIcons} ikona za vas.",
"headerLoading": "",
"headerLoading": "Pretražite slike po nazivu",
"clear": "Očisti sliku",
"uploadImage": "Prenesi sliku",
"results": "Rezultati pretraživanja slika",
"localImages": "Vaše slike",
"svgIcons": "SVG ikone",
"otherImages": "Ostale slike",
"urlLoading": "Provjera slike…",
"urlReady": "Slika je spremna za upotrebu",
"urlError": "Ova se slika nije mogla učitati. Provjerite URL i pokušajte ponovno.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "Prikaži koliko praćenih kontejnera ima dostupna ažuriranja.",
"title": "What's Up Docker",
"monitored": "Praćeno",
"updatesAvailable": "{count, plural, one {# dostupno ažuriranje} few {# dostupna ažuriranja} other {# dostupnih ažuriranja}}",
"updateAvailable": "Dostupno ažuriranje",
"openReleaseNotes": "Otvori bilješke o izdanju",
"option": {
"showTitle": {
"label": "Prikaži naslov"
},
"showRing": {
"label": "Prikaži prsten ažuriranja"
},
"layout": {
"label": "Raspored",
"option": {
"horizontal": {
"label": "Horizontalno"
},
"vertical": {
"label": "Okomito"
}
}
},
"showUpdateList": {
"label": "Prikaži popis ažuriranja kontejnera",
"description": "Nabraja svaki nadzirani kontejner za koji je dostupno ažuriranje slike."
}
}
},
"dnsHoleSummary": {
"name": "Sažetak DNS rupe",
"description": "Prikazuje sažetak vaše DNS rupe",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "",
"description": ""
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Dokumentumkezelő rendszer beolvasott dokumentumok archiválására és rendszerezésére"
@@ -1437,8 +1441,18 @@
"here": "itt",
"iconPicker": {
"label": "Ikonhivatkozás",
"description": "",
"header": "Írjon be nevet vagy objektumokat az ikonok szűréséhez... A Homarr a {countIcons} ikonok között keres.",
"headerLoading": "",
"clear": "",
"uploadImage": "",
"results": "",
"localImages": "",
"svgIcons": "",
"otherImages": "",
"urlLoading": "",
"urlReady": "",
"urlError": "",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "",
"description": "",
"title": "",
"monitored": "",
"updatesAvailable": "",
"updateAvailable": "",
"openReleaseNotes": "",
"option": {
"showTitle": {
"label": "Cím megjelenítése"
},
"showRing": {
"label": ""
},
"layout": {
"label": "Elrendezés",
"option": {
"horizontal": {
"label": "Vízszintes"
},
"vertical": {
"label": "Függőleges"
}
}
},
"showUpdateList": {
"label": "",
"description": ""
}
}
},
"dnsHoleSummary": {
"name": "DNS Blokkoló Összefoglaló",
"description": "Megjeleníti a DNS Blokkoló összefoglalóját",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "Cloud-native reverse proxy e bilanciatore del carico"
},
"wud": {
"name": "Cos'è Il Docker",
"description": "Monitora quali dei tuoi contenitori Docker sono disponibili aggiornamenti delle immagini"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Sistema di gestione documentale per archiviare e organizzare documenti scansionati"
@@ -1437,8 +1441,18 @@
"here": "qui",
"iconPicker": {
"label": "Icona URL",
"description": "Cerca nella libreria, incolla un URL di immagine, o carica un file.",
"header": "Digita il nome o gli oggetti da filtrare per le icone... Homarr cercherà tra le icone {countIcons} per te.",
"headerLoading": "Digita per cercare icone…",
"clear": "Cancella immagine",
"uploadImage": "Carica immagine",
"results": "Risultati della ricerca immagini",
"localImages": "Le tue immagini",
"svgIcons": "Icone SVG",
"otherImages": "Altre immagini",
"urlLoading": "Controllo immagine…",
"urlReady": "L'immagine è pronta per l'uso",
"urlError": "Questa immagine non può essere caricata. Controlla l'URL e riprova.",
"noResults": "Nessuna icona corrisponde a \"{search}\".",
"error": "Impossibile caricare le icone."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Cos'è Il Docker",
"description": "Mostra quanti contenitori visti hanno aggiornamenti disponibili.",
"title": "Cos'è Il Docker",
"monitored": "Monitorato",
"updatesAvailable": "{count, plural, one {# aggiornamento disponibile} other {# aggiornamenti disponibili}}",
"updateAvailable": "Aggiornamento disponibile",
"openReleaseNotes": "Apri note di rilascio",
"option": {
"showTitle": {
"label": "Mostra titolo"
},
"showRing": {
"label": "Mostra anello di aggiornamento"
},
"layout": {
"label": "Layout",
"option": {
"horizontal": {
"label": "Orizzontale"
},
"vertical": {
"label": "Verticale"
}
}
},
"showUpdateList": {
"label": "Mostra elenco aggiornamenti container",
"description": "Elenca ogni contenitore monitorato che ha un aggiornamento dell'immagine disponibile"
}
}
},
"dnsHoleSummary": {
"name": "Riepilogo del filtraggio DNS",
"description": "Mostra il riepilogo del tuo filtro DNS",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "What's Up Docker",
"description": "どのDockerコンテナがイメージアップデートを利用できるかを監視します"
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "こちら",
"iconPicker": {
"label": "アイコン URL",
"description": "ライブラリを検索し、画像の URL を貼り付けるか、ファイルをアップロードします。",
"header": "アイコンをフィルタするために、名前またはオブジェクトを入力してください... Homarrは {countIcons} アイコンを検索します。",
"headerLoading": "",
"headerLoading": "名前で画像を検索",
"clear": "画像をクリア",
"uploadImage": "画像をアップロード",
"results": "画像検索結果",
"localImages": "あなたの画像",
"svgIcons": "SVGアイコン",
"otherImages": "その他の画像",
"urlLoading": "",
"urlReady": "画像を使用する準備ができました",
"urlError": "この画像を読み込めませんでした。URLを確認して再度お試し下さい。",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "利用可能な更新があるコンテナの数を表示します。",
"title": "What's Up Docker",
"monitored": "監視中",
"updatesAvailable": "{count, plural, one {# アップデートが} other {# アップデートが}}",
"updateAvailable": "アップデートがあります",
"openReleaseNotes": "リリースノートを開く",
"option": {
"showTitle": {
"label": "タイトルを表示"
},
"showRing": {
"label": "更新の着信音を表示"
},
"layout": {
"label": "レイアウト",
"option": {
"horizontal": {
"label": "水平方向"
},
"vertical": {
"label": "垂直方向"
}
}
},
"showUpdateList": {
"label": "コンテナ更新リストを表示",
"description": "利用可能な画像の更新がある各監視対象のコンテナを一覧表示します"
}
}
},
"dnsHoleSummary": {
"name": "DNS Hole 概要",
"description": "DNS Hole の概要を表示します",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "클라우드 네이티브 리버스 프록시 및 로드 밸런서"
},
"wud": {
"name": "",
"description": ""
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "스캔된 문서를 보관하고 정리하는 문서 관리 시스템"
@@ -1437,8 +1441,18 @@
"here": "여기",
"iconPicker": {
"label": "아이콘 URL",
"description": "",
"header": "이름 또는 주제를 입력해 아이콘을 필터링하세요... Homarr가 {countIcons}에서 아이콘을 검색합니다.",
"headerLoading": "아이콘을 검색하려면 입력…",
"clear": "",
"uploadImage": "",
"results": "",
"localImages": "내 이미지",
"svgIcons": "",
"otherImages": "기타 이미지",
"urlLoading": "",
"urlReady": "",
"urlError": "",
"noResults": "\"{search}\"에 일치하는 아이콘이 없습니다.",
"error": "아이콘을 불러올 수 없습니다."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "",
"description": "",
"title": "",
"monitored": "",
"updatesAvailable": "",
"updateAvailable": "",
"openReleaseNotes": "",
"option": {
"showTitle": {
"label": ""
},
"showRing": {
"label": ""
},
"layout": {
"label": "레이아웃",
"option": {
"horizontal": {
"label": "가로"
},
"vertical": {
"label": "세로"
}
}
},
"showUpdateList": {
"label": "",
"description": ""
}
}
},
"dnsHoleSummary": {
"name": "DNS 홀 요약",
"description": "DNS 홀의 요약을 표시합니다",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Kas naujo, Docker",
"description": "Stebėkite, kuriems jūsų Docker konteineriams yra prieinami atvaizdų naujinimai."
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Dokumentų valdymo sistema, skirta nuskaitytų dokumentų archyvavimui ir tvarkymui"
@@ -1437,8 +1441,18 @@
"here": "čia",
"iconPicker": {
"label": "Piktogramos URL",
"description": "Ieškokite bibliotekoje, įklijuokite paveikslėlio URL arba įkelkite failą.",
"header": "Įveskite pavadinimą ar objektus, kad filtruoti piktogramas... Homarr ieškos per {countIcons} piktogramas.",
"headerLoading": "",
"headerLoading": "Ieškoti paveikslėlių pagal pavadinimą",
"clear": "Pašalinti paveikslėlį",
"uploadImage": "Įkelti paveikslėlį",
"results": "Vaizdų paieškos rezultatai",
"localImages": "Jūsų vaizdai",
"svgIcons": "SVG piktogramos",
"otherImages": "Kiti vaizdai",
"urlLoading": "Tikrinamas vaizdas…",
"urlReady": "Vaizdas paruoštas naudoti",
"urlError": "Šio vaizdo nepavyko įkelti. Patikrinkite URL ir bandykite dar kartą.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Kas naujo, Docker",
"description": "Rodo, kiek stebimų konteinerių turi prieinamų naujinimų.",
"title": "Kas naujo, Docker",
"monitored": "Stebimi",
"updatesAvailable": "{count, plural, one {# atnaujinimas prieinamas} few {# atnaujinimai prieinami} many {# atnaujinimų prieinama} other {# atnaujinimo prieinama}}",
"updateAvailable": "Galimas naujinimas",
"openReleaseNotes": "Atidaryti leidimo pastabas",
"option": {
"showTitle": {
"label": "Rodyti pavadinimą"
},
"showRing": {
"label": "Rodyti naujinimo žiedą"
},
"layout": {
"label": "Išdėstymas",
"option": {
"horizontal": {
"label": "Horizontalus"
},
"vertical": {
"label": "Vertikalus"
}
}
},
"showUpdateList": {
"label": "Rodyti konteinerių naujinimų sąrašą",
"description": "Išvardija kiekvieną stebimą konteinerį, kuriam yra prieinamas atvaizdo naujinimas"
}
}
},
"dnsHoleSummary": {
"name": "DNS skylės suvestinė",
"description": "Rodo jūsų DNS skylės suvestinę",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "What's Up Docker",
"description": "Uzrauga, kuriem no jūsu Docker konteineriem ir pieejami attēlu atjauninājumi"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Dokumentu pārvaldības sistēma skenētu dokumentu arhivēšanai un organizēšanai"
@@ -1437,8 +1441,18 @@
"here": "šeit",
"iconPicker": {
"label": "Ikonas URL",
"description": "Meklējiet bibliotēkā, ielīmējiet attēla URL vai augšupielādējiet failu.",
"header": "Rakstiet nosaukumu vai objektus, lai filtrētu ikonas... Homarr pārmeklēs {countIcons} ikonas jūsu vietā.",
"headerLoading": "",
"headerLoading": "Meklēt attēlus pēc nosaukuma",
"clear": "Notīrīt attēlu",
"uploadImage": "Augšupielādēt attēlu",
"results": "Attēlu meklēšanas rezultāti",
"localImages": "Jūsu attēli",
"svgIcons": "SVG ikonas",
"otherImages": "Citi attēli",
"urlLoading": "Pārbauda attēlu…",
"urlReady": "Attēls ir gatavs lietošanai",
"urlError": "Šo attēlu nevarēja ielādēt. Pārbaudiet URL un mēģiniet vēlreiz.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "Parāda, cik uzraudzītajiem konteineriem ir pieejami atjauninājumi.",
"title": "What's Up Docker",
"monitored": "Uzraudzītie",
"updatesAvailable": "{count, plural, zero {# atjauninājumu pieejami} one {# atjauninājums pieejams} other {# atjauninājumi pieejami}}",
"updateAvailable": "Pieejams atjauninājums",
"openReleaseNotes": "Atvērt laidiena piezīmes",
"option": {
"showTitle": {
"label": "Rādīt nosaukumu"
},
"showRing": {
"label": "Rādīt atjaunināšanas gredzenu"
},
"layout": {
"label": "Izkārtojums",
"option": {
"horizontal": {
"label": "Horizontāli"
},
"vertical": {
"label": "Vertikāli"
}
}
},
"showUpdateList": {
"label": "Rādīt konteineru atjauninājumu sarakstu",
"description": "Uzskaita katru uzraudzīto konteineru, kuram ir pieejams attēla atjauninājums"
}
}
},
"dnsHoleSummary": {
"name": "DNS cauruma kopsavilkums",
"description": "Parāda jūsu DNS cauruma kopsavilkumu",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Wat is er een Docker",
"description": "Controleer welke van uw Docker containers afbeeldingen updates beschikbaar hebben"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Documentbeheersysteem voor het archiveren en organiseren van gescande documenten"
@@ -1437,8 +1441,18 @@
"here": "hier",
"iconPicker": {
"label": "Icoon-URL",
"description": "Zoek in de bibliotheek, plak een afbeelding-URL of upload een bestand.",
"header": "Typ naam of objecten om te filteren op iconen… Homarr doorzoekt {countIcons} iconen voor je.",
"headerLoading": "",
"headerLoading": "Afbeeldingen zoeken op naam",
"clear": "Verwijder afbeelding",
"uploadImage": "Afbeelding uploaden",
"results": "Zoekresultaten afbeeldingen",
"localImages": "Jouw afbeeldingen",
"svgIcons": "SVG iconen",
"otherImages": "Andere afbeeldingen",
"urlLoading": "Controleren van afbeelding…",
"urlReady": "Afbeelding is klaar voor gebruik",
"urlError": "Deze afbeelding kan niet worden geladen. Controleer de URL en probeer het opnieuw.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Wat is er een Docker",
"description": "Laat zien hoeveel containers er updates beschikbaar hebben.",
"title": "Wat is er een Docker",
"monitored": "Gecontroleerd",
"updatesAvailable": "{count, plural, one {# update beschikbaar} other {# updates zijn beschikbaar}}",
"updateAvailable": "Update beschikbaar",
"openReleaseNotes": "Open release notities",
"option": {
"showTitle": {
"label": "Toon titel"
},
"showRing": {
"label": "Update ring tonen"
},
"layout": {
"label": "Lay-out",
"option": {
"horizontal": {
"label": "Horizontaal"
},
"vertical": {
"label": "Verticaal"
}
}
},
"showUpdateList": {
"label": "Toon container update lijst",
"description": "Lijst van alle gemonitorde container met een afbeelding update beschikbaar"
}
}
},
"dnsHoleSummary": {
"name": "DNS-hole overzicht",
"description": "Toont het overzicht van je DNS-hole",
File diff suppressed because it is too large. Load diff
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "Chmurowy reverse proxy i load balancer"
},
"wud": {
"name": "What's Up Docker",
"description": "Monitoruj, który z twoich kontenerów Docker ma dostępne aktualizacje obrazów"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "System zarządzania dokumentami do archiwizacji i organizacji zeskanowaych dokumnteów"
@@ -1437,8 +1441,18 @@
"here": "tutaj",
"iconPicker": {
"label": "adres URL ikony",
"description": "Przeszukaj bibliotekę, wklej adres URL obrazu lub wgraj plik.",
"header": "Wpisz nazwę lub obiekty do filtrowania ikon... Homarr przeszuka dla Ciebie ikony {countIcons}.",
"headerLoading": "Wpisz, aby wyszukać ikony…",
"clear": "Wyczyść obraz",
"uploadImage": "Prześlij obraz",
"results": "Wyniki wyszukiwania obrazów",
"localImages": "Twoje obrazy",
"svgIcons": "Ikony SVG",
"otherImages": "Inne obrazy",
"urlLoading": "Sprawdzanie obrazu…",
"urlReady": "Obraz jest gotowy do użycia",
"urlError": "Nie można załadować tego obrazu. Sprawdź adres URL i spróbuj ponownie.",
"noResults": "Brak ikon pasujących do \"{search}\".",
"error": "Nie udało się załadować ikon."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "Pokaż, ile oglądanych kontenerów ma dostępne aktualizacje.",
"title": "What's Up Docker",
"monitored": "Monitorowane",
"updatesAvailable": "{count, plural, one {# dostępna aktualizacja} other {# dostępne aktualizacje}}",
"updateAvailable": "Dostępna aktualizacja",
"openReleaseNotes": "Otwórz notatki o wydaniu",
"option": {
"showTitle": {
"label": "Pokaż tytuł"
},
"showRing": {
"label": "Pokaż pierścień aktualizacji"
},
"layout": {
"label": "Układ",
"option": {
"horizontal": {
"label": "Poziomy"
},
"vertical": {
"label": "Pionowy"
}
}
},
"showUpdateList": {
"label": "Pokaż listę aktualizacji kontenera",
"description": "Wymienia każdy monitorowany kontener z dostępną aktualizacją obrazu"
}
}
},
"dnsHoleSummary": {
"name": "Podsumowanie DNS Hole",
"description": "Wyświetla podsumowanie Twojego DNS Hole",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "O Docker",
"description": "Monitora qual dos seus contêineres Docker tem atualizações de imagem disponíveis"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Sistema de gerenciamento de documentos para arquivamento e organização de documentos digitalizados"
@@ -1437,8 +1441,18 @@
"here": "aqui",
"iconPicker": {
"label": "URL do ícone",
"description": "Procure na biblioteca, cole uma URL da imagem ou envie um arquivo.",
"header": "Digite o nome ou os objetos para filtrar os ícones... O Homarr pesquisará {countIcons} ícones para você.",
"headerLoading": "Digite para pesquisar ícones…",
"clear": "Limpar imagem",
"uploadImage": "Upload de imagem",
"results": "Resultados da busca por imagem",
"localImages": "Suas imagens",
"svgIcons": "Ícones SVG",
"otherImages": "Outras imagens",
"urlLoading": "Verificando imagem…",
"urlReady": "A imagem está pronta para uso",
"urlError": "Esta imagem não pôde ser carregada. Verifique a URL e tente novamente.",
"noResults": "Nenhum ícone corresponde a \"{search}\".",
"error": "Não foi possível carregar os ícones."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "O Docker",
"description": "Mostrar quantos contêineres assistidos possuem atualizações disponíveis.",
"title": "O Docker",
"monitored": "Monitorado",
"updatesAvailable": "{count, plural, one {# atualização disponível} other {# atualizações disponíveis}}",
"updateAvailable": "Atualização disponível",
"openReleaseNotes": "Abrir notas de lançamento",
"option": {
"showTitle": {
"label": "Mostrar título"
},
"showRing": {
"label": "Mostrar atualização de toque"
},
"layout": {
"label": "Layout",
"option": {
"horizontal": {
"label": "Horizontal"
},
"vertical": {
"label": "Vertical"
}
}
},
"showUpdateList": {
"label": "Mostrar lista de atualização do recipiente",
"description": "Lista cada container monitorado que tem uma atualização de imagem disponível"
}
}
},
"dnsHoleSummary": {
"name": "Resumo do DNS Hole",
"description": "Exibe o resumo do seu DNS Hole",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "O Docker",
"description": "Monitora qual dos seus contêineres Docker tem atualizações de imagem disponíveis"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Sistema de gerenciamento de documentos para arquivamento e organização de documentos digitalizados"
@@ -1437,8 +1441,18 @@
"here": "aqui",
"iconPicker": {
"label": "URL do ícone",
"description": "Procure na biblioteca, cole uma URL da imagem ou envie um arquivo.",
"header": "Digite o nome ou os objetos para filtrar os ícones... O Homarr pesquisará {countIcons} ícones para você.",
"headerLoading": "",
"headerLoading": "Pesquisar imagens por nome",
"clear": "Limpar imagem",
"uploadImage": "Upload de imagem",
"results": "Resultados da busca por imagem",
"localImages": "Suas imagens",
"svgIcons": "Ícones SVG",
"otherImages": "Outras imagens",
"urlLoading": "Verificando imagem…",
"urlReady": "A imagem está pronta para uso",
"urlError": "Esta imagem não pôde ser carregada. Verifique a URL e tente novamente.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "O Docker",
"description": "Mostrar quantos contêineres assistidos possuem atualizações disponíveis.",
"title": "O Docker",
"monitored": "Monitorado",
"updatesAvailable": "{count, plural, one {# atualização disponível} other {# atualizações disponíveis}}",
"updateAvailable": "Atualização disponível",
"openReleaseNotes": "Abrir notas de lançamento",
"option": {
"showTitle": {
"label": "Mostrar título"
},
"showRing": {
"label": "Mostrar atualização de toque"
},
"layout": {
"label": "Disposição",
"option": {
"horizontal": {
"label": "Horizontal"
},
"vertical": {
"label": "Vertical"
}
}
},
"showUpdateList": {
"label": "Mostrar lista de atualização do recipiente",
"description": "Lista cada container monitorado que tem uma atualização de imagem disponível"
}
}
},
"dnsHoleSummary": {
"name": "Resumo do DNS Hole",
"description": "Exibe o resumo do seu DNS Hole",
File diff suppressed because it is too large. Load diff
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Что же докер",
"description": "Мониторинг того, какие из контейнеров Docker доступны для обновления изображений"
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "здесь",
"iconPicker": {
"label": "URL иконки",
"description": "Поиск в библиотеке, вставьте URL изображения или загрузите файл.",
"header": "Введите название или объекты для фильтрации иконок... Homarr найдёт для вас среди {countIcons} иконок.",
"headerLoading": "",
"headerLoading": "Поиск изображений по имени",
"clear": "Очистить изображение",
"uploadImage": "Загрузить изображение",
"results": "Результаты поиска изображений",
"localImages": "Ваши изображения",
"svgIcons": "Значки SVG",
"otherImages": "Другие изображения",
"urlLoading": "Проверка изображения…",
"urlReady": "Изображение готово к использованию",
"urlError": "Это изображение не может быть загружено. Проверьте URL и повторите попытку.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Что же докер",
"description": "Показывать количество доступных обновлений в просмотренных контейнерах.",
"title": "Что же докер",
"monitored": "Мониторинг",
"updatesAvailable": "{count, plural, one {# доступно обновление} other {# доступно обновлений}}",
"updateAvailable": "Доступно обновление",
"openReleaseNotes": "Открыть примечания к релизу",
"option": {
"showTitle": {
"label": "Показать название"
},
"showRing": {
"label": "Показать обновление кольца"
},
"layout": {
"label": "Макет",
"option": {
"horizontal": {
"label": "Горизонтально"
},
"vertical": {
"label": "Вертикально"
}
}
},
"showUpdateList": {
"label": "Показать список обновлений контейнеров",
"description": "Список каждого контролируемого контейнера, имеющего доступное обновление изображений"
}
}
},
"dnsHoleSummary": {
"name": "Сводка DNS-фильтра",
"description": "Отображает сводную информацию о вашем DNS-фильтре",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "What's Up Docker",
"description": "Sledujte, ktoré z vašich Docker kontajnerov majú k dispozícii aktualizácie obrazov."
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Systém správy dokumentov na archiváciu a organizáciu naskenovaných dokumentov"
@@ -1437,8 +1441,18 @@
"here": "tu",
"iconPicker": {
"label": "URL ikony",
"description": "Vyhľadajte v knižnici, prilepte URL obrázka alebo nahrajte súbor.",
"header": "Zadajte názov alebo objekty na filtrovanie ikon... Homarr pre vás vyhľadá ikony na stránke {countIcons} .",
"headerLoading": "",
"headerLoading": "Vyhľadať obrázky podľa názvu",
"clear": "Odstrániť obrázok",
"uploadImage": "Nahrať obrázok",
"results": "Výsledky vyhľadávania obrázkov",
"localImages": "Vaše obrázky",
"svgIcons": "SVG ikony",
"otherImages": "Ostatné obrázky",
"urlLoading": "Kontrolujem obrázok…",
"urlReady": "Obrázok je pripravený na použitie",
"urlError": "Tento obrázok sa nepodarilo načítať. Skontrolujte adresu URL a skúste to znova.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "Zobrazuje, koľko sledovaných kontajnerov má k dispozícii aktualizácie.",
"title": "What's Up Docker",
"monitored": "Sledované",
"updatesAvailable": "{count, plural, one {# aktualizácia je k dispozícii} few {# aktualizácie sú k dispozícii} many {# aktualizácií je k dispozícii} other {# aktualizácie je k dispozícii}}",
"updateAvailable": "Dostupná aktualizácia",
"openReleaseNotes": "Otvorte poznámky k vydaniu",
"option": {
"showTitle": {
"label": "Zobraziť názov"
},
"showRing": {
"label": "Zobraziť prstenec aktualizácie"
},
"layout": {
"label": "Rozloženie",
"option": {
"horizontal": {
"label": "Horizontálne"
},
"vertical": {
"label": "Vertikálne"
}
}
},
"showUpdateList": {
"label": "Zobraziť zoznam aktualizácií kontajnerov",
"description": "Uvádza každý sledovaný kontajner, ktorý má dostupnú aktualizáciu obrazu."
}
}
},
"dnsHoleSummary": {
"name": "Zhrnutie dier DNS",
"description": "Zobrazí súhrn vašej diery DNS",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "What's Up Docker",
"description": "Spremljajte, kateri vaši Docker vsebniki imajo na voljo posodobitve slik."
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "tukaj",
"iconPicker": {
"label": "URL ikone",
"description": "Iščite po knjižnici, prilepite URL slike ali naložite datoteko.",
"header": "Vnesite ime ali predmete za filtriranje ikon... Homarr bo za vas iskal med {countIcons} ikonami.",
"headerLoading": "",
"headerLoading": "Išči slike po imenu",
"clear": "Počisti sliko",
"uploadImage": "Naloži sliko",
"results": "Rezultati iskanja slik",
"localImages": "Vaše slike",
"svgIcons": "Ikone SVG",
"otherImages": "Druge slike",
"urlLoading": "Preverjanje slike…",
"urlReady": "Slika je pripravljena za uporabo",
"urlError": "Te slike ni bilo mogoče naložiti. Preverite URL in poskusite znova.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "Prikaže, koliko spremljanih vsebnikov ima na voljo posodobitve.",
"title": "What's Up Docker",
"monitored": "Spremljani",
"updatesAvailable": "{count, plural, one {# posodobitev na voljo} two {# posodobitvi na voljo} few {# posodobitve na voljo} other {# posodobitev na voljo}}",
"updateAvailable": "Na voljo je posodobitev",
"openReleaseNotes": "Odpri opombe ob izdaji",
"option": {
"showTitle": {
"label": "Pokaži naslov"
},
"showRing": {
"label": "Pokaži obroč posodobitev"
},
"layout": {
"label": "Postavitev",
"option": {
"horizontal": {
"label": "Vodoravno"
},
"vertical": {
"label": "Navpično"
}
}
},
"showUpdateList": {
"label": "Pokaži seznam posodobitev vsebnikov",
"description": "Prikaže seznam vseh nadzorovanih vsebnikov, ki imajo na voljo posodobitev slike."
}
}
},
"dnsHoleSummary": {
"name": "Povzetek DNS luknje",
"description": "Prikaže povzetek vaše DNS luknje",
+48 -2
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "Vad är Upp Docker",
"description": "Övervaka vilken av dina Docker behållare har bilduppdateringar tillgängliga"
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Dokumenthanteringssystem för arkivering och organisering av skannade dokument"
@@ -1437,8 +1441,18 @@
"here": "",
"iconPicker": {
"label": "",
"header": "",
"headerLoading": "",
"description": "Sök i biblioteket, klistra in en bild-URL eller ladda upp en fil.",
"header": "Sök {countIcons} bilder efter namn",
"headerLoading": "Sök bilder med namn",
"clear": "Rensa bild",
"uploadImage": "Ladda upp bild",
"results": "Sökresultat för bild",
"localImages": "Dina bilder",
"svgIcons": "SVG ikoner",
"otherImages": "Andra bilder",
"urlLoading": "Kontrollerar bild…",
"urlReady": "Bilden är klar att användas",
"urlError": "Bilden kunde inte laddas. Kontrollera URL:en och försök igen.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Vad är Upp Docker",
"description": "Visa hur många bevakade behållare som har uppdateringar tillgängliga.",
"title": "Vad är Upp Docker",
"monitored": "Övervakad",
"updatesAvailable": "{count, plural, one {# uppdatering tillgänglig} other {# uppdateringar tillgängliga}}",
"updateAvailable": "Uppdatering tillgänglig",
"openReleaseNotes": "Öppna versionsfakta",
"option": {
"showTitle": {
"label": "Visa titel"
},
"showRing": {
"label": "Visa uppdateringsring"
},
"layout": {
"label": "Layout",
"option": {
"horizontal": {
"label": "Horisontell"
},
"vertical": {
"label": "Vertikal"
}
}
},
"showUpdateList": {
"label": "Visa uppdateringslista för behållare",
"description": "Listar varje övervakad behållare som har en bild uppdatering tillgänglig"
}
}
},
"dnsHoleSummary": {
"name": "",
"description": "",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "Bulut tabanlı ters proxy ve yük dengeleyici"
},
"wud": {
"name": "What's Up Docker",
"description": "Docker konteynerlerinizden hangilerinde imaj güncellemeleri olduğunu izleyin."
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "Taranmış belgeleri arşivlemek ve düzenlemek için belge yönetim sistemi"
@@ -1437,8 +1441,18 @@
"here": "buraya",
"iconPicker": {
"label": "Simge URL'si",
"description": "Kütüphanede arayın, bir görsel URL'si yapıştırın veya bir dosya yükleyin.",
"header": "Simgeleri filtrelemek için isim veya nesne yazın... Homarr sizin için {countIcons} simge arasında arama yapacaktır.",
"headerLoading": "Simge aramak için yazın…",
"clear": "Görseli temizle",
"uploadImage": "Görsel yükle",
"results": "Görsel arama sonuçları",
"localImages": "Görselleriniz",
"svgIcons": "SVG simgeleri",
"otherImages": "Diğer görseller",
"urlLoading": "Görsel kontrol ediliyor…",
"urlReady": "Görsel kullanıma hazır",
"urlError": "Bu görsel yüklenemedi. URL'yi kontrol edip yeniden deneyin.",
"noResults": "“{search}” aramasıyla eşleşen simge yok.",
"error": "Simgeler yüklenemedi."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "İzlenen konteynerlerin kaçında güncelleme mevcut olduğunu gösterir.",
"title": "What's Up Docker",
"monitored": "İzlenen",
"updatesAvailable": "{count, plural, one {# güncelleme mevcut} other {# güncelleme mevcut}}",
"updateAvailable": "Güncelleme mevcut",
"openReleaseNotes": "Sürüm notlarını aç",
"option": {
"showTitle": {
"label": "Başlığı göster"
},
"showRing": {
"label": "Güncelleme halkasını göster"
},
"layout": {
"label": "Düzen",
"option": {
"horizontal": {
"label": "Yatay"
},
"vertical": {
"label": "Dikey"
}
}
},
"showUpdateList": {
"label": "Konteyner güncelleme listesini göster",
"description": "İmaj güncellemesi mevcut olan her izlenen konteyneri listeler"
}
}
},
"dnsHoleSummary": {
"name": "DNS Çözümleyici Özeti",
"description": "DNS Çözümleyici özetini görüntüler",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "Хмарний зворотний проксі-сервер та балансувальник навантаження"
},
"wud": {
"name": "Що таке Docker",
"description": "Контролювати контейнери Docker мають доступні оновлення зображення"
},
"paperlessNgx": {
"name": "Безпаперовий ngx",
"description": "Система керування документами для архівування та організації відсканованих документів"
@@ -1437,8 +1441,18 @@
"here": "тут",
"iconPicker": {
"label": "URL іконки",
"description": "Знайдіть бібліотеку, вставте URL-адресу зображення або завантажте файл.",
"header": "Введіть назву або об'єкти для фільтрації значків... Homarr виконає пошук серед {countIcons} значків для вас.",
"headerLoading": "Введіть для пошуку іконок…",
"clear": "Очистити зображення",
"uploadImage": "Вивантажити зображення",
"results": "Результати пошуку зображень",
"localImages": "Ваші зображення",
"svgIcons": "Іконки SVG",
"otherImages": "Інші зображення",
"urlLoading": "Перевірка зображень…",
"urlReady": "Зображення можна використовувати",
"urlError": "Це зображення не може бути завантажене. Перевірте URL-адресу та повторіть спробу.",
"noResults": "Немає іконок, що відповідають{search}\".",
"error": "Не вдалося завантажити іконки."
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "Що таке Docker",
"description": "Показати скільки переглянутих контейнерів мають доступні оновлення.",
"title": "Що таке Docker",
"monitored": "Відстежено",
"updatesAvailable": "{count, plural, one {# оновлення доступні} other {# оновлень доступні}}",
"updateAvailable": "Доступне оновлення",
"openReleaseNotes": "Відкрити замітки до випуску",
"option": {
"showTitle": {
"label": "Показати назву"
},
"showRing": {
"label": "Показати кільце оновлення"
},
"layout": {
"label": "Макет",
"option": {
"horizontal": {
"label": "Горизонтально"
},
"vertical": {
"label": "Вертикально"
}
}
},
"showUpdateList": {
"label": "Показати список оновлень контейнерів",
"description": "Відображає список кожного моніторного контейнера, у якого є доступне оновлення зображення"
}
}
},
"dnsHoleSummary": {
"name": "Звіт DNS-Hole",
"description": "Відображає резюме вашого DNS Hole",
+47 -1
View File
@@ -1344,6 +1344,10 @@
"name": "",
"description": ""
},
"wud": {
"name": "What's Up Docker",
"description": "Theo dõi các container Docker của bạn có bản cập nhật hình ảnh."
},
"paperlessNgx": {
"name": "",
"description": ""
@@ -1437,8 +1441,18 @@
"here": "tại đây",
"iconPicker": {
"label": "URL biểu tượng",
"description": "Tìm kiếm trong thư viện, dán URL hình ảnh hoặc tải lên tệp.",
"header": "Nhập tên hoặc đối tượng để lọc biểu tượng... Homarr sẽ tìm qua {countIcons} biểu tượng cho bạn.",
"headerLoading": "",
"headerLoading": "Tìm kiếm hình ảnh theo tên",
"clear": "Xóa hình ảnh",
"uploadImage": "Tải lên hình ảnh",
"results": "Kết quả tìm kiếm hình ảnh",
"localImages": "Hình ảnh của bạn",
"svgIcons": "Biểu tượng SVG",
"otherImages": "Hình ảnh khác",
"urlLoading": "Đang kiểm tra hình ảnh…",
"urlReady": "Hình ảnh đã sẵn sàng để sử dụng",
"urlError": "Không thể tải hình ảnh này. Kiểm tra URL và thử lại.",
"noResults": "",
"error": ""
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "What's Up Docker",
"description": "Hiển thị số container được theo dõi có bản cập nhật.",
"title": "What's Up Docker",
"monitored": "Đã theo dõi",
"updatesAvailable": "{count, plural, other {# bản cập nhật có sẵn}}",
"updateAvailable": "Có bản cập nhật",
"openReleaseNotes": "Mở ghi chú phát hành",
"option": {
"showTitle": {
"label": "Hiển thị tiêu đề"
},
"showRing": {
"label": "Hiển thị vòng cập nhật"
},
"layout": {
"label": "Bố cục",
"option": {
"horizontal": {
"label": "Nằm ngang"
},
"vertical": {
"label": "Thẳng đứng"
}
}
},
"showUpdateList": {
"label": "Hiển thị danh sách cập nhật container",
"description": "Liệt kê từng container được giám sát có bản cập nhật image sẵn có"
}
}
},
"dnsHoleSummary": {
"name": "Tổng quan DNS Hole",
"description": "Hiển thị tổng quan về DNS Hole của bạn",
+46
View File
@@ -1344,6 +1344,10 @@
"name": "Traefik",
"description": "雲原生反向代理和負載平衡器"
},
"wud": {
"name": "",
"description": ""
},
"paperlessNgx": {
"name": "Paperless-ngx",
"description": "用於歸檔和組織掃描文件的文件管理系統"
@@ -1437,8 +1441,18 @@
"here": "此處",
"iconPicker": {
"label": "Icon 網址",
"description": "",
"header": "輸入名稱或物件來篩選 icons,Homarr 會自動搜尋 {countIcons}",
"headerLoading": "輸入以搜尋圖示…",
"clear": "",
"uploadImage": "",
"results": "",
"localImages": "你的圖像",
"svgIcons": "",
"otherImages": "其他圖像",
"urlLoading": "",
"urlReady": "",
"urlError": "",
"noResults": "沒有符合 \"{search}\" 的圖示。",
"error": "無法載入圖示。"
},
@@ -1793,6 +1807,38 @@
}
}
},
"wud": {
"name": "",
"description": "",
"title": "",
"monitored": "",
"updatesAvailable": "",
"updateAvailable": "",
"openReleaseNotes": "",
"option": {
"showTitle": {
"label": ""
},
"showRing": {
"label": ""
},
"layout": {
"label": "顯示布局",
"option": {
"horizontal": {
"label": "橫向"
},
"vertical": {
"label": "垂直"
}
}
},
"showUpdateList": {
"label": "",
"description": ""
}
}
},
"dnsHoleSummary": {
"name": "DNS Hole 概況",
"description": "顯示 DNS Hole 的概況",
@@ -1,10 +1,6 @@
import { describe, expect, test } from "vitest";
import {
filterStorageVolumes,
normalizeStorageDeviceName,
toScopedStorageVolumeValue,
} from "./filter-storage-volumes";
import { filterStorageVolumes, normalizeStorageDeviceName, toScopedStorageVolumeValue } from "./filter-storage-volumes";
import { matchFileSystemAndSmart } from "./health-monitoring/system-health";
describe("normalizeStorageDeviceName", () => {
@@ -13,6 +9,12 @@ describe("normalizeStorageDeviceName", () => {
expect(normalizeStorageDeviceName("/dev/sda")).toBe("/dev/sda");
});
test("strips partition suffixes from unprefixed device names (OpenMediaVault)", () => {
expect(normalizeStorageDeviceName("sda1")).toBe("sda");
expect(normalizeStorageDeviceName("sda")).toBe("sda");
expect(normalizeStorageDeviceName("nvme0n1p1")).toBe("nvme0n1");
});
test("preserves Synology-style volume names", () => {
expect(normalizeStorageDeviceName("volume_1")).toBe("volume_1");
expect(normalizeStorageDeviceName("volume_2")).toBe("volume_2");
@@ -143,4 +145,36 @@ describe("matchFileSystemAndSmart", () => {
overallStatus: "",
});
});
test("joins unprefixed OMV partitions with their base device SMART data", () => {
const result = matchFileSystemAndSmart(
[
{ deviceName: "sda1", used: "3.13 GiB", available: "26853056512", percentage: 12 },
{ deviceName: "sdb1", used: "396.24 GiB", available: "1542365618176", percentage: 22 },
],
[
{ deviceName: "sda", temperature: 0, overallStatus: "BAD_STATUS" },
{ deviceName: "sdb", temperature: 33, overallStatus: "GOOD" },
],
);
expect(result).toEqual([
{
deviceName: "sda",
used: "3.13 GiB",
available: "26853056512",
percentage: 12,
temperature: 0,
overallStatus: "BAD_STATUS",
},
{
deviceName: "sdb",
used: "396.24 GiB",
available: "1542365618176",
percentage: 22,
temperature: 33,
overallStatus: "GOOD",
},
]);
});
});
@@ -5,10 +5,16 @@ interface StorageVolumeEntry {
const partitionSuffixPatterns: ReadonlyArray<{ pattern: RegExp; baseGroupIndex: number }> = [
// SCSI/SATA/VirtIO: /dev/sda1 -> /dev/sda
{ pattern: /^(\/dev\/(?:sd|vd|hd|xvd)[a-z]+)[0-9]+$/, baseGroupIndex: 1 },
// SCSI/SATA/VirtIO without device node prefix: sda1 -> sda (OMV reports unprefixed names)
{ pattern: /^((?:sd|vd)[a-z]+)[0-9]+$/, baseGroupIndex: 1 },
// NVMe: /dev/nvme0n1p2 -> /dev/nvme0n1
{ pattern: /^(\/dev\/nvme[0-9]+n[0-9]+)p[0-9]+$/, baseGroupIndex: 1 },
// NVMe without device node prefix: nvme0n1p2 -> nvme0n1
{ pattern: /^(nvme[0-9]+n[0-9]+)p[0-9]+$/, baseGroupIndex: 1 },
// eMMC: /dev/mmcblk0p1 -> /dev/mmcblk0
{ pattern: /^(\/dev\/mmcblk[0-9]+)p[0-9]+$/, baseGroupIndex: 1 },
// eMMC without device node prefix: mmcblk0p1 -> mmcblk0
{ pattern: /^(mmcblk[0-9]+)p[0-9]+$/, baseGroupIndex: 1 },
];
export const normalizeStorageDeviceName = (deviceName: string): string => {
@@ -28,18 +34,14 @@ export const toScopedStorageVolumeValue = (integrationId: string, value: string)
return `${integrationId}:${volumeName}`;
};
const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => {
export const storageDeviceNamesMatch = (leftDeviceName: string, rightDeviceName: string): boolean => {
return (
leftDeviceName === rightDeviceName ||
normalizeStorageDeviceName(leftDeviceName) === normalizeStorageDeviceName(rightDeviceName)
);
};
const matchesVisibleStorageVolume = (
visibleVolume: string,
integrationId: string,
deviceName: string,
): boolean => {
const matchesVisibleStorageVolume = (visibleVolume: string, integrationId: string, deviceName: string): boolean => {
const separatorIndex = visibleVolume.indexOf(":");
if (separatorIndex === -1) {
return storageDeviceNamesMatch(visibleVolume, deviceName);
@@ -18,16 +18,18 @@ const isClusterIntegration = (integration: { kind: IntegrationKind }) =>
integration.kind === "proxmox" || integration.kind === "mock";
export default function HealthMonitoringWidget(props: WidgetComponentProps<"healthMonitoring">) {
const { data: integrations = [] } = clientApi.integration.byIds.useQuery(props.integrationIds);
const { data: integrations, isPending, isError } = clientApi.integration.byIds.useQuery(props.integrationIds);
const t = useI18n();
const clusterIntegrationId = integrations.find(isClusterIntegration)?.id;
if (isPending || isError) return null;
const clusterIntegrationId = integrations?.find(isClusterIntegration)?.id;
if (!clusterIntegrationId) {
return <SystemHealthMonitoring {...props} />;
}
const otherIntegrationIds = integrations
const otherIntegrationIds = (integrations ?? [])
// We want to have the mock integration also in the system tab, so we use it for both
.filter((integration) => integration.kind !== "proxmox")
.map((integration) => integration.id);
@@ -36,7 +36,7 @@ import { formatBytes } from "@homarr/common";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import { filterStorageVolumes, normalizeStorageDeviceName } from "../filter-storage-volumes";
import { filterStorageVolumes, storageDeviceNamesMatch } from "../filter-storage-volumes";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { CpuRing } from "./rings/cpu-ring";
@@ -296,12 +296,7 @@ interface SmartData {
export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: SmartData[]) => {
return fileSystems
.map((fileSystem) => {
const normalizedFileSystemName = normalizeStorageDeviceName(fileSystem.deviceName);
const smartDisk = smartData.find(
(smart) =>
smart.deviceName === fileSystem.deviceName ||
normalizeStorageDeviceName(smart.deviceName) === normalizedFileSystemName,
);
const smartDisk = smartData.find((smart) => storageDeviceNamesMatch(smart.deviceName, fileSystem.deviceName));
return {
deviceName: smartDisk?.deviceName ?? fileSystem.deviceName,
@@ -10,7 +10,7 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { filterStorageVolumes } from "../filter-storage-volumes";
import { filterStorageVolumes, storageDeviceNamesMatch } from "../filter-storage-volumes";
import { NoIntegrationDataError } from "../errors/no-data-integration";
type DisplayMode = WidgetComponentProps<"systemDisks">["options"]["displayMode"];
@@ -140,7 +140,7 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
return (
<Stack gap="xs" p="xs" h="100%">
{fileSystem.map((item) => {
const smartItem = smart.find((smart) => smart.deviceName === item.deviceName);
const smartItem = smart.find((smart) => storageDeviceNamesMatch(smart.deviceName, item.deviceName));
return (
<SystemDiskCard
+26
View File
@@ -0,0 +1,26 @@
{
"worktree": {
"setup": "mise trust\nmise install\npnpm install\n\nif [ -n \"${PASEO_SOURCE_CHECKOUT_PATH:-}\" ] && [ -f \"$PASEO_SOURCE_CHECKOUT_PATH/.env\" ]; then\n cp \"$PASEO_SOURCE_CHECKOUT_PATH/.env\" .env\nelse\n cp .env.example .env\nfi\n\nDB_PATH=\"$PWD/homarr.sqlite\"\nZERO_KEY=\"0000000000000000000000000000000000000000000000000000000000000000\"\nsed -i.bak \\\n -e \"s|^DB_DRIVER=.*|DB_DRIVER='better-sqlite3'|\" \\\n -e \"s|^DB_URL=.*|DB_URL='$DB_PATH'|\" \\\n -e \"s|^SECRET_ENCRYPTION_KEY=.*|SECRET_ENCRYPTION_KEY=$ZERO_KEY|\" \\\n .env\nrm -f .env.bak\n\nrm -f homarr.sqlite\npnpm db:migration:sqlite:run",
"teardown": "rm -f homarr.sqlite",
"servicePorts": {
"range": "3000-4000"
}
},
"scripts": {
"dev": {
"type": "service",
"command": "PORT=$PASEO_PORT pnpm -F nextjs dev"
},
"docs": {
"type": "service",
"command": "pnpm -F @homarr/docs dev --port $PASEO_PORT"
},
"build": { "command": "pnpm build" },
"test": { "command": "pnpm test" },
"test:e2e": { "command": "pnpm test:e2e" },
"lint": { "command": "pnpm lint" },
"typecheck": { "command": "pnpm typecheck" },
"db:seed": { "command": "pnpm db:seed" },
"db:studio": { "command": "pnpm db:studio" }
}
}
+25 -8
View File
@@ -7,14 +7,31 @@ export PGID=${PGID:-0}
echo "Starting with UID='$PUID', GID='$PGID'"
if [ "${PUID}" != "0" ] || [ "${PGID}" != "0" ]; then
# The below command will change the owner of all files in the /app directory (except node_modules) to the new UID and GID
echo "Changing owner to $PUID:$PGID, this will take about 10 seconds..."
find . -name 'node_modules' -prune -o -mindepth 1 -maxdepth 1 -exec chown -R $PUID:$PGID {} +
chown -R $PUID:$PGID /var/cache/nginx
chown -R $PUID:$PGID /var/log/nginx
chown -R $PUID:$PGID /var/lib/nginx
chown -R $PUID:$PGID /run/nginx/nginx.pid
chown -R $PUID:$PGID /etc/nginx
# Only chown the paths the non-root user must actually write to at runtime.
# Recursively chowning the whole /app tree is extremely slow on network- or
# HDD-backed storage (e.g. TrueNAS Apps), because the Next.js standalone
# bundle and its node_modules contain tens of thousands of tiny files. This
# turned a ~10s step into 15+ minutes for some users (#2190). Application
# code, migrations and native modules are only read/executed, so they stay
# owned by root and do not need chowning.
echo "Changing owner to $PUID:$PGID..."
# Application data volume (db, redis dumps, trusted certificates).
# Ensure the known subdirectories exist and migrate ownership of any
# existing content, so a persistent volume with stale or root-owned
# entries stays writable after a PUID/PGID change. These trees are small
# (a single sqlite db, a redis dump, a few certs), unlike the /app tree.
mkdir -p /appdata/db /appdata/redis /appdata/trusted-certificates
chown "${PUID}:${PGID}" /appdata
chown -R "${PUID}:${PGID}" /appdata/db /appdata/redis /appdata/trusted-certificates
# Next.js runtime cache (image optimization, fetch cache, ...)
mkdir -p /app/apps/nextjs/.next/cache
chown -R "${PUID}:${PGID}" /app/apps/nextjs/.next/cache
# nginx runtime directories and the rendered configuration
chown -R "${PUID}:${PGID}" /var/cache/nginx
chown -R "${PUID}:${PGID}" /var/log/nginx
chown -R "${PUID}:${PGID}" /var/lib/nginx
chown -R "${PUID}:${PGID}" /run/nginx/nginx.pid
chown -R "${PUID}:${PGID}" /etc/nginx
echo "Changing owner to $PUID:$PGID, done."
fi
+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 @@
[{"id":"zaxon793-jy8gpl0v-d89pbrkd-emwl5v04","name":"Mike","slug":"guest-5da646e8","imageUrl":"https://images.opencollective.com/guest-5da646e8/avatar.png"},{"id":"ywz9j4av-god8pgwg-db36mr35-nxklb0e7","name":"Guest","slug":"guest-554ec68f","imageUrl":"https://images.opencollective.com/guest-554ec68f/avatar.png"},{"id":"mlo94zn7-x08dpo09-xlxpewga-3vjbrky5","name":"Robert Hermann","slug":"robert-hermann","imageUrl":"https://images.opencollective.com/robert-hermann/avatar.png"},{"id":"zaxon793-jy8gpl0n-8r3pbrkd-emwl5v04","name":"Sebastian Raubuch","slug":"sebastian-raubuch","imageUrl":"https://images.opencollective.com/sebastian-raubuch/avatar.png"},{"id":"nmlo94zn-7x08dpod-ykm6ewga-3vjbrky5","name":"Didier LUTHI","slug":"didier-luthi","imageUrl":"https://images.opencollective.com/didier-luthi/avatar.png"},{"id":"vedj9wro-z3a56dgk-z3z67blg-8x4m0ykn","name":"Guest","slug":"guest-b1459a68","imageUrl":"https://images.opencollective.com/guest-b1459a68/avatar.png"},{"id":"gm9bnk80-437xqry4-8vepvzeo-ljdayw5r","name":"Mohister","slug":"mohister","imageUrl":"https://images.opencollective.com/mohister/1d8a592/avatar.png"},{"id":"vrgbk357-l4x96e70-m0apomew-a0jdyzn8","name":"sidens","slug":"sidens","imageUrl":"https://images.opencollective.com/sidens/avatar.png"},{"id":"mlo94zn7-x08dpo0d-l7epewga-3vjbrky5","name":"Chris","slug":"guest-937065bb","imageUrl":"https://images.opencollective.com/guest-937065bb/avatar.png"},{"id":"k9mbw7y4-8r3zq3ol-7zlq0ej5-lavnodgx","name":"Nick W","slug":"guest-7501840a","imageUrl":"https://images.opencollective.com/guest-7501840a/avatar.png"},{"id":"e0adkjrv-8xwm69o8-5al67z5n-4l93bgoy","name":"Jeppedy","slug":"guest-ab78e46a","imageUrl":"https://images.opencollective.com/guest-ab78e46a/avatar.png"},{"id":"a47byg9n-xozdp8xo-jgm6mjlv-03rek5w8","name":"naru","slug":"guest-02fd264a","imageUrl":"https://images.opencollective.com/guest-02fd264a/avatar.png"},{"id":"3k0exgzn-m8yj64g3-ll865wao-9r7b4dlv","name":"Jared Hocutt","slug":"jared-hocutt","imageUrl":"https://images.opencollective.com/jared-hocutt/avatar.png"},{"id":"eng0kzdy-vor4pz8l-exopbma8-37xlw95j","name":"Incognito","slug":"user-e63f4a5b","imageUrl":"https://images.opencollective.com/user-e63f4a5b/avatar.png"},{"id":"8k03reyd-5agmq50v-8vyplbwo-z7j4nxv9","name":"dazi","slug":"guest-11663a46","imageUrl":"https://images.opencollective.com/guest-11663a46/avatar.png"},{"id":"vedj9wro-z3a56dgd-zwl67blg-8x4m0ykn","name":"ZoopaMario","slug":"zoopamario","imageUrl":"https://images.opencollective.com/zoopamario/bea900d/avatar.png"},{"id":"a47byg9n-xozdp8xe-xmb6mjlv-03rek5w8","name":"FlamebergUA","slug":"guest-a59fd500","imageUrl":"https://images.opencollective.com/guest-a59fd500/avatar.png"},{"id":"8k03reyd-5agmq50y-kzoplbwo-z7j4nxv9","name":"StLCards","slug":"guest-e1de33dc","imageUrl":"https://images.opencollective.com/guest-e1de33dc/avatar.png"},{"id":"ejoxl3az-45w9pa54-dk46y870-mgkbrvdn","name":"Varadha","slug":"guest-6f68eed9","imageUrl":"https://images.opencollective.com/guest-6f68eed9/avatar.png"},{"id":"jrkx5lmn-v904qjl0-3oz68bwa-7zdygoe3","name":"Guest","slug":"guest-6da419b5","imageUrl":"https://images.opencollective.com/guest-6da419b5/avatar.png"},{"id":"mywxoz34-09rl6k3z-lmlqvenb-dj7gk85a","name":"Guest","slug":"guest-122d7e0a","imageUrl":"https://images.opencollective.com/guest-122d7e0a/avatar.png"},{"id":"3z8arxve-ymko60vm-3eaqgl5n-bj9w704d","name":"Ricardo","slug":"ricardo34","imageUrl":"https://images.opencollective.com/ricardo34/avatar.png"},{"id":"e0adkjrv-8xwm69or-reo67z5n-4l93bgoy","name":"GoM","slug":"guest-3944feab","imageUrl":"https://images.opencollective.com/guest-3944feab/avatar.png"},{"id":"gm9bnk80-437xqrnr-r5o6vzeo-ljdayw5r","name":"Brent Maxwell","slug":"brentmaxwell","imageUrl":"https://images.opencollective.com/brentmaxwell/avatar.png"},{"id":"gnxdwzj3-le5mpwzd-ll0py8rv-bko04a97","name":"Eric G","slug":"guest-478270f1","imageUrl":"https://images.opencollective.com/guest-478270f1/avatar.png"},{"id":"v349mrwg-z75lpyjo-ez8pa08d-jeybknox","name":"ninja","slug":"guest-0bb6ba36","imageUrl":"https://images.opencollective.com/guest-0bb6ba36/avatar.png"},{"id":"ywz9j4av-god8pgw0-53y6mr35-nxklb0e7","name":"s11","slug":"guest-6e9a2588","imageUrl":"https://images.opencollective.com/guest-6e9a2588/avatar.png"},{"id":"mlo94zn7-x08dpo30-v73pewga-3vjbrky5","name":"Incognito","slug":"user-cbad11af","imageUrl":"https://images.opencollective.com/user-cbad11af/avatar.png"},{"id":"v349mrwg-z75lpy7l-yzgpa08d-jeybknox","name":"Ronald Simmons","slug":"ronald-simmons","imageUrl":"https://images.opencollective.com/ronald-simmons/avatar.png"},{"id":"7ywz9j4a-vgod8pgl-me9qmr35-nxklb0e7","name":"Joel","slug":"guest-3376ab02","imageUrl":"https://images.opencollective.com/guest-3376ab02/avatar.png"},{"id":"v349mrwg-z75lpyjw-kxzpa08d-jeybknox","name":"Gokay Yamankurt","slug":"gokay-yamankurt","imageUrl":"https://images.opencollective.com/gokay-yamankurt/avatar.png"},{"id":"v349mrwg-z75lpyjv-59rpa08d-jeybknox","name":"Rhiz3K","slug":"rhiz3k","imagLine truncated
[{"id":"zaxon793-jy8gpl0v-d89pbrkd-emwl5v04","name":"Mike","slug":"guest-5da646e8","imageUrl":"https://images.opencollective.com/guest-5da646e8/avatar.png"},{"id":"ywz9j4av-god8pgwg-db36mr35-nxklb0e7","name":"Guest","slug":"guest-554ec68f","imageUrl":"https://images.opencollective.com/guest-554ec68f/avatar.png"},{"id":"mlo94zn7-x08dpo09-xlxpewga-3vjbrky5","name":"Robert Hermann","slug":"robert-hermann","imageUrl":"https://images.opencollective.com/robert-hermann/avatar.png"},{"id":"zaxon793-jy8gpl0n-8r3pbrkd-emwl5v04","name":"Sebastian Raubuch","slug":"sebastian-raubuch","imageUrl":"https://images.opencollective.com/sebastian-raubuch/avatar.png"},{"id":"nmlo94zn-7x08dpod-ykm6ewga-3vjbrky5","name":"Didier LUTHI","slug":"didier-luthi","imageUrl":"https://images.opencollective.com/didier-luthi/avatar.png"},{"id":"vedj9wro-z3a56dgk-z3z67blg-8x4m0ykn","name":"Guest","slug":"guest-b1459a68","imageUrl":"https://images.opencollective.com/guest-b1459a68/avatar.png"},{"id":"gm9bnk80-437xqry4-8vepvzeo-ljdayw5r","name":"Mohister","slug":"mohister","imageUrl":"https://images.opencollective.com/mohister/1d8a592/avatar.png"},{"id":"vrgbk357-l4x96e70-m0apomew-a0jdyzn8","name":"sidens","slug":"sidens","imageUrl":"https://images.opencollective.com/sidens/avatar.png"},{"id":"mlo94zn7-x08dpo0d-l7epewga-3vjbrky5","name":"Chris","slug":"guest-937065bb","imageUrl":"https://images.opencollective.com/guest-937065bb/avatar.png"},{"id":"k9mbw7y4-8r3zq3ol-7zlq0ej5-lavnodgx","name":"Nick W","slug":"guest-7501840a","imageUrl":"https://images.opencollective.com/guest-7501840a/avatar.png"},{"id":"e0adkjrv-8xwm69o8-5al67z5n-4l93bgoy","name":"Jeppedy","slug":"guest-ab78e46a","imageUrl":"https://images.opencollective.com/guest-ab78e46a/avatar.png"},{"id":"a47byg9n-xozdp8xo-jgm6mjlv-03rek5w8","name":"naru","slug":"guest-02fd264a","imageUrl":"https://images.opencollective.com/guest-02fd264a/avatar.png"},{"id":"3k0exgzn-m8yj64g3-ll865wao-9r7b4dlv","name":"Jared Hocutt","slug":"jared-hocutt","imageUrl":"https://images.opencollective.com/jared-hocutt/avatar.png"},{"id":"eng0kzdy-vor4pz8l-exopbma8-37xlw95j","name":"Incognito","slug":"user-e63f4a5b","imageUrl":"https://images.opencollective.com/user-e63f4a5b/avatar.png"},{"id":"8k03reyd-5agmq50v-8vyplbwo-z7j4nxv9","name":"dazi","slug":"guest-11663a46","imageUrl":"https://images.opencollective.com/guest-11663a46/avatar.png"},{"id":"vedj9wro-z3a56dgd-zwl67blg-8x4m0ykn","name":"ZoopaMario","slug":"zoopamario","imageUrl":"https://images.opencollective.com/zoopamario/bea900d/avatar.png"},{"id":"a47byg9n-xozdp8xe-xmb6mjlv-03rek5w8","name":"FlamebergUA","slug":"guest-a59fd500","imageUrl":"https://images.opencollective.com/guest-a59fd500/avatar.png"},{"id":"8k03reyd-5agmq50y-kzoplbwo-z7j4nxv9","name":"StLCards","slug":"guest-e1de33dc","imageUrl":"https://images.opencollective.com/guest-e1de33dc/avatar.png"},{"id":"ejoxl3az-45w9pa54-dk46y870-mgkbrvdn","name":"Varadha","slug":"guest-6f68eed9","imageUrl":"https://images.opencollective.com/guest-6f68eed9/avatar.png"},{"id":"jrkx5lmn-v904qjl0-3oz68bwa-7zdygoe3","name":"Guest","slug":"guest-6da419b5","imageUrl":"https://images.opencollective.com/guest-6da419b5/avatar.png"},{"id":"mywxoz34-09rl6k3z-lmlqvenb-dj7gk85a","name":"Guest","slug":"guest-122d7e0a","imageUrl":"https://images.opencollective.com/guest-122d7e0a/avatar.png"},{"id":"3z8arxve-ymko60vm-3eaqgl5n-bj9w704d","name":"Ricardo","slug":"ricardo34","imageUrl":"https://images.opencollective.com/ricardo34/avatar.png"},{"id":"e0adkjrv-8xwm69or-reo67z5n-4l93bgoy","name":"GoM","slug":"guest-3944feab","imageUrl":"https://images.opencollective.com/guest-3944feab/avatar.png"},{"id":"gm9bnk80-437xqrnr-r5o6vzeo-ljdayw5r","name":"Brent Maxwell","slug":"brentmaxwell","imageUrl":"https://images.opencollective.com/brentmaxwell/avatar.png"},{"id":"gnxdwzj3-le5mpwzd-ll0py8rv-bko04a97","name":"Eric G","slug":"guest-478270f1","imageUrl":"https://images.opencollective.com/guest-478270f1/avatar.png"},{"id":"v349mrwg-z75lpyjo-ez8pa08d-jeybknox","name":"ninja","slug":"guest-0bb6ba36","imageUrl":"https://images.opencollective.com/guest-0bb6ba36/avatar.png"},{"id":"ywz9j4av-god8pgw0-53y6mr35-nxklb0e7","name":"s11","slug":"guest-6e9a2588","imageUrl":"https://images.opencollective.com/guest-6e9a2588/avatar.png"},{"id":"mlo94zn7-x08dpo30-v73pewga-3vjbrky5","name":"Incognito","slug":"user-cbad11af","imageUrl":"https://images.opencollective.com/user-cbad11af/avatar.png"},{"id":"v349mrwg-z75lpy7l-yzgpa08d-jeybknox","name":"Ronald Simmons","slug":"ronald-simmons","imageUrl":"https://images.opencollective.com/ronald-simmons/avatar.png"},{"id":"7ywz9j4a-vgod8pgl-me9qmr35-nxklb0e7","name":"Joel","slug":"guest-3376ab02","imageUrl":"https://images.opencollective.com/guest-3376ab02/avatar.png"},{"id":"v349mrwg-z75lpyjw-kxzpa08d-jeybknox","name":"Gokay Yamankurt","slug":"gokay-yamankurt","imageUrl":"https://images.opencollective.com/gokay-yamankurt/avatar.png"},{"id":"v349mrwg-z75lpyjv-59rpa08d-jeybknox","name":"Rhiz3K","slug":"rhiz3k","imagLine 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