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
121 changed files with 1676 additions and 4737 deletions

No files matched your search

+3 -6
View File
@@ -3,9 +3,6 @@ version = 1
[[analyzers]] [[analyzers]]
name = "javascript" name = "javascript"
[analyzers.meta] [analyzers.meta]
plugins = ["react"] plugins = ["react"]
environment = ["nodejs"] environment = ["nodejs"]
[[transformers]]
name = "prettier"
+1
View File
@@ -33,6 +33,7 @@ body:
options: options:
# The below comment is used to insert a new version with on-release.yml # The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION# #NEXT_VERSION#
- 1.62.0
- 1.61.0 - 1.61.0
- 1.60.1-beta.1 - 1.60.1-beta.1
- 1.60.0 - 1.60.0
+33 -1
View File
@@ -24,6 +24,38 @@ env:
TURBO_SCM_HEAD: ${{ github.sha }} TURBO_SCM_HEAD: ${{ github.sha }}
jobs: 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: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
@@ -54,7 +86,7 @@ jobs:
uses: ./tooling/github/setup uses: ./tooling/github/setup
- name: Format - name: Format
run: pnpm turbo format --affected --continue -- --cache --cache-location node_modules/.cache/.prettiercache run: pnpm turbo format --affected --continue
typecheck: typecheck:
runs-on: ubuntu-latest 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
View File
@@ -31,6 +31,5 @@ scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n|
# refactorTemplates: # refactorTemplates:
# - i18n.get("$1") # - i18n.get("$1")
# If set to true, only enables this custom framework (will disable all built-in frameworks) # 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", "request": "launch",
"command": "pnpm docker:dev", "command": "pnpm docker:dev",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"skipFiles": [ "skipFiles": ["<node_internals>/**"]
"<node_internals>/**"
]
}, },
{ {
"name": "lint fix", "name": "lint fix",
@@ -25,9 +23,7 @@
"request": "launch", "request": "launch",
"command": "pnpm lint:fix", "command": "pnpm lint:fix",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"skipFiles": [ "skipFiles": ["<node_internals>/**"]
"<node_internals>/**"
]
}, },
{ {
"name": "format fix", "name": "format fix",
@@ -35,9 +31,7 @@
"request": "launch", "request": "launch",
"command": "pnpm format:fix", "command": "pnpm format:fix",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"skipFiles": [ "skipFiles": ["<node_internals>/**"]
"<node_internals>/**"
]
}, },
{ {
"name": "db push", "name": "db push",
@@ -69,9 +63,7 @@
"request": "launch", "request": "launch",
"command": "pnpm test", "command": "pnpm test",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"skipFiles": [ "skipFiles": ["<node_internals>/**"]
"<node_internals>/**"
]
}, },
{ {
"name": "test ui", "name": "test ui",
@@ -79,9 +71,7 @@
"request": "launch", "request": "launch",
"command": "pnpm test:ui", "command": "pnpm test:ui",
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"skipFiles": [ "skipFiles": ["<node_internals>/**"]
"<node_internals>/**"
]
} }
] ]
} }
+3 -14
View File
@@ -1,13 +1,6 @@
{ {
"eslint.workingDirectories": [
{
"mode": "auto"
}
],
"eslint.experimental.useFlatConfig": true,
"typescript.tsdk": "node_modules\\typescript\\lib", "typescript.tsdk": "node_modules\\typescript\\lib",
"js/ts.implicitProjectConfig.experimentalDecorators": true, "js/ts.implicitProjectConfig.experimentalDecorators": true,
"prettier.configPath": "./tooling/prettier/index.mjs",
"cSpell.words": [ "cSpell.words": [
"ajnart", "ajnart",
"cqmin", "cqmin",
@@ -34,11 +27,7 @@
], ],
"i18n-ally.dirStructure": "auto", "i18n-ally.dirStructure": "auto",
"i18n-ally.displayLanguage": "en", "i18n-ally.displayLanguage": "en",
"i18n-ally.enabledFrameworks": [ "i18n-ally.enabledFrameworks": ["custom"],
"custom" "i18n-ally.localesPaths": ["packages/translation/src/lang"],
], "i18n-ally.keystyle": "nested"
"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) ## [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 ### 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", "build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules", "clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev", "dev": "pnpm with-env next dev",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"start": "pnpm with-env next start", "start": "pnpm with-env next start",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --" "with-env": "dotenv -e ../../.env --"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@dnd-kit/core": "catalog:", "@dnd-kit/core": "catalog:",
"@dnd-kit/modifiers": "catalog:", "@dnd-kit/modifiers": "catalog:",
@@ -92,8 +91,6 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "catalog:", "@types/chroma-js": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
@@ -102,9 +99,7 @@
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@types/swagger-ui-react": "catalog:", "@types/swagger-ui-react": "catalog:",
"concurrently": "catalog:", "concurrently": "catalog:",
"eslint": "catalog:",
"node-loader": "catalog:", "node-loader": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }
@@ -1,70 +1,27 @@
"use client"; "use client";
import { useCallback, useRef } from "react"; import { Box, Stack } from "@mantine/core";
import { Box, LoadingOverlay, Stack } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { BoardCategorySection } from "~/components/board/sections/category-section"; import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section"; import { BoardEmptySection } from "~/components/board/sections/empty-section";
import { BoardBackgroundVideo } from "~/components/layout/background"; 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 = () => { export const ClientBoard = () => {
const board = useRequiredBoard(); const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout(); const currentLayoutId = useCurrentLayout();
const isReady = useIsBoardReady();
const fullWidthSortedSections = board.sections const fullWidthSortedSections = board.sections
.filter((section) => section.kind === "empty" || section.kind === "category") .filter((section) => section.kind === "empty" || section.kind === "category")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset); .sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
const ref = useRef<HTMLDivElement>(null);
return ( return (
<Box h="100%" pos="relative"> <Box h="100%" pos="relative">
<BoardBackgroundVideo /> <BoardBackgroundVideo />
<LoadingOverlay <Stack h="100%">
visible={!isReady}
transitionProps={{ duration: 500 }}
loaderProps={{ size: "lg" }}
h={fullHeightWithoutHeaderAndFooter}
/>
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
{fullWidthSortedSections.map((section) => {fullWidthSortedSections.map((section) =>
section.kind === "empty" ? ( section.kind === "empty" ? (
// Unique keys per layout to always reinitialize the gridstack
<BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} /> <BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} />
) : ( ) : (
<BoardCategorySection 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 // eslint-disable-next-line no-restricted-syntax
page: async ({ params }: { params: Promise<TParams> }) => { page: async ({ params }: { params: Promise<TParams> }) => {
const session = await auth(); const resolvedParams = await params;
const integrations = await getIntegrationsWithPermissionsAsync(session);
const board = await getInitialBoard(await params);
const queryClient = getQueryClient(); const queryClient = getQueryClient();
// Prefetch item data const [board, session] = await Promise.all([getInitialBoard(resolvedParams), auth()]);
const itemsMap = board.items.reduce((acc, item) => { const itemsMap = board.items.reduce((acc, item) => {
const existing = acc.get(item.kind); const existing = acc.get(item.kind);
if (existing) { if (existing) {
@@ -58,17 +56,20 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
return acc; return acc;
}, new Map<WidgetKind, Item[]>()); }, new Map<WidgetKind, Item[]>());
for (const [kind, items] of itemsMap) { const [integrations] = await Promise.all([
await prefetchForKindAsync(kind, queryClient, items).catch((error) => { getIntegrationsWithPermissionsAsync(session),
logger.error( ...Array.from(itemsMap).map(([kind, items]) =>
new ErrorWithMetadata( prefetchForKindAsync(kind, queryClient, items).catch((error) => {
"Failed to prefetch widget", logger.error(
{ widgetKind: kind, itemCount: items.length }, new ErrorWithMetadata(
{ cause: error }, "Failed to prefetch widget",
), { widgetKind: kind, itemCount: items.length },
); { cause: error },
}); ),
} );
}),
),
]);
return ( return (
<HydrationBoundary state={dehydrate(queryClient)}> <HydrationBoundary state={dehydrate(queryClient)}>
@@ -1,42 +1,19 @@
"use client"; "use client";
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react"; import { createContext, useCallback, useContext } from "react";
import { usePathname } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
const BoardReadyContext = createContext<{ const BoardReadyContext = createContext<{
isReady: boolean;
markAsReady: (id: string) => void; markAsReady: (id: string) => void;
} | null>(null); } | null>(null);
export const BoardReadyProvider = ({ children }: PropsWithChildren) => { export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
const pathname = usePathname(); // eslint-disable-next-line @typescript-eslint/no-empty-function
const utils = clientApi.useUtils(); const markAsReady = useCallback((_id: string) => {}, []);
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]));
}, []);
return ( return (
<BoardReadyContext.Provider <BoardReadyContext.Provider
value={{ value={{
isReady: board.sections.length === readySections.length,
markAsReady, markAsReady,
}} }}
> >
@@ -54,13 +31,3 @@ export const useMarkSectionAsReady = () => {
return context.markAsReady; 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"; import { getSectionElements } from "./section-elements";
export interface CreateItemInput { export interface CreateItemInput {
id?: string;
kind: WidgetKind; kind: WidgetKind;
options?: Record<string, unknown>; options?: Record<string, unknown>;
integrationIds?: string[];
} }
export const createItemCallback = export const createItemCallback =
({ kind, options = {} }: CreateItemInput) => ({ id, kind, options = {}, integrationIds = [] }: CreateItemInput) =>
(previous: Board): Board => { (previous: Board): Board => {
const firstSection = previous.sections const firstSection = previous.sections
.filter((section): section is EmptySection => section.kind === "empty") .filter((section): section is EmptySection => section.kind === "empty")
@@ -23,11 +25,11 @@ export const createItemCallback =
if (!firstSection) return previous; if (!firstSection) return previous;
const widget = { const widget = {
id: createId(), id: id ?? createId(),
kind, kind,
options, options,
layouts: createItemLayouts(previous, firstSection), layouts: createItemLayouts(previous, firstSection),
integrationIds: [], integrationIds,
advancedOptions: { advancedOptions: {
title: null, title: null,
customCssClasses: [], 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 { Avatar, Button, Card, Center, Grid, Group, Input, Stack, Text, Tooltip } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react"; 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 { getIconUrl, getIntegrationName } from "@homarr/definitions";
import type { IntegrationKind, WidgetKind } 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 { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui"; 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"; import { useItemActions } from "./item-actions";
export const ItemSelectModal = createModal<void>(({ actions }) => { export const ItemSelectModal = createModal<void>(({ actions }) => {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const t = useI18n(); 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( const items = useMemo(
() => () =>
@@ -40,8 +46,43 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
); );
const handleAdd = (kind: WidgetKind) => { 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(); 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 ( return (
@@ -3,6 +3,7 @@ import { useDisclosure } from "@mantine/hooks";
import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import type { CategorySection } from "~/app/[locale]/boards/_types"; import type { CategorySection } from "~/app/[locale]/boards/_types";
@@ -10,19 +11,36 @@ import { CategoryMenu } from "./category/category-menu";
import { GridStack } from "./gridstack/gridstack"; import { GridStack } from "./gridstack/gridstack";
import classes from "./item.module.css"; import classes from "./item.module.css";
const localStorageKey = (sectionId: string) => `homarr-section-collapsed-${sectionId}`;
interface Props { interface Props {
section: CategorySection; section: CategorySection;
} }
export const BoardCategorySection = ({ section }: Props) => { export const BoardCategorySection = ({ section }: Props) => {
const { status } = useSession();
const { mutate } = clientApi.section.changeCollapsed.useMutation(); const { mutate } = clientApi.section.changeCollapsed.useMutation();
const board = useRequiredBoard(); 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() { 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() { 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 muted
loop loop
playsInline playsInline
aria-hidden="true"
style={{ style={{
position: "fixed", position: "fixed",
width: "100vw", 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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"main": "./src/main.ts",
"types": "./src/main.ts",
"exports": { "exports": {
".": "./src/main.ts" ".": "./src/main.ts"
}, },
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts", "dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --" "with-env": "dotenv -e ../../.env --"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/analytics": "workspace:^0.1.0", "@homarr/analytics": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
@@ -43,14 +42,10 @@
"undici": "catalog:" "undici": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "catalog:", "@types/node": "catalog:",
"dotenv-cli": "catalog:", "dotenv-cli": "catalog:",
"esbuild": "catalog:", "esbuild": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"tsx": "catalog:", "tsx": "catalog:",
"typescript": "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. // 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 "./overrides";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { jobGroup } from "@homarr/cron-jobs"; import { jobGroup } from "@homarr/cron-jobs";
import { handshakeAsync } from "@homarr/redis";
import { onStartAsync } from "./on-start"; 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 () => { void (async () => {
await waitForRedisAsync();
await onStartAsync(); await onStartAsync();
await jobGroup.initializeAsync(); await jobGroup.initializeAsync();
await jobGroup.startAllAsync(); 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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"main": "./src/main.ts",
"types": "./src/main.ts",
"exports": { "exports": {
".": "./src/main.ts" ".": "./src/main.ts"
}, },
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts", "dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --" "with-env": "dotenv -e ../../.env --"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
@@ -32,13 +31,9 @@
"ws": "catalog:" "ws": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "catalog:", "@types/ws": "catalog:",
"esbuild": "catalog:", "esbuild": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }
+1 -1
View File
@@ -4,11 +4,11 @@
"dependencies": { "dependencies": {
"better-sqlite3": "^12.10.0" "better-sqlite3": "^12.10.0"
}, },
"packageManager": "pnpm@10.33.4",
"engines": { "engines": {
"node": ">=24.15.0", "node": ">=24.15.0",
"pnpm": ">=10.33.4" "pnpm": ">=10.33.4"
}, },
"packageManager": "pnpm@10.33.4",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"better-sqlite3" "better-sqlite3"
+10 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.61.1-beta.1", "version": "1.61.1",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -17,12 +17,11 @@
"db:studio": "pnpm -F db studio", "db:studio": "pnpm -F db studio",
"dev": "pnpm -F nextjs dev", "dev": "pnpm -F nextjs dev",
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up", "docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache", "format": "turbo format --continue",
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache", "format:fix": "turbo format --continue -- --write",
"lint": "turbo lint --continue -- --cache --cache-location node_modules/.cache/.eslintcache", "lint": "turbo lint --continue",
"lint:fix": "turbo lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", "lint:fix": "turbo lint --continue -- --fix",
"lint:ws": "pnpm dlx sherif@latest", "lint:ws": "pnpm dlx sherif@latest",
"package:new": "turbo gen init",
"release": "semantic-release", "release": "semantic-release",
"scripts:update-bug-report-template": "tsx ./scripts/update-bug-report-template.mts", "scripts:update-bug-report-template": "tsx ./scripts/update-bug-report-template.mts",
"scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts", "scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts",
@@ -33,9 +32,7 @@
"typecheck": "turbo typecheck", "typecheck": "turbo typecheck",
"with-env": "dotenv -e .env --" "with-env": "dotenv -e .env --"
}, },
"prettier": "@homarr/prettier-config",
"devDependencies": { "devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",
"@playwright/test": "catalog:", "@playwright/test": "catalog:",
"@semantic-release/changelog": "catalog:", "@semantic-release/changelog": "catalog:",
"@semantic-release/commit-analyzer": "catalog:", "@semantic-release/commit-analyzer": "catalog:",
@@ -52,7 +49,8 @@
"cross-env": "catalog:", "cross-env": "catalog:",
"jsdom": "catalog:", "jsdom": "catalog:",
"json5": "catalog:", "json5": "catalog:",
"prettier": "catalog:", "oxfmt": "catalog:",
"oxlint": "catalog:",
"semantic-release": "catalog:", "semantic-release": "catalog:",
"testcontainers": "catalog:", "testcontainers": "catalog:",
"turbo": "catalog:", "turbo": "catalog:",
@@ -60,11 +58,11 @@
"vite-tsconfig-paths": "catalog:", "vite-tsconfig-paths": "catalog:",
"vitest": "catalog:" "vitest": "catalog:"
}, },
"packageManager": "pnpm@10.33.4",
"engines": { "engines": {
"node": ">=24.15.0", "node": ">=24.15.0",
"pnpm": ">=10.33.4" "pnpm": ">=10.33.4"
}, },
"packageManager": "pnpm@10.33.4",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@tree-sitter-grammars/tree-sitter-yaml", "@tree-sitter-grammars/tree-sitter-yaml",
@@ -85,7 +83,7 @@
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6", "brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6",
"esbuild@<=0.24.2": ">=0.28.0", "esbuild@<=0.24.2": ">=0.28.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5", "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", "linkifyjs@<4.3.2": ">=4.3.3",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11", "nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
@@ -96,7 +94,7 @@
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
"tmp@<=0.2.3": ">=0.2.5", "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": { "patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch", "@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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
@@ -32,10 +31,7 @@
"superjson": "catalog:" "superjson": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./client": "./src/client.ts", "./client": "./src/client.ts",
@@ -12,15 +14,12 @@
"./shared": "./src/shared.ts", "./shared": "./src/shared.ts",
"./open-api": "./src/open-api.ts" "./open-api": "./src/open-api.ts"
}, },
"main": "./index.ts",
"types": "./index.ts",
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
@@ -55,11 +54,7 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"main": "./index.ts",
"types": "./index.ts",
"exports": { "exports": {
".": "./index.ts", ".": "./index.ts",
"./next": "./next.ts", "./next": "./next.ts",
@@ -14,15 +16,12 @@
"./shared": "./shared.ts", "./shared": "./shared.ts",
"./env": "./env.ts" "./env": "./env.ts"
}, },
"main": "./index.ts",
"types": "./index.ts",
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@auth/core": "catalog:", "@auth/core": "catalog:",
"@auth/drizzle-adapter": "catalog:", "@auth/drizzle-adapter": "catalog:",
@@ -41,13 +40,9 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "catalog:", "@types/bcrypt": "catalog:",
"@types/cookies": "catalog:", "@types/cookies": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
"./context": "./src/context.tsx",
"./updater": "./src/updater.ts",
"./edit-mode": "./src/edit-mode.tsx"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -16,23 +11,24 @@
] ]
} }
}, },
"exports": {
"./context": "./src/context.tsx",
"./updater": "./src/updater.ts",
"./edit-mode": "./src/edit-mode.tsx"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:" "react-dom": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,16 +11,18 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"build": "esbuild src/index.ts --bundle --platform=node --outfile=cli.cjs --external:bcrypt --external:cpu-features --loader:.html=text --loader:.node=text", "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", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"run": "pnpm with-env node cli.cjs", "run": "pnpm with-env node cli.cjs",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e .env --" "with-env": "dotenv -e .env --"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@drizzle-team/brocli": "catalog:", "@drizzle-team/brocli": "catalog:",
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
@@ -33,11 +32,8 @@
"dotenv": "catalog:" "dotenv": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "catalog:", "esbuild": "catalog:",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./types": "./src/types.ts",
"./server": "./src/server.ts",
"./client": "./src/client.ts",
"./env": "./env.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -18,13 +11,19 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./types": "./src/types.ts",
"./server": "./src/server.ts",
"./client": "./src/client.ts",
"./env": "./env.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@paralleldrive/cuid2": "catalog:", "@paralleldrive/cuid2": "catalog:",
@@ -39,10 +38,7 @@
"zod-validation-error": "catalog:" "zod-validation-error": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"exports": { "exports": {
"./infrastructure/redis": "./src/infrastructure/redis/client.ts", "./infrastructure/redis": "./src/infrastructure/redis/client.ts",
"./infrastructure/env": "./src/infrastructure/env/index.ts", "./infrastructure/env": "./src/infrastructure/env/index.ts",
@@ -22,20 +29,12 @@
"./infrastructure/http": "./src/infrastructure/http/index.ts", "./infrastructure/http": "./src/infrastructure/http/index.ts",
"./infrastructure/http/timeout": "./src/infrastructure/http/timeout.ts" "./infrastructure/http/timeout": "./src/infrastructure/http/timeout.ts"
}, },
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "catalog:", "@t3-oss/env-nextjs": "catalog:",
"better-sqlite3": "catalog:", "better-sqlite3": "catalog:",
@@ -49,12 +48,9 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "catalog:", "@types/better-sqlite3": "catalog:",
"@types/pg": "catalog:", "@types/pg": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }
@@ -5,7 +5,6 @@ import { redisEnv } from "./env";
const defaultRedisOptions = { const defaultRedisOptions = {
connectionName: "homarr", connectionName: "homarr",
lazyConnect: true,
} satisfies RedisOptions; } satisfies RedisOptions;
export type { Redis as RedisClient } from "ioredis"; 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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./publisher": "./src/publisher.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -15,21 +11,21 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./publisher": "./src/publisher.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/redis": "workspace:^0.1.0" "@homarr/redis": "workspace:^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./expressions": "./src/expressions.ts",
"./logger": "./src/logger.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -16,13 +11,17 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./expressions": "./src/expressions.ts",
"./logger": "./src/logger.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^", "@homarr/core": "workspace:^",
@@ -30,10 +29,7 @@
"croner": "catalog:" "croner": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./job-manager": "./src/job-manager.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -15,13 +11,16 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./job-manager": "./src/job-manager.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/analytics": "workspace:^0.1.0", "@homarr/analytics": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
@@ -43,10 +42,7 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"main": "./index.ts",
"types": "./index.ts",
"exports": { "exports": {
".": "./index.ts", ".": "./index.ts",
"./collection": "./collection.ts", "./collection": "./collection.ts",
@@ -12,16 +14,14 @@
"./queries": "./queries/index.ts", "./queries": "./queries/index.ts",
"./validationSchemas": "./validationSchemas.ts" "./validationSchemas": "./validationSchemas.ts"
}, },
"main": "./index.ts",
"types": "./index.ts",
"scripts": { "scripts": {
"build": "pnpm run build:sqlite && pnpm run build:mysql && pnpm run build:postgresql", "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: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: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", "build:sqlite": "esbuild migrations/sqlite/migrate.ts --bundle --platform=node --outfile=migrations/sqlite/migrate.cjs",
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"migration:custom": "pnpm with-env tsx ./migrations/custom/run-custom.ts", "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: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", "migration:mysql:generate": "pnpm with-env drizzle-kit generate --config ./configs/mysql.config.ts",
@@ -42,7 +42,6 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --" "with-env": "dotenv -e ../../.env --"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@auth/core": "catalog:", "@auth/core": "catalog:",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
@@ -63,15 +62,11 @@
"superjson": "catalog:" "superjson": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "catalog:", "@types/better-sqlite3": "catalog:",
"@types/pg": "catalog:", "@types/pg": "catalog:",
"dotenv-cli": "catalog:", "dotenv-cli": "catalog:",
"esbuild": "catalog:", "esbuild": "catalog:",
"eslint": "catalog:",
"prettier": "catalog:",
"tsx": "catalog:", "tsx": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,24 +11,23 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"postinstall": "tsx ./src/docs/codegen.ts", "postinstall": "tsx ./src/docs/codegen.ts",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "catalog:", "fast-xml-parser": "catalog:",
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"tsx": "catalog:", "tsx": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./shared": "./src/shared.ts",
"./env": "./src/env.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -16,24 +11,25 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./shared": "./src/shared.ts",
"./env": "./src/env.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"dockerode": "catalog:" "dockerode": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "catalog:", "@types/dockerode": "catalog:",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./types": "./src/types.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -15,13 +11,16 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./types": "./src/types.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
@@ -31,10 +30,7 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
@@ -35,10 +34,7 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./local": "./src/local.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -15,23 +11,23 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./local": "./src/local.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0" "@homarr/db": "workspace:^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./src/index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
".": "./src/index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
@@ -28,11 +27,8 @@
"bcrypt": "catalog:" "bcrypt": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "catalog:", "@types/bcrypt": "catalog:",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./test-connection": "./src/base/test-connection/index.ts",
"./client": "./src/client.ts",
"./types": "./src/types.ts"
},
"typesVersions": { "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": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@ctrl/deluge": "catalog:", "@ctrl/deluge": "catalog:",
"@ctrl/qbittorrent": "catalog:", "@ctrl/qbittorrent": "catalog:",
@@ -52,12 +51,9 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "catalog:", "@types/node-unifi": "catalog:",
"@types/xml2js": "catalog:", "@types/xml2js": "catalog:",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
@@ -42,10 +41,7 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
@@ -29,10 +28,7 @@
"react": "catalog:" "react": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -15,23 +11,23 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "catalog:", "@mantine/notifications": "catalog:",
"@tabler/icons-react": "catalog:" "@tabler/icons-react": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "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": { "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": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
@@ -48,11 +47,8 @@
"zod-form-data": "catalog:" "zod-form-data": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "catalog:", "@types/adm-zip": "catalog:",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,22 +11,21 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,22 +11,21 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0" "@homarr/core": "workspace:^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^", "@homarr/common": "workspace:^",
"@homarr/core": "workspace:^", "@homarr/core": "workspace:^",
@@ -30,10 +29,7 @@
"superjson": "catalog:" "superjson": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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 type { MaybePromise } from "@homarr/common/types";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import { createRedisConnection } from "./connection"; import { createRedisConnection } from "./connection";
@@ -19,10 +18,7 @@ type SubscriptionCallback = (message: string) => MaybePromise<void>;
*/ */
export class ChannelSubscriptionTracker { export class ChannelSubscriptionTracker {
private static subscriptions = new Map<string, Map<string, SubscriptionCallback>>(); private static subscriptions = new Map<string, Map<string, SubscriptionCallback>>();
private static _redis: RedisClient | undefined; private static redis = createRedisConnection();
private static get redis() {
return (this._redis ??= createRedisConnection());
}
private static listenerActive = false; 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 { createId, hashObjectBase64 } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs"; import { createLogger } from "@homarr/core/infrastructure/logs";
import type { RedisClient } from "@homarr/core/infrastructure/redis";
import type { WidgetKind } from "@homarr/definitions"; import type { WidgetKind } from "@homarr/definitions";
import { ChannelSubscriptionTracker } from "./channel-subscription-tracker"; import { ChannelSubscriptionTracker } from "./channel-subscription-tracker";
@@ -10,11 +9,8 @@ import { createRedisConnection } from "./connection";
const logger = createLogger({ module: "redisChannel" }); const logger = createLogger({ module: "redisChannel" });
let _publisher: RedisClient | undefined; const publisher = createRedisConnection();
const getPublisher = () => (_publisher ??= createRedisConnection()); const lastDataClient = createRedisConnection();
let _lastDataClient: RedisClient | undefined;
const getLastDataClient = () => (_lastDataClient ??= createRedisConnection());
/** /**
* Creates a new pub/sub channel. * Creates a new pub/sub channel.
@@ -31,33 +27,34 @@ export const createSubPubChannel = <TData>(name: string, { persist }: { persist:
*/ */
subscribe: (callback: (data: TData) => void) => { subscribe: (callback: (data: TData) => void) => {
if (persist) { if (persist) {
void getLastDataClient() void lastDataClient.get(lastChannelName).then((data) => {
.get(lastChannelName) if (data) {
.then((data) => { callback(superjson.parse(data));
if (data) { }
callback(superjson.parse(data)); });
}
});
} }
return ChannelSubscriptionTracker.subscribe(channelName, (message) => { return ChannelSubscriptionTracker.subscribe(channelName, (message) => {
callback(superjson.parse(message)); callback(superjson.parse(message));
}); });
}, },
/**
* Publish data to the channel with last data saved.
* @param data data to be published
*/
publishAsync: async (data: TData) => { publishAsync: async (data: TData) => {
if (persist) { 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 () => { getLastDataAsync: async () => {
const data = await getLastDataClient().get(lastChannelName); const data = await lastDataClient.get(lastChannelName);
return data ? superjson.parse<TData>(data) : null; return data ? superjson.parse<TData>(data) : null;
}, },
}; };
}; };
let _getSetClient: RedisClient | undefined; const getSetClient = createRedisConnection();
const getSetClient = () => (_getSetClient ??= createRedisConnection());
/** /**
* Creates a new redis channel for a list * Creates a new redis channel for a list
@@ -72,7 +69,7 @@ export const createListChannel = <TItem>(name: string) => {
* @returns an array of all items * @returns an array of all items
*/ */
getAllAsync: async () => { 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)); return items.map((item) => superjson.parse<TItem>(item));
}, },
/** /**
@@ -80,20 +77,20 @@ export const createListChannel = <TItem>(name: string) => {
* @param item item to remove * @param item item to remove
*/ */
removeAsync: async (item: TItem) => { 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 * Clear all items from the channels list
*/ */
clearAsync: async () => { clearAsync: async () => {
await getSetClient().del(listChannelName); await getSetClient.del(listChannelName);
}, },
/** /**
* Add an item to the channels list * Add an item to the channels list
* @param item item to add * @param item item to add
*/ */
addAsync: async (item: TItem) => { 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 * @returns data or null if not found
*/ */
getAsync: async () => { getAsync: async () => {
const data = await getSetClient().get(name); const data = await getSetClient.get(name);
return data ? superjson.parse<TData>(data) : null; 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 * @param data data to be stored in the channel
*/ */
setAsync: async (data: TData) => { setAsync: async (data: TData) => {
await getSetClient().set(name, superjson.stringify(data)); await getSetClient.set(name, superjson.stringify(data));
}, },
/** /**
* Remove data from the channel * Remove data from the channel
*/ */
removeAsync: async () => { 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 * @returns data or null if not found or expired
*/ */
getAsync: async () => { getAsync: async () => {
const data = await getSetClient().get(cacheChannelName); const data = await getSetClient.get(cacheChannelName);
if (!data) return null; if (!data) return null;
const parsedData = superjson.parse<{ data: TData; timestamp: Date }>(data); 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 * @returns data or new data if not present or expired
*/ */
consumeAsync: async (callback: () => Promise<TData>) => { consumeAsync: async (callback: () => Promise<TData>) => {
const data = await getSetClient().get(cacheChannelName); const data = await getSetClient.get(cacheChannelName);
const getNewDataAsync = async () => { const getNewDataAsync = async () => {
logger.debug(`Cache miss for channel '${cacheChannelName}'`); logger.debug(`Cache miss for channel '${cacheChannelName}'`);
const newData = await callback(); const newData = await callback();
const result = { data: newData, timestamp: new Date() }; 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}'`); logger.debug(`Cache updated for channel '${cacheChannelName}'`);
return result; return result;
}; };
@@ -190,14 +187,14 @@ export const createCacheChannel = <TData>(name: string, cacheDurationMs: number
* Invalidate the cache channels data. * Invalidate the cache channels data.
*/ */
invalidateAsync: async () => { invalidateAsync: async () => {
await getSetClient().del(cacheChannelName); await getSetClient.del(cacheChannelName);
}, },
/** /**
* Set the data in the cache channel. * Set the data in the cache channel.
* @param data data to be stored in the cache channel * @param data data to be stored in the cache channel
*/ */
setAsync: async (data: TData) => { 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 }) => { pushAsync: async (data: TData, options = { publish: false }) => {
if (options.publish) await getPublisher().publish(channelName, superjson.stringify(data)); if (options.publish) await publisher.publish(channelName, superjson.stringify(data));
await getSetClient().lpush(channelName, superjson.stringify({ data, timestamp: new Date() })); await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await getSetClient().ltrim(channelName, 0, maxElements); await getSetClient.ltrim(channelName, 0, maxElements);
}, },
clearAsync: async () => { clearAsync: async () => {
await getSetClient().del(channelName); await getSetClient.del(channelName);
}, },
/** /**
* Returns a slice of the available data in the channel. * 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. * @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) => { 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)); return range.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item));
}, },
getSliceUntilTimeAsync: async (time: Date) => { getSliceUntilTimeAsync: async (time: Date) => {
const itemsInCollection = await getSetClient().lrange(channelName, 0, -1); const itemsInCollection = await getSetClient.lrange(channelName, 0, -1);
return itemsInCollection return itemsInCollection
.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item)) .map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item))
.filter((item) => item.timestamp < time); .filter((item) => item.timestamp < time);
}, },
getLengthAsync: async () => { getLengthAsync: async () => {
return await getSetClient().llen(channelName); return await getSetClient.llen(channelName);
}, },
name: channelName, name: channelName,
}; };
@@ -274,11 +271,11 @@ export const createChannelEventHistory = <TData>(channelName: string, maxElement
*/ */
export const createChannelEventHistoryOld = <TData>(channelName: string, maxElements = 15) => { export const createChannelEventHistoryOld = <TData>(channelName: string, maxElements = 15) => {
const popElementsOverMaxAsync = async () => { const popElementsOverMaxAsync = async () => {
const length = await getSetClient().llen(channelName); const length = await getSetClient.llen(channelName);
if (length <= maxElements) { if (length <= maxElements) {
return; return;
} }
await getSetClient().ltrim(channelName, 0, maxElements - 1); await getSetClient.ltrim(channelName, 0, maxElements - 1);
}; };
return { return {
@@ -288,33 +285,33 @@ export const createChannelEventHistoryOld = <TData>(channelName: string, maxElem
}); });
}, },
publishAndPushAsync: async (data: TData) => { publishAndPushAsync: async (data: TData) => {
await getPublisher().publish(channelName, superjson.stringify(data)); await publisher.publish(channelName, superjson.stringify(data));
await getSetClient().lpush(channelName, superjson.stringify({ data, timestamp: new Date() })); await getSetClient.lpush(channelName, superjson.stringify({ data, timestamp: new Date() }));
await popElementsOverMaxAsync(); await popElementsOverMaxAsync();
}, },
pushAsync: async (data: TData) => { 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(); await popElementsOverMaxAsync();
}, },
clearAsync: async () => { clearAsync: async () => {
await getSetClient().del(channelName); await getSetClient.del(channelName);
}, },
getLastAsync: async () => { getLastAsync: async () => {
const length = await getSetClient().llen(channelName); const length = await getSetClient.llen(channelName);
const data = await getSetClient().lrange(channelName, length - 1, length); const data = await getSetClient.lrange(channelName, length - 1, length);
if (data.length !== 1) return null; if (data.length !== 1) return null;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return superjson.parse<{ data: TData; timestamp: Date }>(data[0]!); return superjson.parse<{ data: TData; timestamp: Date }>(data[0]!);
}, },
getSliceAsync: async (startIndex: number, endIndex: number) => { 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)); return range.map((item) => superjson.parse<{ data: TData; timestamp: Date }>(item));
}, },
getSliceUntilTimeAsync: async (time: Date) => { getSliceUntilTimeAsync: async (time: Date) => {
const length = await getSetClient().llen(channelName); const length = await getSetClient.llen(channelName);
const items: TData[] = []; 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++) { for (let i = 0; i < length - 1; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -327,7 +324,7 @@ export const createChannelEventHistoryOld = <TData>(channelName: string, maxElem
return items; return items;
}, },
getLengthAsync: async () => { getLengthAsync: async () => {
return await getSetClient().llen(channelName); return await getSetClient.llen(channelName);
}, },
name: channelName, name: channelName,
}; };
@@ -341,14 +338,14 @@ export const createChannelWithLatestAndEvents = <TData>(channelName: string) =>
}); });
}, },
publishAndUpdateLastStateAsync: async (data: TData) => { publishAndUpdateLastStateAsync: async (data: TData) => {
await getPublisher().publish(channelName, superjson.stringify(data)); await publisher.publish(channelName, superjson.stringify(data));
await getSetClient().set(channelName, superjson.stringify({ data, timestamp: new Date() })); await getSetClient.set(channelName, superjson.stringify({ data, timestamp: new Date() }));
}, },
setAsync: async (data: TData) => { 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 () => { getAsync: async () => {
const data = await getSetClient().get(channelName); const data = await getSetClient.get(channelName);
if (!data) return null; if (!data) return null;
return superjson.parse<{ data: TData; timestamp: Date }>(data); return superjson.parse<{ data: TData; timestamp: Date }>(data);
@@ -357,8 +354,7 @@ export const createChannelWithLatestAndEvents = <TData>(channelName: string) =>
}; };
}; };
let _queueClient: RedisClient | undefined; const queueClient = createRedisConnection();
const getQueueClient = () => (_queueClient ??= createRedisConnection());
type WithId<TItem> = TItem & { _id: string }; type WithId<TItem> = TItem & { _id: string };
@@ -370,11 +366,11 @@ type WithId<TItem> = TItem & { _id: string };
export const createQueueChannel = <TItem>(name: string) => { export const createQueueChannel = <TItem>(name: string) => {
const queueChannelName = `queue:${name}`; const queueChannelName = `queue:${name}`;
const getDataAsync = async () => { const getDataAsync = async () => {
const data = await getQueueClient().get(queueChannelName); const data = await queueClient.get(queueChannelName);
return data ? superjson.parse<WithId<TItem>[]>(data) : []; return data ? superjson.parse<WithId<TItem>[]>(data) : [];
}; };
const setDataAsync = async (data: WithId<TItem>[]) => { const setDataAsync = async (data: WithId<TItem>[]) => {
await getQueueClient().set(queueChannelName, superjson.stringify(data)); await queueClient.set(queueChannelName, superjson.stringify(data));
}; };
return { return {
@@ -421,5 +417,5 @@ export const createQueueChannel = <TItem>(name: string) => {
}; };
export const handshakeAsync = async () => { 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"; 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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
"./*": "./src/*.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
"./*": "./src/*.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@extractus/feed-extractor": "catalog:", "@extractus/feed-extractor": "catalog:",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
@@ -37,10 +36,7 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,22 +11,21 @@
] ]
} }
}, },
"exports": {
".": "./index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0" "@homarr/translation": "workspace:^0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./creator": "./src/creator.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -15,13 +11,16 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./creator": "./src/creator.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
@@ -31,10 +30,7 @@
"react-dom": "catalog:" "react-dom": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -15,13 +11,16 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0",
@@ -44,10 +43,7 @@
"use-deep-compare-effect": "catalog:" "use-deep-compare-effect": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"exports": { "exports": {
".": "./index.ts", ".": "./index.ts",
"./client": "./src/client/index.ts", "./client": "./src/client/index.ts",
@@ -12,20 +19,12 @@
"./request": "./src/request.ts", "./request": "./src/request.ts",
"./dayjs": "./src/dayjs.ts" "./dayjs": "./src/dayjs.ts"
}, },
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
@@ -38,10 +37,7 @@
"react-dom": "catalog:" "react-dom": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }
+74 -74
View File
@@ -159,7 +159,7 @@
"label": "Abrir resultados da busca em uma nova guia" "label": "Abrir resultados da busca em uma nova guia"
}, },
"ddgBangs": { "ddgBangs": {
"label": "" "label": "Ativar bangs do DuckDuckGo"
} }
}, },
"error": { "error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": { "changeDdgBangs": {
"notification": { "notification": {
"success": { "success": {
"message": "" "message": "Bangs do DuckDuckGo alternados com sucesso"
}, },
"error": { "error": {
"message": "" "message": "Não foi possível alternar os bangs do DuckDuckGo"
} }
} }
}, },
@@ -1093,7 +1093,7 @@
}, },
"colorScheme": { "colorScheme": {
"options": { "options": {
"auto": "", "auto": "Automático",
"light": "Claro", "light": "Claro",
"dark": "Escuro" "dark": "Escuro"
} }
@@ -2111,101 +2111,101 @@
} }
}, },
"umami": { "umami": {
"name": "", "name": "Umami Analytics",
"description": "", "description": "Exibir estatísticas de visitantes da sua instância do Umami",
"visitors": "", "visitors": "visitantes",
"active": "", "active": "ativos",
"pageviews": "", "pageviews": "visualizações de página",
"visits": "", "visits": "visitas",
"bounceRate": "", "bounceRate": "rejeição",
"avgDuration": "", "avgDuration": "média",
"option": { "option": {
"websiteId": { "websiteId": {
"label": "", "label": "site",
"description": "", "description": "Selecione um site monitorado pela sua instância do Umami",
"placeholder": "", "placeholder": "Selecionar um site",
"loading": "", "loading": "Carregando sites...",
"noWebsites": "", "noWebsites": "Nenhum site encontrado",
"configureIntegrationFirst": "" "configureIntegrationFirst": "Conecte uma integração do Umami primeiro"
}, },
"timeFrame": { "timeFrame": {
"label": "", "label": "Período de tempo",
"description": "", "description": "O período de tempo para exibir os dados de visitantes",
"option": { "option": {
"today": "", "today": "Hoje",
"24h": "", "24h": "Últimas 24 horas",
"7d": "", "7d": "Últimos 7 dias",
"30d": "", "30d": "Últimos 30 dias",
"month": "", "month": "Este mês",
"lastMonth": "" "lastMonth": "Mês passado"
} }
}, },
"eventName": { "eventName": {
"label": "", "label": "Evento",
"description": "", "description": "Exibir total de ocorrências de um evento específico",
"placeholder": "", "placeholder": "Selecionar um evento",
"loading": "", "loading": "Carregando eventos...",
"noEvents": "", "noEvents": "Nenhum evento encontrado",
"none": "", "none": "Nenhum evento",
"configureWebsiteFirst": "" "configureWebsiteFirst": "Selecione um site primeiro para carregar os eventos"
}, },
"chartType": { "chartType": {
"label": "", "label": "Tipo de gráfico",
"description": "", "description": "Estilo de exibição do gráfico",
"option": { "option": {
"bar": "", "bar": "Gráfico de barras",
"sparkline": "" "sparkline": "Sparkline"
} }
}, },
"chartStyle": { "chartStyle": {
"label": "", "label": "Estilo do gráfico",
"description": "", "description": "Como exibir a série de eventos no gráfico",
"option": { "option": {
"grouped": "", "grouped": "Lado a lado",
"overlay": "" "overlay": "Sobreposição (subconjunto)"
} }
}, },
"viewMode": { "viewMode": {
"label": "", "label": "Visualização",
"description": "", "description": "O que exibir no corpo do widget",
"option": { "option": {
"chart": "", "chart": "Gráfico",
"events": "", "events": "Gráfico de eventos",
"topPages": "", "topPages": "Principais páginas",
"topReferrers": "" "topReferrers": "Principais referenciadores"
} }
}, },
"eventNames": { "eventNames": {
"label": "", "label": "Eventos",
"description": "", "description": "Selecione um ou mais eventos para o gráfico",
"placeholder": "", "placeholder": "Selecionar eventos...",
"loading": "", "loading": "Carregando eventos...",
"noEvents": "", "noEvents": "Nenhum evento encontrado",
"configureWebsiteFirst": "" "configureWebsiteFirst": "Selecione um site primeiro para carregar os eventos"
}, },
"topCount": { "topCount": {
"label": "", "label": "Itens para exibir",
"description": "" "description": "Número de itens principais para exibir (máx 500)"
} }
}, },
"event": { "event": {
"occurrences": "", "occurrences": "Eventos",
"label": "" "label": "Eventos"
}, },
"topPages": { "topPages": {
"direct": "" "direct": "Direto"
}, },
"topReferrers": { "topReferrers": {
"direct": "" "direct": "Direto"
}, },
"events": { "events": {
"selectPrompt": "" "selectPrompt": "Selecionar eventos para o gráfico"
}, },
"error": { "error": {
"noIntegration": "", "noIntegration": "Nenhuma integração Umami configurada",
"noWebsite": "", "noWebsite": "Nenhum site configurado",
"noData": "", "noData": "Nenhum dado disponível",
"internalServerError": "" "internalServerError": "Falha ao buscar dados do Umami"
} }
}, },
"common": { "common": {
@@ -3680,7 +3680,7 @@
"defaultColorScheme": { "defaultColorScheme": {
"label": "Esquema de cores padrão", "label": "Esquema de cores padrão",
"options": { "options": {
"auto": "", "auto": "Automático",
"light": "Claro", "light": "Claro",
"dark": "Escuro" "dark": "Escuro"
} }
@@ -3783,7 +3783,7 @@
"label": "Tracearr" "label": "Tracearr"
}, },
"umamiActiveVisitors": { "umamiActiveVisitors": {
"label": "" "label": "Visitantes Ativos do Umami"
} }
}, },
"interval": { "interval": {
@@ -3972,7 +3972,7 @@
"label": "Portas" "label": "Portas"
}, },
"host": { "host": {
"label": "" "label": "Host"
} }
}, },
"action": { "action": {
@@ -4499,7 +4499,7 @@
"title": "Comandos globais", "title": "Comandos globais",
"option": { "option": {
"colorScheme": { "colorScheme": {
"auto": "", "auto": "Alternar modo de aparência",
"light": "Alternar para o modo claro", "light": "Alternar para o modo claro",
"dark": "Alternar para o modo escuro" "dark": "Alternar para o modo escuro"
}, },
@@ -4551,13 +4551,13 @@
"group": { "group": {
"searchEngine": { "searchEngine": {
"title": "Mecanismos de busca", "title": "Mecanismos de busca",
"tipDdgBangs": "", "tipDdgBangs": "Dica: Você também pode usar qualquer bang do DuckDuckGo aqui, ex: !something",
"bang": { "bang": {
"searchWithQuery": "", "searchWithQuery": "Pesquisar \"{query}\" com {label}",
"pressEnterToOpen": "", "pressEnterToOpen": "Pressione Enter para abrir",
"engineSelected": "", "engineSelected": "{label} selecionado (!{bang})",
"typeQueryToContinue": "", "typeQueryToContinue": "Digite sua pesquisa para continuar",
"emptyBangHint": "" "emptyBangHint": "Digite um bang, ex: !yt, e pressione Espaço para selecionar"
}, },
"children": { "children": {
"action": { "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css",
"./hooks": "./src/hooks/index.ts",
"./icons": "./src/icons/index.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -17,13 +11,18 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./styles.css": "./src/styles.css",
"./hooks": "./src/hooks/index.ts",
"./icons": "./src/icons/index.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
@@ -42,12 +41,9 @@
"svgson": "catalog:" "svgson": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/crypto-js": "catalog:", "@types/crypto-js": "catalog:",
"@types/css-modules": "catalog:", "@types/css-modules": "catalog:",
"eslint": "catalog:",
"typescript": "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, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
"./*": "./src/*.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -14,13 +11,15 @@
] ]
} }
}, },
"exports": {
"./*": "./src/*.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
@@ -28,10 +27,7 @@
"zod-form-data": "catalog:" "zod-form-data": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "catalog:",
"typescript": "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];
+8 -12
View File
@@ -4,12 +4,6 @@
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"exports": {
".": "./index.ts",
"./errors": "./src/errors/component.tsx",
"./modals": "./src/modals/index.ts",
"./prefetch": "./src/prefetch.ts"
},
"typesVersions": { "typesVersions": {
"*": { "*": {
"*": [ "*": [
@@ -17,13 +11,18 @@
] ]
} }
}, },
"exports": {
".": "./index.ts",
"./errors": "./src/errors/component.tsx",
"./modals": "./src/modals/index.ts",
"./prefetch": "./src/prefetch.ts"
},
"scripts": { "scripts": {
"clean": "rm -rf .turbo node_modules", "clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "oxfmt --check .",
"lint": "eslint", "lint": "oxlint",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@dnd-kit/core": "catalog:", "@dnd-kit/core": "catalog:",
"@dnd-kit/sortable": "catalog:", "@dnd-kit/sortable": "catalog:",
@@ -82,11 +81,8 @@
"zod": "catalog:" "zod": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "catalog:", "@types/video.js": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:" "typescript": "catalog:"
} }
} }
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more