Compare commits

..
Author SHA1 Message Date
Thomas 9239e6ecc5 Merge branch 'dev' into perf/parallelize-board-prefetch 2026-05-27 09:38:16 +02:00
605af19cd8 refactor: replace ESLint + Prettier with OXC (oxlint + oxfmt) (#5741)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
Co-authored-by: Meierschlumpf <63781622+Meierschlumpf@users.noreply.github.com>
2026-05-27 09:37:44 +02:00
homarr-renovate[bot] b4921af60d chore(deps): update dependency sass to ^1.100.0 (#5795)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-27 05:24:00 +00:00
homarr-renovate[bot] 32385206ad chore(deps): update tiptap monorepo to v3.23.6 (#5793)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-26 21:03:11 +00:00
homarr-renovate[bot] 7e59ba3d5e chore(deps): update dependency vite@>=5.0.0 <=5.4.18 to >=8.0.14 (#5791)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-26 07:57:31 +00:00
homarr-renovate[bot] 81caaa9d6c chore(deps): update vitest monorepo to ^4.1.7 (#5789)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-25 08:30:36 +00:00
Thomas 8bb284e1d3 Delete apps/nextjs/src/app/[locale]/boards/(content)/_creator.spec.ts 2026-05-25 09:42:38 +02:00
homarr-renovate[bot] 09243148da chore(deps): update dependency ldapts to v8.1.8 (#5788)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-25 04:03:12 +00:00
homarr-update-contributors[bot] c7c57887cf chore: update contributors (#5771) 2026-05-24 22:44:36 +02:00
homarr-renovate[bot] 990a924631 chore(deps): update testcontainers-node monorepo to v12 (#5785) 2026-05-24 22:09:13 +02:00
homarr-renovate[bot] 83caf3ef4b chore(deps): update dependency @types/react to v19.2.15 (#5787)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 19:26:04 +00:00
homarr-renovate[bot] 991f07ae51 chore(deps): update tiptap monorepo to v3.23.5 (#5783)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 15:12:39 +00:00
homarr-renovate[bot] bc05128cd4 chore(deps): update dependency hono@<4.6.5 to >=4.12.21 (#5782)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 13:34:35 +00:00
homarr-renovate[bot] cf0fb8a879 chore(deps): update dependency hono@<4.6.5 to >=4.12.20 (#5780)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 11:25:35 +00:00
homarr-renovate[bot] 1f31735ee0 chore(deps): update dependency isomorphic-dompurify to ^3.14.0 (#5779)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 10:02:26 +00:00
homarr-crowdin[bot] ff974bf343 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-05-24 02:04:49 +00:00
homarr-renovate[bot] d42bbfce17 chore(deps): update tanstack-query monorepo to ^5.100.11 (#5777)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-23 20:13:50 +00:00
homarr-renovate[bot] 0130303e74 chore(deps): update dependency semver-parser to ^4.2.0 (#5766)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-23 15:08:34 +00:00
AustinandClaude Sonnet 4.6 66212b4e47 feat(board): persist category collapse state in localStorage for guests (#5714)
* feat(board): persist category collapse state in localStorage for guests

Authenticated users already had their collapse state saved server-side.
This extends the same behaviour to unauthenticated (guest) users by
reading/writing localStorage keyed by section ID.

On mount, if the user is a guest, the stored value is applied to the
disclosure. On toggle, guests write to localStorage instead of calling
the protected API mutation.

Closes #3355

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: set localStorage value as useDisclosure default instead of useEffect

Replaces the useEffect that read localStorage after mount with a direct
initial value computation passed to useDisclosure. Removes the effect,
the open/close destructuring, and the useEffect import.

Note: initialCollapsed is computed during render rather than post-mount,
which may cause a hydration mismatch on SSR if localStorage differs from
the server-provided section.collapsed. The typeof window guard handles
the SSR case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:25:10 +02:00
Thomas f10bb3ae23 feat: auto-open widget settings on add with all integrations pre-selected (#5767)
When adding a widget from the "Choose item to add" modal, the widget
edit modal now opens immediately after placement, allowing the user to
configure options and integrations before the widget renders.

All available integrations for the widget are selected by default.
2026-05-23 16:19:36 +02:00
homarr-renovate[bot] af1991a4a3 chore(deps): update dependency pg to ^8.21.0 (#5776)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-23 13:35:11 +00:00
homarr-releases[bot] 9a9551130c chore: update bug report template (#5775)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-05-22 21:01:54 +00:00
Meier Lukas a93f03eae4 Merge branch 'main' into dev 2026-05-22 22:46:15 +02:00
Thomas df4b57bd41 perf(boards): remove loading overlay and show board immediately (#5735) 2026-05-22 07:06:48 +00:00
Thomas Camlong 52fc678e44 test(boards): add tests for parallelized board page data fetching 2026-05-22 09:00:02 +02:00
Thomas Camlong 136c269935 perf(boards): parallelize board page data fetching
Board fetch + auth() now run concurrently via Promise.all instead of
sequentially. Widget prefetches for all kinds also run in parallel
alongside integration permission loading.
2026-05-21 14:01:01 +02:00
Releases Homarr 48ef8eb6c0 chore(release): 1.61.1 [skip ci] 2026-05-18 19:45:44 +00:00
Austin e9142c0dbc fix(downloads-widget): increase name column default width ratio 2026-05-18 21:44:47 +02:00
122 changed files with 1679 additions and 4740 deletions

No files matched your search

+3 -6
View File
@@ -3,9 +3,6 @@ version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[transformers]]
name = "prettier"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
+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.62.0
- 1.61.0
- 1.60.1-beta.1
- 1.60.0
+33 -1
View File
@@ -24,6 +24,38 @@ env:
TURBO_SCM_HEAD: ${{ github.sha }}
jobs:
lint-format-autofix:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Setup
uses: ./tooling/github/setup
- name: Get changed files
id: changed
run: |
FILES=$(git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' | tr '\n' ' ')
echo "files=$FILES" >> $GITHUB_OUTPUT
- name: Auto-fix lint + format
if: steps.changed.outputs.files != ''
run: |
pnpm exec oxlint --fix ${{ steps.changed.outputs.files }} || true
pnpm exec oxfmt --write ${{ steps.changed.outputs.files }}
- name: Commit fixes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "style: auto-fix lint and format"
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -54,7 +86,7 @@ jobs:
uses: ./tooling/github/setup
- name: Format
run: pnpm turbo format --affected --continue -- --cache --cache-location node_modules/.cache/.prettiercache
run: pnpm turbo format --affected --continue
typecheck:
runs-on: ubuntu-latest
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 120,
"endOfLine": "lf",
"sortPackageJson": true,
"ignorePatterns": ["**/*.cjs", "**/lang/**", "static-data/**", "CHANGELOG.md"]
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "import", "jsx-a11y"],
"categories": {
"correctness": "error",
"suspicious": "warn",
"pedantic": "off",
"perf": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"typescript/consistent-type-imports": ["warn", { "prefer": "type-imports", "fixStyle": "separate-type-imports" }],
"typescript/no-non-null-assertion": "error",
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"unicorn/no-new-array": "warn",
"unicorn/no-useless-spread": "warn",
"unicorn/no-useless-length-check": "warn",
"unicorn/no-useless-fallback-in-spread": "warn",
"jsx-a11y/prefer-tag-over-role": "warn",
"oxc/const-comparisons": "warn",
"react-hooks/exhaustive-deps": "warn",
"react/no-children-prop": "warn",
"jsx-a11y/no-autofocus": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"react/react-in-jsx-scope": "off"
},
"options": {
"reportUnusedDisableDirectives": "off"
},
"ignorePatterns": ["**/*.config.js", "**/*.config.mjs", "**/*.cjs", "**/lang/*.d.json.ts", ".next/**"],
"settings": {
"react": { "version": "19" }
}
}
+1 -2
View File
@@ -31,6 +31,5 @@ scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n|
# refactorTemplates:
# - i18n.get("$1")
# If set to true, only enables this custom framework (will disable all built-in frameworks)
monopoly: true
monopoly: true
+5 -15
View File
@@ -15,9 +15,7 @@
"request": "launch",
"command": "pnpm docker:dev",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "lint fix",
@@ -25,9 +23,7 @@
"request": "launch",
"command": "pnpm lint:fix",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "format fix",
@@ -35,9 +31,7 @@
"request": "launch",
"command": "pnpm format:fix",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "db push",
@@ -69,9 +63,7 @@
"request": "launch",
"command": "pnpm test",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "test ui",
@@ -79,9 +71,7 @@
"request": "launch",
"command": "pnpm test:ui",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
}
]
}
+3 -14
View File
@@ -1,13 +1,6 @@
{
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"eslint.experimental.useFlatConfig": true,
"typescript.tsdk": "node_modules\\typescript\\lib",
"js/ts.implicitProjectConfig.experimentalDecorators": true,
"prettier.configPath": "./tooling/prettier/index.mjs",
"cSpell.words": [
"ajnart",
"cqmin",
@@ -34,11 +27,7 @@
],
"i18n-ally.dirStructure": "auto",
"i18n-ally.displayLanguage": "en",
"i18n-ally.enabledFrameworks": [
"custom"
],
"i18n-ally.localesPaths": [
"packages/translation/src/lang",
],
"i18n-ally.keystyle": "nested",
"i18n-ally.enabledFrameworks": ["custom"],
"i18n-ally.localesPaths": ["packages/translation/src/lang"],
"i18n-ally.keystyle": "nested"
}
+6
View File
@@ -1,3 +1,9 @@
## [1.61.1](https://github.com/homarr-labs/homarr/compare/v1.61.0...v1.61.1) (2026-05-18)
### Bug Fixes
* **downloads-widget:** increase name column default width ratio ([e9142c0](https://github.com/homarr-labs/homarr/commit/e9142c0dbcaca6cd9215b5292a2d139d96919b51))
## [1.61.1-beta.1](https://github.com/homarr-labs/homarr/compare/v1.61.0...v1.61.1-beta.1) (2026-05-08)
### Performance Improvements
-30
View File
@@ -1,30 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
import nextjsConfig from "@homarr/eslint-config/nextjs";
import reactConfig from "@homarr/eslint-config/react";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [".next/**"],
},
...baseConfig,
...reactConfig,
...nextjsConfig,
{
files: ["public/sw.js"],
languageOptions: {
globals: {
self: "readonly",
clients: "readonly",
skipWaiting: "readonly",
importScripts: "readonly",
caches: "readonly",
fetch: "readonly",
},
},
rules: {
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
},
},
];
+2 -7
View File
@@ -7,13 +7,12 @@
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@dnd-kit/core": "catalog:",
"@dnd-kit/modifiers": "catalog:",
@@ -92,8 +91,6 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "catalog:",
"@types/node": "catalog:",
@@ -102,9 +99,7 @@
"@types/react-dom": "catalog:",
"@types/swagger-ui-react": "catalog:",
"concurrently": "catalog:",
"eslint": "catalog:",
"node-loader": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
}
}
@@ -1,70 +1,27 @@
"use client";
import { useCallback, useRef } from "react";
import { Box, LoadingOverlay, Stack } from "@mantine/core";
import { Box, Stack } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section";
import { BoardBackgroundVideo } from "~/components/layout/background";
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
import { useIsBoardReady } from "./_ready-context";
let boardName: string | null = null;
export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
const updateBoard = useCallback(
(updaterWithoutUndefined: UpdateCallback) => {
if (!boardName) {
throw new Error("Board name is not set");
}
utils.board.getBoardByName.setData({ name: boardName }, (previous) =>
previous ? updaterWithoutUndefined(previous) : previous,
);
},
[utils],
);
return {
updateBoard,
};
};
export const ClientBoard = () => {
const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout();
const isReady = useIsBoardReady();
const fullWidthSortedSections = board.sections
.filter((section) => section.kind === "empty" || section.kind === "category")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
const ref = useRef<HTMLDivElement>(null);
return (
<Box h="100%" pos="relative">
<BoardBackgroundVideo />
<LoadingOverlay
visible={!isReady}
transitionProps={{ duration: 500 }}
loaderProps={{ size: "lg" }}
h={fullHeightWithoutHeaderAndFooter}
/>
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
<Stack h="100%">
{fullWidthSortedSections.map((section) =>
section.kind === "empty" ? (
// Unique keys per layout to always reinitialize the gridstack
<BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} />
) : (
<BoardCategorySection key={`${currentLayoutId}-${section.id}`} section={section} />
@@ -41,13 +41,11 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
}),
// eslint-disable-next-line no-restricted-syntax
page: async ({ params }: { params: Promise<TParams> }) => {
const session = await auth();
const integrations = await getIntegrationsWithPermissionsAsync(session);
const board = await getInitialBoard(await params);
const resolvedParams = await params;
const queryClient = getQueryClient();
// Prefetch item data
const [board, session] = await Promise.all([getInitialBoard(resolvedParams), auth()]);
const itemsMap = board.items.reduce((acc, item) => {
const existing = acc.get(item.kind);
if (existing) {
@@ -58,17 +56,20 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
return acc;
}, new Map<WidgetKind, Item[]>());
for (const [kind, items] of itemsMap) {
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
logger.error(
new ErrorWithMetadata(
"Failed to prefetch widget",
{ widgetKind: kind, itemCount: items.length },
{ cause: error },
),
);
});
}
const [integrations] = await Promise.all([
getIntegrationsWithPermissionsAsync(session),
...Array.from(itemsMap).map(([kind, items]) =>
prefetchForKindAsync(kind, queryClient, items).catch((error) => {
logger.error(
new ErrorWithMetadata(
"Failed to prefetch widget",
{ widgetKind: kind, itemCount: items.length },
{ cause: error },
),
);
}),
),
]);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
@@ -1,42 +1,19 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { createContext, useCallback, useContext } from "react";
const BoardReadyContext = createContext<{
isReady: boolean;
markAsReady: (id: string) => void;
} | null>(null);
export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
const pathname = usePathname();
const utils = clientApi.useUtils();
const board = useRequiredBoard();
const [readySections, setReadySections] = useState<string[]>([]);
// Reset sections required for ready state
useEffect(() => {
return () => {
setReadySections([]);
};
}, [pathname, utils]);
useEffect(() => {
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
}, [board.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
}, []);
// eslint-disable-next-line @typescript-eslint/no-empty-function
const markAsReady = useCallback((_id: string) => {}, []);
return (
<BoardReadyContext.Provider
value={{
isReady: board.sections.length === readySections.length,
markAsReady,
}}
>
@@ -54,13 +31,3 @@ export const useMarkSectionAsReady = () => {
return context.markAsReady;
};
export const useIsBoardReady = () => {
const context = useContext(BoardReadyContext);
if (!context) {
throw new Error("BoardReadyProvider is required");
}
return context.isReady;
};
@@ -8,12 +8,14 @@ import { getFirstEmptyPosition } from "./empty-position";
import { getSectionElements } from "./section-elements";
export interface CreateItemInput {
id?: string;
kind: WidgetKind;
options?: Record<string, unknown>;
integrationIds?: string[];
}
export const createItemCallback =
({ kind, options = {} }: CreateItemInput) =>
({ id, kind, options = {}, integrationIds = [] }: CreateItemInput) =>
(previous: Board): Board => {
const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty")
@@ -23,11 +25,11 @@ export const createItemCallback =
if (!firstSection) return previous;
const widget = {
id: createId(),
id: id ?? createId(),
kind,
options,
layouts: createItemLayouts(previous, firstSection),
integrationIds: [],
integrationIds,
advancedOptions: {
title: null,
customCssClasses: [],
@@ -2,20 +2,26 @@ import { useMemo, useState } from "react";
import { Avatar, Button, Card, Center, Grid, Group, Input, Stack, Text, Tooltip } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import { objectEntries } from "@homarr/common";
import { clientApi } from "@homarr/api/client";
import { createId, objectEntries } from "@homarr/common";
import { getIconUrl, getIntegrationName } from "@homarr/definitions";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import { createModal } from "@homarr/modals";
import { createModal, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings";
import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
import { widgetImports } from "@homarr/widgets";
import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import { WidgetEditModal } from "@homarr/widgets/modals";
import { useItemActions } from "./item-actions";
export const ItemSelectModal = createModal<void>(({ actions }) => {
const [search, setSearch] = useState("");
const t = useI18n();
const { createItem } = useItemActions();
const { createItem, updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations } = useItemActions();
const { openModal: openEditModal } = useModalAction(WidgetEditModal);
const { data: integrationData } = clientApi.integration.all.useQuery();
const settings = useSettings();
const items = useMemo(
() =>
@@ -40,8 +46,43 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
);
const handleAdd = (kind: WidgetKind) => {
createItem({ kind });
const definition = widgetImports[kind].definition;
const hasIntegrationSupport = "supportedIntegrations" in definition;
const matchingIntegrations = hasIntegrationSupport
? (integrationData ?? []).filter((integration) =>
(definition.supportedIntegrations as string[]).includes(integration.kind),
)
: [];
const integrationIds = matchingIntegrations.map((i) => i.id);
const itemId = createId();
const defaultOptions = reduceWidgetOptionsWithDefaultValues(kind, settings);
createItem({ id: itemId, kind, integrationIds });
actions.closeModal();
openEditModal(
{
kind,
value: {
advancedOptions: { title: null, customCssClasses: [], borderColor: "" },
options: defaultOptions,
integrationIds,
},
onSuccessfulEdit: ({ options, integrationIds: newIntegrationIds, advancedOptions }) => {
updateItemOptions({ itemId, newOptions: options });
updateItemAdvancedOptions({ itemId, newAdvancedOptions: advancedOptions });
updateItemIntegrations({ itemId, newIntegrations: newIntegrationIds });
},
integrationData: matchingIntegrations,
integrationSupport: hasIntegrationSupport,
settings,
},
{
title: (titleT) => `${titleT("item.edit.title")} - ${titleT(`widget.${kind}.name`)}`,
},
);
};
return (
@@ -3,6 +3,7 @@ import { useDisclosure } from "@mantine/hooks";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { CategorySection } from "~/app/[locale]/boards/_types";
@@ -10,19 +11,36 @@ import { CategoryMenu } from "./category/category-menu";
import { GridStack } from "./gridstack/gridstack";
import classes from "./item.module.css";
const localStorageKey = (sectionId: string) => `homarr-section-collapsed-${sectionId}`;
interface Props {
section: CategorySection;
}
export const BoardCategorySection = ({ section }: Props) => {
const { status } = useSession();
const { mutate } = clientApi.section.changeCollapsed.useMutation();
const board = useRequiredBoard();
const [opened, { toggle }] = useDisclosure(section.collapsed, {
const initialCollapsed =
typeof window !== "undefined"
? (localStorage.getItem(localStorageKey(section.id)) ?? String(section.collapsed)) === "true"
: section.collapsed;
const [opened, { toggle }] = useDisclosure(initialCollapsed, {
onOpen() {
mutate({ sectionId: section.id, collapsed: true });
if (status === "authenticated") {
mutate({ sectionId: section.id, collapsed: true });
} else if (status === "unauthenticated") {
localStorage.setItem(localStorageKey(section.id), "true");
}
},
onClose() {
mutate({ sectionId: section.id, collapsed: false });
if (status === "authenticated") {
mutate({ sectionId: section.id, collapsed: false });
} else if (status === "unauthenticated") {
localStorage.setItem(localStorageKey(section.id), "false");
}
},
});
@@ -44,6 +44,7 @@ export const BoardBackgroundVideo = () => {
muted
loop
playsInline
aria-hidden="true"
style={{
position: "fixed",
width: "100vw",
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: ["tasks.cjs"],
},
...baseConfig,
];
+4 -9
View File
@@ -4,20 +4,19 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./src/main.ts",
"types": "./src/main.ts",
"exports": {
".": "./src/main.ts"
},
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
@@ -43,14 +42,10 @@
"undici": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "catalog:",
"dotenv-cli": "catalog:",
"esbuild": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:"
}
-108
View File
@@ -1,108 +0,0 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { handshakeAsync } from "@homarr/redis";
import { waitForRedisAsync } from "./main";
const { mockLogger } = vi.hoisted(() => ({
mockLogger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
vi.mock("./overrides", () => ({}));
vi.mock("@homarr/core/infrastructure/logs", () => ({
createLogger: () => mockLogger,
}));
vi.mock("@homarr/redis", () => ({
handshakeAsync: vi.fn(),
}));
vi.mock("@homarr/cron-jobs", () => ({
jobGroup: {
initializeAsync: vi.fn().mockResolvedValue(undefined),
startAllAsync: vi.fn().mockResolvedValue(undefined),
},
}));
vi.mock("./on-start", () => ({
onStartAsync: vi.fn().mockResolvedValue(undefined),
}));
describe("waitForRedisAsync", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
test("succeeds on first attempt", async () => {
vi.mocked(handshakeAsync).mockResolvedValueOnce(undefined);
await waitForRedisAsync();
expect(handshakeAsync).toHaveBeenCalledOnce();
expect(mockLogger.info).toHaveBeenCalledWith("Redis connection established");
});
test("retries on failure then succeeds", async () => {
vi.mocked(handshakeAsync)
.mockRejectedValueOnce(new Error("ECONNREFUSED"))
.mockRejectedValueOnce(new Error("ECONNREFUSED"))
.mockResolvedValueOnce(undefined);
const promise = waitForRedisAsync();
await vi.advanceTimersByTimeAsync(500);
await vi.advanceTimersByTimeAsync(500);
await promise;
expect(handshakeAsync).toHaveBeenCalledTimes(3);
expect(mockLogger.warn).toHaveBeenCalledTimes(2);
expect(mockLogger.info).toHaveBeenCalledWith("Redis connection established");
});
test("logs error and proceeds after max retries exhausted", async () => {
vi.mocked(handshakeAsync).mockRejectedValue(new Error("ECONNREFUSED"));
const promise = waitForRedisAsync();
for (let index = 0; index < 10; index++) {
await vi.advanceTimersByTimeAsync(500);
}
await promise;
expect(handshakeAsync).toHaveBeenCalledTimes(10);
expect(mockLogger.error).toHaveBeenCalledWith("Redis did not become ready after retries, proceeding anyway");
expect(mockLogger.info).not.toHaveBeenCalled();
});
test("warns with attempt count on each retry", async () => {
vi.mocked(handshakeAsync).mockRejectedValueOnce(new Error("fail")).mockResolvedValueOnce(undefined);
const promise = waitForRedisAsync();
await vi.advanceTimersByTimeAsync(500);
await promise;
expect(mockLogger.warn).toHaveBeenCalledWith("Redis not ready, retrying (1/10)...");
});
test("does not retry after success", async () => {
vi.mocked(handshakeAsync).mockResolvedValueOnce(undefined);
await waitForRedisAsync();
expect(handshakeAsync).toHaveBeenCalledOnce();
expect(mockLogger.warn).not.toHaveBeenCalled();
expect(mockLogger.error).not.toHaveBeenCalled();
});
});
-22
View File
@@ -1,33 +1,11 @@
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
import "./overrides";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { jobGroup } from "@homarr/cron-jobs";
import { handshakeAsync } from "@homarr/redis";
import { onStartAsync } from "./on-start";
const logger = createLogger({ module: "tasks" });
const MAX_RETRIES = 10;
const RETRY_DELAY_MS = 500;
export const waitForRedisAsync = async () => {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
await handshakeAsync();
logger.info("Redis connection established");
return;
} catch {
logger.warn(`Redis not ready, retrying (${attempt}/${MAX_RETRIES})...`);
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
}
}
logger.error("Redis did not become ready after retries, proceeding anyway");
};
void (async () => {
await waitForRedisAsync();
await onStartAsync();
await jobGroup.initializeAsync();
await jobGroup.startAllAsync();
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: ["wssServer.cjs"],
},
...baseConfig,
];
+4 -9
View File
@@ -4,20 +4,19 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./src/main.ts",
"types": "./src/main.ts",
"exports": {
".": "./src/main.ts"
},
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
@@ -32,13 +31,9 @@
"ws": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "catalog:",
"esbuild": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
}
}
+1 -1
View File
@@ -4,11 +4,11 @@
"dependencies": {
"better-sqlite3": "^12.10.0"
},
"packageManager": "pnpm@10.33.4",
"engines": {
"node": ">=24.15.0",
"pnpm": ">=10.33.4"
},
"packageManager": "pnpm@10.33.4",
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3"
+1 -1
View File
@@ -51,4 +51,4 @@ services:
volumes:
mysql_data:
postgresql_data:
postgresql_data:
+10 -12
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.61.1-beta.1",
"version": "1.61.1",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -17,12 +17,11 @@
"db:studio": "pnpm -F db studio",
"dev": "pnpm -F nextjs dev",
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
"lint": "turbo lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache",
"lint:fix": "turbo lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache",
"format": "turbo format --continue",
"format:fix": "turbo format --continue -- --write",
"lint": "turbo lint --continue",
"lint:fix": "turbo lint --continue -- --fix",
"lint:ws": "pnpm dlx sherif@latest",
"package:new": "turbo gen init",
"release": "semantic-release",
"scripts:update-bug-report-template": "tsx ./scripts/update-bug-report-template.mts",
"scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts",
@@ -33,9 +32,7 @@
"typecheck": "turbo typecheck",
"with-env": "dotenv -e .env --"
},
"prettier": "@homarr/prettier-config",
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",
"@playwright/test": "catalog:",
"@semantic-release/changelog": "catalog:",
"@semantic-release/commit-analyzer": "catalog:",
@@ -52,7 +49,8 @@
"cross-env": "catalog:",
"jsdom": "catalog:",
"json5": "catalog:",
"prettier": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"semantic-release": "catalog:",
"testcontainers": "catalog:",
"turbo": "catalog:",
@@ -60,11 +58,11 @@
"vite-tsconfig-paths": "catalog:",
"vitest": "catalog:"
},
"packageManager": "pnpm@10.33.4",
"engines": {
"node": ">=24.15.0",
"pnpm": ">=10.33.4"
},
"packageManager": "pnpm@10.33.4",
"pnpm": {
"onlyBuiltDependencies": [
"@tree-sitter-grammars/tree-sitter-yaml",
@@ -85,7 +83,7 @@
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6",
"esbuild@<=0.24.2": ">=0.28.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.12.19",
"hono@<4.6.5": ">=4.12.21",
"linkifyjs@<4.3.2": ">=4.3.3",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
"prismjs@<1.30.0": ">=1.30.0",
@@ -96,7 +94,7 @@
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
"tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=8.0.13"
"vite@>=5.0.0 <=5.4.18": ">=8.0.14"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
@@ -32,10 +31,7 @@
"superjson": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+4 -9
View File
@@ -4,6 +4,8 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./client": "./src/client.ts",
@@ -12,15 +14,12 @@
"./shared": "./src/shared.ts",
"./open-api": "./src/open-api.ts"
},
"main": "./index.ts",
"types": "./index.ts",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
@@ -55,11 +54,7 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+4 -9
View File
@@ -4,6 +4,8 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./index.ts",
"types": "./index.ts",
"exports": {
".": "./index.ts",
"./next": "./next.ts",
@@ -14,15 +16,12 @@
"./shared": "./shared.ts",
"./env": "./env.ts"
},
"main": "./index.ts",
"types": "./index.ts",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "catalog:",
"@auth/drizzle-adapter": "catalog:",
@@ -41,13 +40,9 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "catalog:",
"@types/cookies": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
}
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+7 -11
View File
@@ -4,11 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
"./context": "./src/context.tsx",
"./updater": "./src/updater.ts",
"./edit-mode": "./src/edit-mode.tsx"
},
"typesVersions": {
"*": {
"*": [
@@ -16,23 +11,24 @@
]
}
},
"exports": {
"./context": "./src/context.tsx",
"./updater": "./src/updater.ts",
"./edit-mode": "./src/edit-mode.tsx"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,16 +11,18 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --outfile=cli.cjs --external:bcrypt --external:cpu-features --loader:.html=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"run": "pnpm with-env node cli.cjs",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e .env --"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@drizzle-team/brocli": "catalog:",
"@homarr/auth": "workspace:^0.1.0",
@@ -33,11 +32,8 @@
"dotenv": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+9 -13
View File
@@ -4,13 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./types": "./src/types.ts",
"./server": "./src/server.ts",
"./client": "./src/client.ts",
"./env": "./env.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -18,13 +11,19 @@
]
}
},
"exports": {
".": "./index.ts",
"./types": "./src/types.ts",
"./server": "./src/server.ts",
"./client": "./src/client.ts",
"./env": "./env.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/core": "workspace:^0.1.0",
"@paralleldrive/cuid2": "catalog:",
@@ -39,10 +38,7 @@
"zod-validation-error": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+9 -13
View File
@@ -4,6 +4,13 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"exports": {
"./infrastructure/redis": "./src/infrastructure/redis/client.ts",
"./infrastructure/env": "./src/infrastructure/env/index.ts",
@@ -22,20 +29,12 @@
"./infrastructure/http": "./src/infrastructure/http/index.ts",
"./infrastructure/http/timeout": "./src/infrastructure/http/timeout.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "catalog:",
"better-sqlite3": "catalog:",
@@ -49,12 +48,9 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "catalog:",
"@types/pg": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
@@ -5,7 +5,6 @@ import { redisEnv } from "./env";
const defaultRedisOptions = {
connectionName: "homarr",
lazyConnect: true,
} satisfies RedisOptions;
export type { Redis as RedisClient } from "ioredis";
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+6 -10
View File
@@ -4,10 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./publisher": "./src/publisher.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -15,21 +11,21 @@
]
}
},
"exports": {
".": "./index.ts",
"./publisher": "./src/publisher.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/redis": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+7 -11
View File
@@ -4,11 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./expressions": "./src/expressions.ts",
"./logger": "./src/logger.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -16,13 +11,17 @@
]
}
},
"exports": {
".": "./index.ts",
"./expressions": "./src/expressions.ts",
"./logger": "./src/logger.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^",
@@ -30,10 +29,7 @@
"croner": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+6 -10
View File
@@ -4,10 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./job-manager": "./src/job-manager.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -15,13 +11,16 @@
]
}
},
"exports": {
".": "./index.ts",
"./job-manager": "./src/job-manager.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/analytics": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
@@ -43,10 +42,7 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+4 -9
View File
@@ -4,6 +4,8 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./index.ts",
"types": "./index.ts",
"exports": {
".": "./index.ts",
"./collection": "./collection.ts",
@@ -12,16 +14,14 @@
"./queries": "./queries/index.ts",
"./validationSchemas": "./validationSchemas.ts"
},
"main": "./index.ts",
"types": "./index.ts",
"scripts": {
"build": "pnpm run build:sqlite && pnpm run build:mysql && pnpm run build:postgresql",
"build:mysql": "esbuild migrations/mysql/migrate.ts --bundle --platform=node --outfile=migrations/mysql/migrate.cjs",
"build:postgresql": "esbuild migrations/postgresql/migrate.ts --bundle --platform=node --outfile=migrations/postgresql/migrate.cjs",
"build:sqlite": "esbuild migrations/sqlite/migrate.ts --bundle --platform=node --outfile=migrations/sqlite/migrate.cjs",
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"migration:custom": "pnpm with-env tsx ./migrations/custom/run-custom.ts",
"migration:mysql:drop": "pnpm with-env drizzle-kit drop --config ./configs/mysql.config.ts",
"migration:mysql:generate": "pnpm with-env drizzle-kit generate --config ./configs/mysql.config.ts",
@@ -42,7 +42,6 @@
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "catalog:",
"@homarr/common": "workspace:^0.1.0",
@@ -63,15 +62,11 @@
"superjson": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "catalog:",
"@types/pg": "catalog:",
"dotenv-cli": "catalog:",
"esbuild": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:"
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,24 +11,23 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"format": "oxfmt --check .",
"postinstall": "tsx ./src/docs/codegen.ts",
"lint": "eslint",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:"
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+7 -11
View File
@@ -4,11 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./shared": "./src/shared.ts",
"./env": "./src/env.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -16,24 +11,25 @@
]
}
},
"exports": {
".": "./index.ts",
"./shared": "./src/shared.ts",
"./env": "./src/env.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"dockerode": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+6 -10
View File
@@ -4,10 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./types": "./src/types.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -15,13 +11,16 @@
]
}
},
"exports": {
".": "./index.ts",
"./types": "./src/types.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
@@ -31,10 +30,7 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
@@ -35,10 +34,7 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+6 -10
View File
@@ -4,10 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./local": "./src/local.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -15,23 +11,23 @@
]
}
},
"exports": {
".": "./index.ts",
"./local": "./src/local.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./src/index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
".": "./src/index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
@@ -28,11 +27,8 @@
"bcrypt": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+8 -12
View File
@@ -4,12 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./test-connection": "./src/base/test-connection/index.ts",
"./client": "./src/client.ts",
"./types": "./src/types.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -17,13 +11,18 @@
]
}
},
"exports": {
".": "./index.ts",
"./test-connection": "./src/base/test-connection/index.ts",
"./client": "./src/client.ts",
"./types": "./src/types.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ctrl/deluge": "catalog:",
"@ctrl/qbittorrent": "catalog:",
@@ -52,12 +51,9 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "catalog:",
"@types/xml2js": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
@@ -42,10 +41,7 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-5
View File
@@ -1,5 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
import reactConfig from "@homarr/eslint-config/react";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig, ...reactConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
@@ -29,10 +28,7 @@
"react": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+6 -10
View File
@@ -4,10 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"typesVersions": {
"*": {
"*": [
@@ -15,23 +11,23 @@
]
}
},
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "catalog:",
"@tabler/icons-react": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+9 -13
View File
@@ -4,13 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./analyse": "./src/analyse/index.ts",
"./components": "./src/components/index.ts",
"./import": "./src/import/index.ts",
"./shared": "./src/shared.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -18,13 +11,19 @@
]
}
},
"exports": {
".": "./index.ts",
"./analyse": "./src/analyse/index.ts",
"./components": "./src/components/index.ts",
"./import": "./src/import/index.ts",
"./shared": "./src/shared.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
@@ -48,11 +47,8 @@
"zod-form-data": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,22 +11,21 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,22 +11,21 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^",
"@homarr/core": "workspace:^",
@@ -30,10 +29,7 @@
"superjson": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
@@ -1,102 +0,0 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { ChannelSubscriptionTracker } from "./channel-subscription-tracker";
import { createRedisConnection } from "./connection";
const mockRedisClient = {
subscribe: vi.fn().mockResolvedValue("OK"),
unsubscribe: vi.fn().mockResolvedValue("OK"),
on: vi.fn(),
};
vi.mock("@homarr/core/infrastructure/logs", () => ({
createLogger: () => ({ debug: vi.fn(), warn: vi.fn() }),
}));
vi.mock("./connection", () => ({
createRedisConnection: vi.fn(() => mockRedisClient),
}));
describe("ChannelSubscriptionTracker", () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset static state between tests
// @ts-expect-error accessing private static for test reset
ChannelSubscriptionTracker.subscriptions = new Map();
// @ts-expect-error accessing private static for test reset
ChannelSubscriptionTracker._redis = undefined;
// @ts-expect-error accessing private static for test reset
ChannelSubscriptionTracker.listenerActive = false;
});
test("redis client is lazily created on first subscribe", () => {
vi.mocked(createRedisConnection).mockClear();
ChannelSubscriptionTracker.subscribe("channel-1", vi.fn());
expect(createRedisConnection).toHaveBeenCalledOnce();
});
test("redis client is reused across multiple subscribes", () => {
vi.mocked(createRedisConnection).mockClear();
ChannelSubscriptionTracker.subscribe("channel-1", vi.fn());
ChannelSubscriptionTracker.subscribe("channel-2", vi.fn());
expect(createRedisConnection).toHaveBeenCalledOnce();
});
test("subscribes to redis channel on first callback for a channel", () => {
ChannelSubscriptionTracker.subscribe("test-channel", vi.fn());
expect(mockRedisClient.subscribe).toHaveBeenCalledWith("test-channel");
});
test("does not re-subscribe when adding second callback to same channel", () => {
ChannelSubscriptionTracker.subscribe("test-channel", vi.fn());
mockRedisClient.subscribe.mockClear();
ChannelSubscriptionTracker.subscribe("test-channel", vi.fn());
expect(mockRedisClient.subscribe).not.toHaveBeenCalled();
});
test("unsubscribe function removes the callback", () => {
const unsubscribe = ChannelSubscriptionTracker.subscribe("test-channel", vi.fn());
unsubscribe();
expect(mockRedisClient.unsubscribe).toHaveBeenCalledWith("test-channel");
});
test("does not unsubscribe from redis if other callbacks remain", () => {
ChannelSubscriptionTracker.subscribe("test-channel", vi.fn());
const unsubscribe = ChannelSubscriptionTracker.subscribe("test-channel", vi.fn());
unsubscribe();
expect(mockRedisClient.unsubscribe).not.toHaveBeenCalled();
});
test("activates message listener only once", () => {
ChannelSubscriptionTracker.subscribe("ch-1", vi.fn());
ChannelSubscriptionTracker.subscribe("ch-2", vi.fn());
expect(mockRedisClient.on).toHaveBeenCalledTimes(1);
expect(mockRedisClient.on).toHaveBeenCalledWith("message", expect.any(Function));
});
test("message listener dispatches to correct channel callbacks", () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
ChannelSubscriptionTracker.subscribe("channel-a", callback1);
ChannelSubscriptionTracker.subscribe("channel-b", callback2);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const messageHandler = mockRedisClient.on.mock.calls[0]![1] as (channel: string, message: string) => void;
messageHandler("channel-a", "test-message");
expect(callback1).toHaveBeenCalledWith("test-message");
expect(callback2).not.toHaveBeenCalled();
});
});
@@ -2,7 +2,6 @@ import { randomUUID } from "crypto";
import type { MaybePromise } from "@homarr/common/types";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import { createRedisConnection } from "./connection";
@@ -19,10 +18,7 @@ type SubscriptionCallback = (message: string) => MaybePromise<void>;
*/
export class ChannelSubscriptionTracker {
private static subscriptions = new Map<string, Map<string, SubscriptionCallback>>();
private static _redis: RedisClient | undefined;
private static get redis() {
return (this._redis ??= createRedisConnection());
}
private static redis = createRedisConnection();
private static listenerActive = false;
/**
-147
View File
@@ -1,147 +0,0 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import {
createCacheChannel,
createGetSetChannel,
createListChannel,
createSubPubChannel,
handshakeAsync,
} from "./channel";
import { createRedisConnection } from "./connection";
const mockRedisClient = {
get: vi.fn().mockResolvedValue(null),
set: vi.fn().mockResolvedValue("OK"),
del: vi.fn().mockResolvedValue(1),
publish: vi.fn().mockResolvedValue(1),
lrange: vi.fn().mockResolvedValue([]),
lrem: vi.fn().mockResolvedValue(0),
lpush: vi.fn().mockResolvedValue(1),
ltrim: vi.fn().mockResolvedValue("OK"),
llen: vi.fn().mockResolvedValue(0),
hello: vi.fn().mockResolvedValue({}),
subscribe: vi.fn().mockResolvedValue("OK"),
on: vi.fn(),
};
vi.mock("@homarr/core/infrastructure/logs", () => ({
createLogger: () => ({ debug: vi.fn(), warn: vi.fn(), error: vi.fn(), info: vi.fn() }),
}));
vi.mock("./connection", () => ({
createRedisConnection: vi.fn(() => mockRedisClient),
}));
vi.mock("./channel-subscription-tracker", () => ({
ChannelSubscriptionTracker: {
subscribe: vi.fn(() => vi.fn()),
},
}));
describe("channel lazy initialization", () => {
beforeEach(() => {
vi.mocked(createRedisConnection).mockClear();
});
test("getSetClient is lazily created on first channel operation", async () => {
vi.mocked(createRedisConnection).mockClear();
const channel = createGetSetChannel<string>("test-lazy");
expect(createRedisConnection).not.toHaveBeenCalled();
await channel.getAsync();
expect(createRedisConnection).toHaveBeenCalled();
});
test("publisher is lazily created on first publish", async () => {
const channel = createSubPubChannel<string>("test-pub", { persist: false });
const callsBefore = vi.mocked(createRedisConnection).mock.calls.length;
await channel.publishAsync("hello");
expect(vi.mocked(createRedisConnection).mock.calls.length).toBeGreaterThan(callsBefore);
});
test("handshakeAsync creates a client and calls hello", async () => {
await handshakeAsync();
expect(mockRedisClient.hello).toHaveBeenCalledOnce();
});
});
describe("createSubPubChannel", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("publishAsync persists data and publishes", async () => {
const channel = createSubPubChannel<{ value: number }>("test-channel");
await channel.publishAsync({ value: 42 });
expect(mockRedisClient.set).toHaveBeenCalledOnce();
expect(mockRedisClient.publish).toHaveBeenCalledOnce();
});
test("publishAsync with persist:false skips set", async () => {
const channel = createSubPubChannel<string>("no-persist", { persist: false });
await channel.publishAsync("data");
expect(mockRedisClient.set).not.toHaveBeenCalled();
expect(mockRedisClient.publish).toHaveBeenCalledOnce();
});
test("getLastDataAsync returns null when no data", async () => {
const channel = createSubPubChannel<string>("empty");
const result = await channel.getLastDataAsync();
expect(result).toBeNull();
});
});
describe("createListChannel", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("addAsync pushes to list", async () => {
const channel = createListChannel<string>("test-list");
await channel.addAsync("item1");
expect(mockRedisClient.lpush).toHaveBeenCalledOnce();
});
test("clearAsync deletes the list", async () => {
const channel = createListChannel<string>("test-list");
await channel.clearAsync();
expect(mockRedisClient.del).toHaveBeenCalledOnce();
});
});
describe("createCacheChannel", () => {
beforeEach(() => {
vi.clearAllMocks();
});
test("invalidateAsync deletes the cache key", async () => {
const channel = createCacheChannel<string>("test-cache");
await channel.invalidateAsync();
expect(mockRedisClient.del).toHaveBeenCalledWith("cache:test-cache");
});
test("getAsync returns null when no cached data", async () => {
const channel = createCacheChannel<string>("test-cache");
const result = await channel.getAsync();
expect(result).toBeNull();
});
});
+54 -58
View File
@@ -2,7 +2,6 @@ import superjson from "superjson";
import { createId, hashObjectBase64 } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import type { WidgetKind } from "@homarr/definitions";
import { ChannelSubscriptionTracker } from "./channel-subscription-tracker";
@@ -10,11 +9,8 @@ import { createRedisConnection } from "./connection";
const logger = createLogger({ module: "redisChannel" });
let _publisher: RedisClient | undefined;
const getPublisher = () => (_publisher ??= createRedisConnection());
let _lastDataClient: RedisClient | undefined;
const getLastDataClient = () => (_lastDataClient ??= createRedisConnection());
const publisher = createRedisConnection();
const lastDataClient = createRedisConnection();
/**
* Creates a new pub/sub channel.
@@ -31,33 +27,34 @@ export const createSubPubChannel = <TData>(name: string, { persist }: { persist:
*/
subscribe: (callback: (data: TData) => void) => {
if (persist) {
void getLastDataClient()
.get(lastChannelName)
.then((data) => {
if (data) {
callback(superjson.parse(data));
}
});
void lastDataClient.get(lastChannelName).then((data) => {
if (data) {
callback(superjson.parse(data));
}
});
}
return ChannelSubscriptionTracker.subscribe(channelName, (message) => {
callback(superjson.parse(message));
});
},
/**
* Publish data to the channel with last data saved.
* @param data data to be published
*/
publishAsync: async (data: TData) => {
if (persist) {
await getLastDataClient().set(lastChannelName, superjson.stringify(data));
await lastDataClient.set(lastChannelName, superjson.stringify(data));
}
await getPublisher().publish(channelName, superjson.stringify(data));
await publisher.publish(channelName, superjson.stringify(data));
},
getLastDataAsync: async () => {
const data = await getLastDataClient().get(lastChannelName);
const data = await lastDataClient.get(lastChannelName);
return data ? superjson.parse<TData>(data) : null;
},
};
};
let _getSetClient: RedisClient | undefined;
const getSetClient = () => (_getSetClient ??= createRedisConnection());
const getSetClient = createRedisConnection();
/**
* Creates a new redis channel for a list
@@ -72,7 +69,7 @@ export const createListChannel = <TItem>(name: string) => {
* @returns an array of all items
*/
getAllAsync: async () => {
const items = await getSetClient().lrange(listChannelName, 0, -1);
const items = await getSetClient.lrange(listChannelName, 0, -1);
return items.map((item) => superjson.parse<TItem>(item));
},
/**
@@ -80,20 +77,20 @@ export const createListChannel = <TItem>(name: string) => {
* @param item item to remove
*/
removeAsync: async (item: TItem) => {
await getSetClient().lrem(listChannelName, 0, superjson.stringify(item));
await getSetClient.lrem(listChannelName, 0, superjson.stringify(item));
},
/**
* Clear all items from the channels list
*/
clearAsync: async () => {
await getSetClient().del(listChannelName);
await getSetClient.del(listChannelName);
},
/**
* Add an item to the channels list
* @param item item to add
*/
addAsync: async (item: TItem) => {
await getSetClient().lpush(listChannelName, superjson.stringify(item));
await getSetClient.lpush(listChannelName, superjson.stringify(item));
},
};
};
@@ -109,7 +106,7 @@ export const createGetSetChannel = <TData>(name: string) => {
* @returns data or null if not found
*/
getAsync: async () => {
const data = await getSetClient().get(name);
const data = await getSetClient.get(name);
return data ? superjson.parse<TData>(data) : null;
},
/**
@@ -117,13 +114,13 @@ export const createGetSetChannel = <TData>(name: string) => {
* @param data data to be stored in the channel
*/
setAsync: async (data: TData) => {
await getSetClient().set(name, superjson.stringify(data));
await getSetClient.set(name, superjson.stringify(data));
},
/**
* Remove data from the channel
*/
removeAsync: async () => {
await getSetClient().del(name);
await getSetClient.del(name);
},
};
};
@@ -143,7 +140,7 @@ export const createCacheChannel = <TData>(name: string, cacheDurationMs: number
* @returns data or null if not found or expired
*/
getAsync: async () => {
const data = await getSetClient().get(cacheChannelName);
const data = await getSetClient.get(cacheChannelName);
if (!data) return null;
const parsedData = superjson.parse<{ data: TData; timestamp: Date }>(data);
@@ -159,13 +156,13 @@ export const createCacheChannel = <TData>(name: string, cacheDurationMs: number
* @returns data or new data if not present or expired
*/
consumeAsync: async (callback: () => Promise<TData>) => {
const data = await getSetClient().get(cacheChannelName);
const data = await getSetClient.get(cacheChannelName);
const getNewDataAsync = async () => {
logger.debug(`Cache miss for channel '${cacheChannelName}'`);
const newData = await callback();
const result = { data: newData, timestamp: new Date() };
await getSetClient().set(cacheChannelName, superjson.stringify(result));
await getSetClient.set(cacheChannelName, superjson.stringify(result));
logger.debug(`Cache updated for channel '${cacheChannelName}'`);
return result;
};
@@ -190,14 +187,14 @@ export const createCacheChannel = <TData>(name: string, cacheDurationMs: number
* Invalidate the cache channels data.
*/
invalidateAsync: async () => {
await getSetClient().del(cacheChannelName);
await getSetClient.del(cacheChannelName);
},
/**
* Set the data in the cache channel.
* @param data data to be stored in the cache channel
*/
setAsync: async (data: TData) => {
await getSetClient().set(cacheChannelName, superjson.stringify({ data, timestamp: new Date() }));
await getSetClient.set(cacheChannelName, superjson.stringify({ data, timestamp: new Date() }));
},
};
};
@@ -239,12 +236,12 @@ export const createChannelEventHistory = <TData>(channelName: string, maxElement
});
},
pushAsync: async (data: TData, options = { publish: false }) => {
if (options.publish) await getPublisher().publish(channelName, superjson.stringify(data));
await getSetClient().lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await getSetClient().ltrim(channelName, 0, maxElements);
if (options.publish) await publisher.publish(channelName, superjson.stringify(data));
await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await getSetClient.ltrim(channelName, 0, maxElements);
},
clearAsync: async () => {
await getSetClient().del(channelName);
await getSetClient.del(channelName);
},
/**
* Returns a slice of the available data in the channel.
@@ -253,17 +250,17 @@ export const createChannelEventHistory = <TData>(channelName: string, maxElement
* @param endIndex End index of the slice, negative values are counted from the end, defaults at end of range.
*/
getSliceAsync: async (startIndex = 0, endIndex = -1) => {
const range = await getSetClient().lrange(channelName, startIndex, endIndex);
const range = await getSetClient.lrange(channelName, startIndex, endIndex);
return range.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item));
},
getSliceUntilTimeAsync: async (time: Date) => {
const itemsInCollection = await getSetClient().lrange(channelName, 0, -1);
const itemsInCollection = await getSetClient.lrange(channelName, 0, -1);
return itemsInCollection
.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item))
.filter((item) => item.timestamp < time);
},
getLengthAsync: async () => {
return await getSetClient().llen(channelName);
return await getSetClient.llen(channelName);
},
name: channelName,
};
@@ -274,11 +271,11 @@ export const createChannelEventHistory = <TData>(channelName: string, maxElement
*/
export const createChannelEventHistoryOld = <TData>(channelName: string, maxElements = 15) => {
const popElementsOverMaxAsync = async () => {
const length = await getSetClient().llen(channelName);
const length = await getSetClient.llen(channelName);
if (length <= maxElements) {
return;
}
await getSetClient().ltrim(channelName, 0, maxElements - 1);
await getSetClient.ltrim(channelName, 0, maxElements - 1);
};
return {
@@ -288,33 +285,33 @@ export const createChannelEventHistoryOld = <TData>(channelName: string, maxElem
});
},
publishAndPushAsync: async (data: TData) => {
await getPublisher().publish(channelName, superjson.stringify(data));
await getSetClient().lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await publisher.publish(channelName, superjson.stringify(data));
await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await popElementsOverMaxAsync();
},
pushAsync: async (data: TData) => {
await getSetClient().lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await popElementsOverMaxAsync();
},
clearAsync: async () => {
await getSetClient().del(channelName);
await getSetClient.del(channelName);
},
getLastAsync: async () => {
const length = await getSetClient().llen(channelName);
const data = await getSetClient().lrange(channelName, length - 1, length);
const length = await getSetClient.llen(channelName);
const data = await getSetClient.lrange(channelName, length - 1, length);
if (data.length !== 1) return null;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return superjson.parse<{ data: TData; timestamp: Date }>(data[0]!);
},
getSliceAsync: async (startIndex: number, endIndex: number) => {
const range = await getSetClient().lrange(channelName, startIndex, endIndex);
const range = await getSetClient.lrange(channelName, startIndex, endIndex);
return range.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item));
},
getSliceUntilTimeAsync: async (time: Date) => {
const length = await getSetClient().llen(channelName);
const length = await getSetClient.llen(channelName);
const items: TData[] = [];
const itemsInCollection = await getSetClient().lrange(channelName, 0, length - 1);
const itemsInCollection = await getSetClient.lrange(channelName, 0, length - 1);
for (let i = 0; i < length - 1; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -327,7 +324,7 @@ export const createChannelEventHistoryOld = <TData>(channelName: string, maxElem
return items;
},
getLengthAsync: async () => {
return await getSetClient().llen(channelName);
return await getSetClient.llen(channelName);
},
name: channelName,
};
@@ -341,14 +338,14 @@ export const createChannelWithLatestAndEvents = <TData>(channelName: string) =>
});
},
publishAndUpdateLastStateAsync: async (data: TData) => {
await getPublisher().publish(channelName, superjson.stringify(data));
await getSetClient().set(channelName, superjson.stringify({ data, timestamp: new Date() }));
await publisher.publish(channelName, superjson.stringify(data));
await getSetClient.set(channelName, superjson.stringify({ data, timestamp: new Date() }));
},
setAsync: async (data: TData) => {
await getSetClient().set(channelName, superjson.stringify({ data, timestamp: new Date() }));
await getSetClient.set(channelName, superjson.stringify({ data, timestamp: new Date() }));
},
getAsync: async () => {
const data = await getSetClient().get(channelName);
const data = await getSetClient.get(channelName);
if (!data) return null;
return superjson.parse<{ data: TData; timestamp: Date }>(data);
@@ -357,8 +354,7 @@ export const createChannelWithLatestAndEvents = <TData>(channelName: string) =>
};
};
let _queueClient: RedisClient | undefined;
const getQueueClient = () => (_queueClient ??= createRedisConnection());
const queueClient = createRedisConnection();
type WithId<TItem> = TItem & { _id: string };
@@ -370,11 +366,11 @@ type WithId<TItem> = TItem & { _id: string };
export const createQueueChannel = <TItem>(name: string) => {
const queueChannelName = `queue:${name}`;
const getDataAsync = async () => {
const data = await getQueueClient().get(queueChannelName);
const data = await queueClient.get(queueChannelName);
return data ? superjson.parse<WithId<TItem>[]>(data) : [];
};
const setDataAsync = async (data: WithId<TItem>[]) => {
await getQueueClient().set(queueChannelName, superjson.stringify(data));
await queueClient.set(queueChannelName, superjson.stringify(data));
};
return {
@@ -421,5 +417,5 @@ export const createQueueChannel = <TItem>(name: string) => {
};
export const handshakeAsync = async () => {
await getSetClient().hello();
await getSetClient.hello();
};
-45
View File
@@ -1,45 +0,0 @@
import { describe, expect, test, vi } from "vitest";
import { createRedisClient } from "@homarr/core/infrastructure/redis";
import { createRedisConnection } from "./connection";
const mockRedisInstance = { ping: vi.fn() };
vi.mock("@homarr/core/infrastructure/redis", () => ({
createRedisClient: vi.fn(() => mockRedisInstance),
}));
describe("createRedisConnection", () => {
test("returns a redis client regardless of CI env", () => {
const original = process.env.CI;
process.env.CI = "true";
const client = createRedisConnection();
expect(client).toBe(mockRedisInstance);
expect(client).not.toBeNull();
process.env.CI = original;
});
test("returns a redis client when DISABLE_REDIS_LOGS is set", () => {
const original = process.env.DISABLE_REDIS_LOGS;
process.env.DISABLE_REDIS_LOGS = "true";
const client = createRedisConnection();
expect(client).toBe(mockRedisInstance);
expect(client).not.toBeNull();
process.env.DISABLE_REDIS_LOGS = original;
});
test("always delegates to createRedisClient", () => {
vi.mocked(createRedisClient).mockClear();
createRedisConnection();
expect(createRedisClient).toHaveBeenCalledOnce();
});
});
+13 -1
View File
@@ -1,3 +1,15 @@
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import { createRedisClient } from "@homarr/core/infrastructure/redis";
export const createRedisConnection = () => createRedisClient();
/**
* Creates a new Redis connection
* @returns redis client
*/
export const createRedisConnection = () => {
if (Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS)) {
// Return null if we are in CI as we don't want to connect to Redis
return null as unknown as RedisClient;
}
return createRedisClient();
};
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
"./*": "./src/*.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
"./*": "./src/*.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@extractus/feed-extractor": "catalog:",
"@homarr/common": "workspace:^0.1.0",
@@ -37,10 +36,7 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,22 +11,21 @@
]
}
},
"exports": {
".": "./index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
+6 -10
View File
@@ -4,10 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./creator": "./src/creator.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -15,13 +11,16 @@
]
}
},
"exports": {
".": "./index.ts",
"./creator": "./src/creator.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
@@ -31,10 +30,7 @@
"react-dom": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-5
View File
@@ -1,5 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
import reactConfig from "@homarr/eslint-config/react";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig, ...reactConfig];
+6 -10
View File
@@ -4,10 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"typesVersions": {
"*": {
"*": [
@@ -15,13 +11,16 @@
]
}
},
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
@@ -44,10 +43,7 @@
"use-deep-compare-effect": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+9 -13
View File
@@ -4,6 +4,13 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"exports": {
".": "./index.ts",
"./client": "./src/client/index.ts",
@@ -12,20 +19,12 @@
"./request": "./src/request.ts",
"./dayjs": "./src/dayjs.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
@@ -38,10 +37,7 @@
"react-dom": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
+74 -74
View File
@@ -159,7 +159,7 @@
"label": "Abrir resultados da busca em uma nova guia"
},
"ddgBangs": {
"label": ""
"label": "Ativar bangs do DuckDuckGo"
}
},
"error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": {
"notification": {
"success": {
"message": ""
"message": "Bangs do DuckDuckGo alternados com sucesso"
},
"error": {
"message": ""
"message": "Não foi possível alternar os bangs do DuckDuckGo"
}
}
},
@@ -1093,7 +1093,7 @@
},
"colorScheme": {
"options": {
"auto": "",
"auto": "Automático",
"light": "Claro",
"dark": "Escuro"
}
@@ -2111,101 +2111,101 @@
}
},
"umami": {
"name": "",
"description": "",
"visitors": "",
"active": "",
"pageviews": "",
"visits": "",
"bounceRate": "",
"avgDuration": "",
"name": "Umami Analytics",
"description": "Exibir estatísticas de visitantes da sua instância do Umami",
"visitors": "visitantes",
"active": "ativos",
"pageviews": "visualizações de página",
"visits": "visitas",
"bounceRate": "rejeição",
"avgDuration": "média",
"option": {
"websiteId": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noWebsites": "",
"configureIntegrationFirst": ""
"label": "site",
"description": "Selecione um site monitorado pela sua instância do Umami",
"placeholder": "Selecionar um site",
"loading": "Carregando sites...",
"noWebsites": "Nenhum site encontrado",
"configureIntegrationFirst": "Conecte uma integração do Umami primeiro"
},
"timeFrame": {
"label": "",
"description": "",
"label": "Período de tempo",
"description": "O período de tempo para exibir os dados de visitantes",
"option": {
"today": "",
"24h": "",
"7d": "",
"30d": "",
"month": "",
"lastMonth": ""
"today": "Hoje",
"24h": "Últimas 24 horas",
"7d": "Últimos 7 dias",
"30d": "Últimos 30 dias",
"month": "Este mês",
"lastMonth": "Mês passado"
}
},
"eventName": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"none": "",
"configureWebsiteFirst": ""
"label": "Evento",
"description": "Exibir total de ocorrências de um evento específico",
"placeholder": "Selecionar um evento",
"loading": "Carregando eventos...",
"noEvents": "Nenhum evento encontrado",
"none": "Nenhum evento",
"configureWebsiteFirst": "Selecione um site primeiro para carregar os eventos"
},
"chartType": {
"label": "",
"description": "",
"label": "Tipo de gráfico",
"description": "Estilo de exibição do gráfico",
"option": {
"bar": "",
"sparkline": ""
"bar": "Gráfico de barras",
"sparkline": "Sparkline"
}
},
"chartStyle": {
"label": "",
"description": "",
"label": "Estilo do gráfico",
"description": "Como exibir a série de eventos no gráfico",
"option": {
"grouped": "",
"overlay": ""
"grouped": "Lado a lado",
"overlay": "Sobreposição (subconjunto)"
}
},
"viewMode": {
"label": "",
"description": "",
"label": "Visualização",
"description": "O que exibir no corpo do widget",
"option": {
"chart": "",
"events": "",
"topPages": "",
"topReferrers": ""
"chart": "Gráfico",
"events": "Gráfico de eventos",
"topPages": "Principais páginas",
"topReferrers": "Principais referenciadores"
}
},
"eventNames": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"configureWebsiteFirst": ""
"label": "Eventos",
"description": "Selecione um ou mais eventos para o gráfico",
"placeholder": "Selecionar eventos...",
"loading": "Carregando eventos...",
"noEvents": "Nenhum evento encontrado",
"configureWebsiteFirst": "Selecione um site primeiro para carregar os eventos"
},
"topCount": {
"label": "",
"description": ""
"label": "Itens para exibir",
"description": "Número de itens principais para exibir (máx 500)"
}
},
"event": {
"occurrences": "",
"label": ""
"occurrences": "Eventos",
"label": "Eventos"
},
"topPages": {
"direct": ""
"direct": "Direto"
},
"topReferrers": {
"direct": ""
"direct": "Direto"
},
"events": {
"selectPrompt": ""
"selectPrompt": "Selecionar eventos para o gráfico"
},
"error": {
"noIntegration": "",
"noWebsite": "",
"noData": "",
"internalServerError": ""
"noIntegration": "Nenhuma integração Umami configurada",
"noWebsite": "Nenhum site configurado",
"noData": "Nenhum dado disponível",
"internalServerError": "Falha ao buscar dados do Umami"
}
},
"common": {
@@ -3680,7 +3680,7 @@
"defaultColorScheme": {
"label": "Esquema de cores padrão",
"options": {
"auto": "",
"auto": "Automático",
"light": "Claro",
"dark": "Escuro"
}
@@ -3783,7 +3783,7 @@
"label": "Tracearr"
},
"umamiActiveVisitors": {
"label": ""
"label": "Visitantes Ativos do Umami"
}
},
"interval": {
@@ -3972,7 +3972,7 @@
"label": "Portas"
},
"host": {
"label": ""
"label": "Host"
}
},
"action": {
@@ -4499,7 +4499,7 @@
"title": "Comandos globais",
"option": {
"colorScheme": {
"auto": "",
"auto": "Alternar modo de aparência",
"light": "Alternar para o modo claro",
"dark": "Alternar para o modo escuro"
},
@@ -4551,13 +4551,13 @@
"group": {
"searchEngine": {
"title": "Mecanismos de busca",
"tipDdgBangs": "",
"tipDdgBangs": "Dica: Você também pode usar qualquer bang do DuckDuckGo aqui, ex: !something",
"bang": {
"searchWithQuery": "",
"pressEnterToOpen": "",
"engineSelected": "",
"typeQueryToContinue": "",
"emptyBangHint": ""
"searchWithQuery": "Pesquisar \"{query}\" com {label}",
"pressEnterToOpen": "Pressione Enter para abrir",
"engineSelected": "{label} selecionado (!{bang})",
"typeQueryToContinue": "Digite sua pesquisa para continuar",
"emptyBangHint": "Digite um bang, ex: !yt, e pressione Espaço para selecionar"
},
"children": {
"action": {
-5
View File
@@ -1,5 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
import reactConfig from "@homarr/eslint-config/react";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig, ...reactConfig];
+8 -12
View File
@@ -4,12 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css",
"./hooks": "./src/hooks/index.ts",
"./icons": "./src/icons/index.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -17,13 +11,18 @@
]
}
},
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css",
"./hooks": "./src/hooks/index.ts",
"./icons": "./src/icons/index.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
@@ -42,12 +41,9 @@
"svgson": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/crypto-js": "catalog:",
"@types/css-modules": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-4
View File
@@ -1,4 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+5 -9
View File
@@ -4,9 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
"./*": "./src/*.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -14,13 +11,15 @@
]
}
},
"exports": {
"./*": "./src/*.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt --check .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
@@ -28,10 +27,7 @@
"zod-form-data": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-5
View File
@@ -1,5 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
import reactConfig from "@homarr/eslint-config/react";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig, ...reactConfig];
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more