Compare commits

...
Author SHA1 Message Date
manuel-rw db31f880c2 style: auto-fix lint and format 2026-07-01 09:36:02 +00:00
Manuel b902f93a3c feat: add widgets 2026-07-01 11:34:43 +02:00
manuel-rw 973baedfcb style: auto-fix lint and format 2026-07-01 09:05:27 +00:00
Manuel 629f3b0955 feat: add dawarich integration 2026-07-01 11:04:12 +02:00
homarr-renovate[bot] b83c8b6546 chore(deps): update dependency posthog-js to ^1.393.5 (#6135)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-30 21:53:10 +00:00
homarr-renovate[bot] 0e4189f693 chore(deps): update dependency dockerode to ^5.0.1 (#6130)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-30 20:09:36 +00:00
homarr-renovate[bot] 1cf010e9fb chore(deps): update node.js to v24.18.0 (#6107)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-30 18:11:58 +00:00
Thomasandgithub-actions[bot] 7e58858353 feat(widgets): right-click context menu with auto-generated toggle actions (#6126)
* docs(helm): Update Helm chart documentation

* feat(widgets): add right-click context menu with auto-generated toggle actions

- Add WidgetContextMenu component using Mantine 9.3 Menu.ContextMenu
- Extend WidgetDefinition with contextActions API for widget-specific actions
- Auto-generate context menu toggle items for switch-type widget options
- Add 'Search' context action to media-requests widgets (list + stats)
- Keep existing dots-button menu for discoverability in edit mode
- Add widgetStateRef to WidgetComponentProps for client-side-only state
- Add skipContextMenu opt-out on switch option definitions

* style: auto-fix lint and format

* fix(widgets): address CodeRabbit review — badge in context menu target, edit-only pending gate

- Move title badge inside WidgetContextMenu wrapper so right-clicking the title overlay also triggers the context menu
- Remove full-menu hide during integration loading; only disable 'Edit' action while isPending
- Add isPending to allActions useMemo dependency array

* style: auto-fix lint and format

* fix(widgets): gate context menu behind authenticated session

- Only show right-click context menu for logged-in users
- Unauthenticated users see plain children without menu wrapper
- Matches existing BoardItemMenu session gating pattern

* style: auto-fix lint and format

* chore: revert unrelated sitemap change from auto-fix

* style: auto-fix lint and format

* fix(context-menu): persist widget option changes to server

Toggle switches and edit modal changes from the context menu now call
saveBoard mutation to persist to DB. Previously only the client cache
was updated, losing changes on reload. Removes unnecessary invalidation
that caused cascading refetch loops.

* style: auto-fix lint and format

* fix: use explicit React type imports instead of global namespace

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-30 18:10:08 +02:00
9ed55d57dd Main (#6138)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-06-30 17:52:37 +02:00
43 changed files with 850 additions and 73 deletions

No files matched your search

+62
View File
@@ -163,3 +163,65 @@ jobs:
run: cp .env.example .env
- name: Build
run: pnpm turbo build --affected
memory-regression:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Docker image
uses: docker/build-push-action@v7
with:
platforms: linux/amd64
context: .
push: false
load: true
tags: homarr-memory-test
network: host
cache-from: type=gha
cache-to: type=gha,mode=max
env:
SKIP_ENV_VALIDATION: true
- name: Start container and measure memory
run: |
docker run -d --name homarr-mem-test -p 17575:7575 homarr-memory-test
# Wait for container to be healthy (up to 60s)
healthy=false
for i in $(seq 1 30); do
if curl -sf http://localhost:17575/api/health 2>/dev/null; then
echo "Container healthy after $((i * 2))s"
healthy=true
break
fi
sleep 2
done
if [ "$healthy" != "true" ]; then
echo "ERROR: Container never became healthy"
docker logs homarr-mem-test --tail 50
exit 1
fi
# Warm up: hit the dashboard page a few times
for i in $(seq 1 5); do
curl -sf http://localhost:17575/ > /dev/null 2>&1 || true
sleep 2
done
# Measure RSS after warmup
MEM_KB=$(docker stats --no-stream --format '{{.MemUsage}}' homarr-mem-test | sed 's| /.*||')
echo "Raw memory: $MEM_KB"
# Convert to numeric MB (handles GiB and MiB suffixes)
MEM_NUM=$(echo "$MEM_KB" | awk '{
if ($0 ~ /GiB/) { gsub(/GiB/,""); print $1 * 1024 }
else if ($0 ~ /MiB/) { gsub(/MiB/,""); print $1 }
else if ($0 ~ /KiB/) { gsub(/KiB/,""); print $1 / 1024 }
else { print $1 / 1048576 }
}')
echo "Memory usage: ${MEM_NUM} MB"
MAX_MB=700
if awk "BEGIN {exit !($MEM_NUM > $MAX_MB)}"; then
echo "ERROR: Memory usage ${MEM_NUM} MB exceeds ${MAX_MB} MB threshold"
docker logs homarr-mem-test --tail 50
exit 1
fi
echo "Memory check passed: ${MEM_NUM} MB <= ${MAX_MB} MB"
@@ -66,7 +66,7 @@ jobs:
- uses: actions/setup-node@v6
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.17.0
node-version: 24.18.0
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
+1 -1
View File
@@ -1,2 +1,2 @@
[tools]
node = "24.17.0"
node = "24.18.0"
+1 -1
View File
@@ -1 +1 @@
24.17.0
24.18.0
+7
View File
@@ -1,3 +1,10 @@
## [1.68.1](https://github.com/homarr-labs/homarr/compare/v1.68.0...v1.68.1) (2026-06-30)
### Bug Fixes
* make source maps opt-in to restore v1.67.0 memory baseline ([57ef0af](https://github.com/homarr-labs/homarr/commit/57ef0af4a252ea39727caeba7e13587dabc6254e)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
* revert production source maps to restore v1.67.0 memory baseline ([60a69e3](https://github.com/homarr-labs/homarr/commit/60a69e31efc7168b8f4309daf75954d7730d883b)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
## [1.68.0](https://github.com/homarr-labs/homarr/compare/v1.67.0...v1.68.0) (2026-06-26)
### Features
+1 -15
View File
@@ -1,4 +1,4 @@
FROM node:24.17.0-alpine AS base
FROM node:24.18.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -20,17 +20,6 @@ ARG DISABLE_REDIS_LOGS='true'
RUN corepack enable pnpm && pnpm build
# The standalone output tracing omits server chunk source maps (.next/server/**/*.map), which is
# where production Node stack frames point. Copy the .map files into the standalone tree (preserving
# directory structure) so the existing standalone COPY ships them, keeping traces readable.
# See https://github.com/homarr-labs/homarr/issues/5891
RUN cd apps/nextjs/.next/server && \
find . -name '*.map' | while IFS= read -r map; do \
dest="../standalone/apps/nextjs/.next/server/$map"; \
mkdir -p "$(dirname "$dest")"; \
cp "$map" "$dest"; \
done
FROM base AS runner
WORKDIR /app
@@ -75,9 +64,6 @@ ENV DB_DRIVER='better-sqlite3'
ENV AUTH_PROVIDERS='credentials'
ENV REDIS_IS_EXTERNAL='false'
ENV NODE_ENV='production'
# Tell Node to symbolicate stack traces using the shipped .map files so production logs
# show original source locations instead of minified frames, see https://github.com/homarr-labs/homarr/issues/5891
ENV NODE_OPTIONS='--enable-source-maps'
ENTRYPOINT [ "/app/entrypoint.sh" ]
CMD ["sh", "run.sh"]
@@ -2,9 +2,9 @@
<img src="https://raw.githubusercontent.com/homarr-labs/charts/refs/heads/main/charts/homarr/icon.svg" align="right" width="92" alt="homarr logo">
![Version: 8.19.0](https://img.shields.io/badge/Version-8.19.0-informational?style=flat)
![Version: 8.20.0](https://img.shields.io/badge/Version-8.20.0-informational?style=flat)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat)
![AppVersion: v1.60.0](https://img.shields.io/badge/AppVersion-v1.60.0-informational?style=flat)
![AppVersion: v1.68.0](https://img.shields.io/badge/AppVersion-v1.68.0-informational?style=flat)
A Helm chart to deploy homarr for Kubernetes
@@ -383,6 +383,7 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
| containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. |
| database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). |
| database.type | string | `"sqlite"` | Database type: sqlite, mysql or postgresql |
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
| env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server |
| env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups |
| env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups |
@@ -395,7 +396,6 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
| env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) |
| env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field |
| env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. |
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
| env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login |
| env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) |
| env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim |
-4
View File
@@ -31,8 +31,6 @@ const nextConfig: NextConfig = {
},
output: "standalone",
reactStrictMode: true,
// Ship browser source maps so production client stack traces are readable, see https://github.com/homarr-labs/homarr/issues/5891
productionBrowserSourceMaps: true,
// react compiler breaks mantine-react-table, so disabled for now
//reactCompiler: true,
/** We already do typechecking as separate tasks in CI */
@@ -43,8 +41,6 @@ const nextConfig: NextConfig = {
*/
serverExternalPackages: ["dockerode", "isomorphic-dompurify", "jsdom", "better-sqlite3"],
experimental: {
// Ship server source maps so production Node stack traces in logs reference original source, see https://github.com/homarr-labs/homarr/issues/5891
serverSourceMaps: true,
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
@@ -1,3 +1,5 @@
import type { MutableRefObject } from "react";
import { useRef } from "react";
import { Badge, Card } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { QueryErrorResetBoundary } from "@tanstack/react-query";
@@ -16,6 +18,7 @@ import classes from "../sections/item.module.css";
import { useItemActions } from "./item-actions";
import itemContentClasses from "./item-content.module.css";
import { BoardItemMenu } from "./item-menu";
import { WidgetContextMenu } from "./widget-context-menu";
interface BoardItemContentProps {
item: SectionItem;
@@ -30,30 +33,33 @@ const getOverflowFromKind = (kind: SectionItem["kind"]) => {
export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const { ref, width, height } = useElementSize<HTMLDivElement>();
const board = useRequiredBoard();
const widgetStateRef = useRef<Record<string, unknown> | null>(null);
return (
<>
<Card
ref={ref}
className={combineClasses(
classes.itemCard,
`${item.kind}-wrapper`,
"grid-stack-item-content",
item.advancedOptions.customCssClasses.join(" "),
)}
radius={board.itemRadius}
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: getOverflowFromKind(item.kind),
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} />
</Card>
<WidgetContextMenu item={item} widgetStateRef={widgetStateRef}>
<Card
ref={ref}
className={combineClasses(
classes.itemCard,
`${item.kind}-wrapper`,
"grid-stack-item-content",
item.advancedOptions.customCssClasses.join(" "),
)}
radius={board.itemRadius}
styles={{
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: getOverflowFromKind(item.kind),
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
p={0}
>
<InnerContent item={item} width={width} height={height} widgetStateRef={widgetStateRef} />
</Card>
</WidgetContextMenu>
{item.advancedOptions.title?.trim() && (
<Badge
pos="absolute"
@@ -83,6 +89,7 @@ interface InnerContentProps {
item: SectionItem;
width: number;
height: number;
widgetStateRef: MutableRefObject<Record<string, unknown> | null>;
}
const InnerContent = ({ item, ...dimensions }: InnerContentProps) => {
@@ -0,0 +1,250 @@
"use client";
import type { MutableRefObject, ReactNode } from "react";
import { useCallback, useMemo } from "react";
import { Group, Menu, Switch } from "@mantine/core";
import { IconCopy, IconLayoutKanban, IconSettings, IconTrash } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings";
import { translateIfNecessary } from "@homarr/translation";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { WidgetContextMenuAction } from "@homarr/widgets";
import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets";
import { WidgetEditModal } from "@homarr/widgets/modals";
import type { SectionItem } from "~/app/[locale]/boards/_types";
import { useSectionContext } from "../sections/section-context";
import { useItemActions } from "./item-actions";
import { ItemMoveModal } from "./item-move-modal";
interface WidgetContextMenuProps {
item: SectionItem;
widgetStateRef: MutableRefObject<Record<string, unknown> | null>;
children: ReactNode;
}
export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetContextMenuProps) => {
const { data: session } = useSession();
const [isEditMode] = useEditMode();
const board = useRequiredBoard();
const tItem = useScopedI18n("item");
const tMenu = useScopedI18n("item.menu.label");
const t = useI18n();
const settings = useSettings();
const { openModal } = useModalAction(WidgetEditModal);
const { openModal: openMoveModal } = useModalAction(ItemMoveModal);
const { openConfirmModal } = useConfirmModal();
const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, duplicateItem, removeItem } =
useItemActions();
const { data: integrationData, isPending } = clientApi.integration.all.useQuery();
const { mutate: saveBoard } = clientApi.board.saveBoard.useMutation();
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
const { gridstack } = useSectionContext().refs;
const options = useMemo(
() => reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options) as Record<string, unknown>,
[item.kind, settings, item.options],
);
type OptionDef = { type: string; skipContextMenu?: boolean };
const toggleOptions = useMemo(() => {
const rawOptions = currentDefinition.createOptions(settings) as unknown as Record<string, OptionDef>;
return Object.entries(rawOptions).filter(([, def]) => def.type === "switch" && !def.skipContextMenu);
}, [currentDefinition, settings]);
const widgetContextActions = useMemo(() => {
const def = currentDefinition as Record<string, unknown>;
if (typeof def.contextActions !== "function") return [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const actions = (def.contextActions as any)({
options,
setOptions: (partial: Record<string, unknown>) => {
updateItemOptions({ itemId: item.id, newOptions: { ...options, ...partial } });
},
integrationIds: item.integrationIds,
context: { isEditMode, boardId: board.id, itemId: item.id },
widgetStateRef,
});
return (Array.isArray(actions) ? actions : []) as WidgetContextMenuAction[];
}, [currentDefinition, options, item, updateItemOptions, isEditMode, board.id, widgetStateRef]);
const persistBoard = useCallback(
(updatedItems: typeof board.items) => {
saveBoard({ ...board, items: updatedItems });
},
[board, saveBoard],
);
const openEditModal = useCallback(() => {
openModal(
{
kind: item.kind,
value: {
advancedOptions: item.advancedOptions,
options: item.options,
integrationIds: item.integrationIds,
},
onSuccessfulEdit: (editResult) => {
updateItemOptions({ itemId: item.id, newOptions: editResult.options });
updateItemAdvancedOptions({ itemId: item.id, newAdvancedOptions: editResult.advancedOptions });
updateItemIntegrations({ itemId: item.id, newIntegrations: editResult.integrationIds });
if (!isEditMode) {
persistBoard(
board.items.map((boardItem) =>
boardItem.id !== item.id
? boardItem
: {
...boardItem,
options: editResult.options,
advancedOptions: editResult.advancedOptions,
integrationIds: editResult.integrationIds,
},
),
);
}
},
integrationData: (integrationData ?? []).filter(
(integration) =>
"supportedIntegrations" in currentDefinition &&
(currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind),
),
integrationSupport: "supportedIntegrations" in currentDefinition,
settings,
appId: item.kind === "app" ? (item.options.appId as string | undefined) : undefined,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{ title: (fn: any) => `${fn("item.edit.title")} - ${fn(`widget.${item.kind}.name`)}` },
);
}, [
openModal,
item,
updateItemOptions,
updateItemAdvancedOptions,
updateItemIntegrations,
integrationData,
currentDefinition,
settings,
isEditMode,
persistBoard,
board,
]);
const handleToggle = useCallback(
(key: string) => (checked: boolean) => {
const newOptions = { ...options, [key]: checked };
updateItemOptions({ itemId: item.id, newOptions });
persistBoard(
board.items.map((boardItem) => (boardItem.id !== item.id ? boardItem : { ...boardItem, options: newOptions })),
);
},
[options, item.id, updateItemOptions, persistBoard, board],
);
if (!session) return <>{children}</>;
const visibleWidgetActions = widgetContextActions.filter((a) => !a.hidden);
return (
<Menu shadow="md" width={300} closeOnItemClick={false} position="right-start" offset={4}>
<Menu.ContextMenu>{children}</Menu.ContextMenu>
<Menu.Dropdown>
{toggleOptions.length > 0 && (
<>
<Menu.Label>{tMenu("options")}</Menu.Label>
{toggleOptions.map(([key]) => (
<Menu.Item key={key} onClick={() => handleToggle(key)(!options[key])}>
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
<Group wrap="nowrap">
{String(translateIfNecessary(t, ((fn: any) => fn(`widget.${item.kind}.option.${key}.label`)) as any))}
<Switch size="xs" checked={Boolean(options[key])} readOnly tabIndex={-1} ml="auto" />
</Group>
</Menu.Item>
))}
</>
)}
{visibleWidgetActions.length > 0 && (
<>
{toggleOptions.length > 0 && <Menu.Divider />}
<Menu.Label>{tMenu("actions")}</Menu.Label>
{visibleWidgetActions.map((action) => {
const Icon = action.icon;
return (
<Menu.Item
key={action.key}
closeMenuOnClick
leftSection={Icon ? <Icon size={16} /> : undefined}
onClick={action.onClick}
disabled={action.disabled}
color={action.color}
>
{String(translateIfNecessary(t, action.label))}
</Menu.Item>
);
})}
</>
)}
{isEditMode && (
<>
{(toggleOptions.length > 0 || visibleWidgetActions.length > 0) && <Menu.Divider />}
<Menu.Item
closeMenuOnClick
leftSection={<IconLayoutKanban size={16} />}
onClick={() => {
if (!gridstack.current) return;
openMoveModal({ item, columnCount: gridstack.current.getColumn(), gridStack: gridstack.current });
}}
>
{tItem("action.moveResize")}
</Menu.Item>
<Menu.Item
closeMenuOnClick
leftSection={<IconCopy size={16} />}
onClick={() => duplicateItem({ itemId: item.id })}
>
{tItem("action.duplicate")}
</Menu.Item>
</>
)}
<>
{!isEditMode && (toggleOptions.length > 0 || visibleWidgetActions.length > 0) && <Menu.Divider />}
<Menu.Item
closeMenuOnClick
leftSection={<IconSettings size={16} />}
onClick={openEditModal}
disabled={isPending}
>
{tMenu("settings")}
</Menu.Item>
</>
{isEditMode && (
<>
<Menu.Divider />
<Menu.Item
closeMenuOnClick
color="red"
leftSection={<IconTrash size={16} />}
onClick={() => {
openConfirmModal({
title: tItem("remove.title"),
children: tItem("remove.message"),
onConfirm: () => removeItem({ itemId: item.id }),
});
}}
>
{tItem("action.remove")}
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
);
};
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24.17.0-trixie AS base
FROM node:24.18.0-trixie AS base
WORKDIR /app
COPY package.json .
COPY pnpm-lock.yaml .
+1 -1
View File
@@ -5,7 +5,7 @@
"better-sqlite3": "^12.10.0"
},
"engines": {
"node": ">=24.17.0",
"node": ">=24.18.0",
"pnpm": ">=11.9.0"
},
"packageManager": "pnpm@11.9.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.68.0",
"version": "1.68.1",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -61,7 +61,7 @@
"vitest": "catalog:"
},
"engines": {
"node": ">=24.17.0",
"node": ">=24.18.0",
"pnpm": ">=11.9.0"
},
"packageManager": "pnpm@11.9.0",
@@ -0,0 +1,27 @@
import { z } from "zod/v4";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const dawarichRouter = createTRPCRouter({
getStatistics: publicProcedure.concat(createOneIntegrationMiddleware("query", "dawarich")).query(async ({ ctx }) => {
const { dawarichStatisticsRequestHandler } = await import("@homarr/request-handler/dawarich");
const innerHandler = dawarichStatisticsRequestHandler.handler(ctx.integration, {});
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data.data;
}),
getPlaces: publicProcedure
.concat(createOneIntegrationMiddleware("query", "dawarich"))
.input(
z.object({
limit: z.number().min(1).max(50).default(10),
}),
)
.query(async ({ ctx, input }) => {
const { dawarichPlacesRequestHandler } = await import("@homarr/request-handler/dawarich");
const innerHandler = dawarichPlacesRequestHandler.handler(ctx.integration, {});
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data.slice(0, input.limit);
}),
});
+1
View File
@@ -39,4 +39,5 @@ export const widgetRouter = createTRPCRouter({
vpn: lazy(() => import("./vpn").then((mod) => mod.vpnRouter)),
ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)),
customApi: lazy(() => import("./custom-api").then((mod) => mod.customApiRouter)),
dawarich: lazy(() => import("./dawarich").then((mod) => mod.dawarichRouter)),
});
@@ -343,6 +343,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/gluetun"
| "/docs/integrations/peanut"
| "/docs/integrations/beszel"
| "/docs/integrations/dawarich"
| "/docs/widgets/clock"
| "/docs/widgets/weather"
| "/docs/widgets/app"
@@ -59,5 +59,6 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
peaNut: "peanut",
beszel: "beszel",
archiveTeamWarrior: null,
dawarich: "dawarich",
mock: null,
};
@@ -51,4 +51,6 @@ export const widgetDocSlugs: Record<WidgetKind, string | null> = {
beszelAlerts: "beszel-alerts",
beszelSystemStats: "beszel-system-stats",
customApi: "custom-api",
dawarichStatistics: null,
dawarichPlaces: null,
};
+8
View File
@@ -504,6 +504,13 @@ export const integrationDefs = {
defaultUrl: "http://localhost:8001",
documentationUrl: createDocumentationLink("/docs/integrations/archiveteam-warrior"),
},
dawarich: {
name: "Dawarch",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/dawarich.png",
category: ["geolocationTracking"],
documentationUrl: createDocumentationLink("/docs/integrations/dawarich"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: {
name: "Mock",
@@ -617,6 +624,7 @@ export const integrationCategories = [
"documents",
"mediaLibrary",
"uptimeMonitoring",
"geolocationTracking",
] as const;
export type IntegrationCategory = (typeof integrationCategories)[number];
@@ -40,6 +40,8 @@ export const widgetIntegrationSupport: Partial<Record<WidgetKind, readonly Integ
audioStats: ["navidrome", "audiobookshelf"],
vpn: getIntegrationKindsByCategory("vpn"),
anchorNote: ["anchor"],
dawarichStatistics: ["dawarich"],
dawarichPlaces: ["dawarich"],
};
export const getWidgetKindsForIntegration = (integrationKind: IntegrationKind): WidgetKind[] => {
+2
View File
@@ -49,6 +49,8 @@ export const widgetKinds = [
"beszelAlerts",
"beszelSystemStats",
"customApi",
"dawarichStatistics",
"dawarichPlaces",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
@@ -60,6 +60,7 @@ import { UptimeKumaIntegration } from "../uptime-kuma/uptime-kuma-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import { UnraidIntegration } from "../unraid/unraid-integration";
import type { Integration, IntegrationInput } from "./integration";
import { DawarichIntegration } from "../dawarich/dawarich-integration";
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
integration: IntegrationInput & { kind: TKind },
@@ -143,6 +144,7 @@ export const integrationCreators = {
uptimeKuma: UptimeKumaIntegration,
peaNut: PeaNutIntegration,
beszel: BeszelIntegration,
dawarich: DawarichIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
@@ -0,0 +1,52 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { dawarichPlacesSchema, dawarichStatisticsSchema } from "./dawarich-schemas";
import type { z } from "zod/v4";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
export class DawarichIntegration extends Integration {
constructor(integration: IntegrationInput) {
super(integration);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/users/me`), {
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
});
if (response.status === 200) {
return { success: true };
}
return TestConnectionError.StatusResult(response);
}
async getStatisticsAsync(): Promise<{
totalDistanceKm: number;
totalPointsTracked: number;
totalReverseGeocodedPoints: number;
totalCountriesVisited: number;
totalCitiesVisited: number;
}> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/stats"), {
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
});
if (!response.ok) throw new ResponseError(response);
return dawarichStatisticsSchema.parseAsync(await response.json());
}
async getPlacesAsync(): Promise<z.infer<typeof dawarichPlacesSchema>> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/places"), {
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
});
if (!response.ok) throw new ResponseError(response);
return dawarichPlacesSchema.parseAsync(await response.json());
}
}
@@ -0,0 +1,31 @@
import { z } from "zod/v4";
export const dawarichStatisticsSchema = z.object({
totalDistanceKm: z.number(),
totalPointsTracked: z.number(),
totalReverseGeocodedPoints: z.number(),
totalCountriesVisited: z.number(),
totalCitiesVisited: z.number(),
});
export const dawarichPlaceTagSchema = z.object({
id: z.number(),
name: z.string(),
icon: z.string(),
color: z.string(),
});
export const dawarichPlaceSchema = z.object({
id: z.number(),
name: z.string(),
latitude: z.number(),
longitude: z.number(),
source: z.string().optional(),
icon: z.string().nullable(),
color: z.string().nullable(),
visits_count: z.number(),
created_at: z.string(),
tags: z.array(dawarichPlaceTagSchema),
});
export const dawarichPlacesSchema = z.array(dawarichPlaceSchema);
@@ -0,0 +1,5 @@
import type { z } from "zod/v4";
import type { dawarichPlacesSchema, dawarichStatisticsSchema } from "./dawarich-schemas";
export type DawarichStatistics = z.infer<typeof dawarichStatisticsSchema>;
export type DawarichPlace = z.infer<typeof dawarichPlacesSchema>[number];
+2
View File
@@ -41,6 +41,7 @@ export { NavidromeIntegration } from "./navidrome/navidrome-integration";
export { UptimeKumaIntegration } from "./uptime-kuma/uptime-kuma-integration";
export { UmamiIntegration } from "./umami/umami-integration";
export { PeaNutIntegration } from "./peanut/peanut-integration";
export { DawarichIntegration } from "./dawarich/dawarich-integration";
// Types
export type { IntegrationInput } from "./base/integration";
@@ -86,6 +87,7 @@ export type { AudiobookshelfDashboardData } from "./audiobookshelf/audiobookshel
export type { NavidromeDashboardData } from "./navidrome/navidrome-types";
export type { UptimeKumaDashboardData } from "./uptime-kuma/uptime-kuma-types";
export type { UmamiVisitorStats } from "./umami/umami-types";
export type { DawarichStatistics, DawarichPlace } from "./dawarich/dawarich-types";
// Schemas
export { anchorNotesListInputSchema } from "./anchor/anchor-types";
+1
View File
@@ -25,3 +25,4 @@ export * from "./paperless-ngx/paperless-ngx-types";
export * from "./umami/umami-types";
export * from "./gluetun/gluetun-types";
export * from "./beszel/beszel-types";
export * from "./dawarich/dawarich-types";
+32
View File
@@ -0,0 +1,32 @@
import dayjs from "dayjs";
import { createIntegrationAsync } from "@homarr/integrations";
import type { DawarichPlace, DawarichStatistics } from "@homarr/integrations";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const dawarichStatisticsRequestHandler = createCachedIntegrationRequestHandler<
DawarichStatistics,
"dawarich",
Record<string, never>
>({
async requestAsync(integration) {
const instance = await createIntegrationAsync(integration);
return await instance.getStatisticsAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "dawarichStatistics",
});
export const dawarichPlacesRequestHandler = createCachedIntegrationRequestHandler<
DawarichPlace[],
"dawarich",
Record<string, never>
>({
async requestAsync(integration) {
const instance = await createIntegrationAsync(integration);
return await instance.getPlacesAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "dawarichPlaces",
});
+40 -2
View File
@@ -1598,7 +1598,9 @@
},
"menu": {
"label": {
"settings": "Settings"
"settings": "Settings",
"options": "Options",
"actions": "Actions"
}
},
"create": {
@@ -3543,6 +3545,42 @@
"executeSuccess": "Action executed successfully",
"executeFailed": "Action failed. Check the widget configuration."
},
"dawarichStatistics": {
"name": "Dawarich Statistics",
"description": "Display travel statistics from Dawarich",
"option": {
"showTotalDistance": {
"label": "Show total distance"
},
"showTotalPoints": {
"label": "Show total points tracked"
},
"showReverseGeocoded": {
"label": "Show reverse geocoded points"
},
"showCountries": {
"label": "Show countries visited"
},
"showCities": {
"label": "Show cities visited"
}
},
"totalDistance": "Total distance",
"totalPoints": "Points tracked",
"reverseGeocoded": "Reverse geocoded",
"countries": "Countries",
"cities": "Cities"
},
"dawarichPlaces": {
"name": "Dawarich Places",
"description": "Display recently visited places from Dawarich",
"option": {
"maxPlaces": {
"label": "Max places to show"
}
},
"visits": "visits"
},
"beszelSystemTable": {
"name": "Beszel Systems (Table)",
"description": "Table view of all Beszel-monitored systems with sortable columns and status indicators",
@@ -4311,7 +4349,7 @@
"notStarted": "Not started"
},
"name": {
"management": "Management",
"management": "Management",
"dashboard": "Dashboard"
}
}
@@ -0,0 +1,10 @@
.placeItem {
padding: 8px 12px;
border-radius: var(--mantine-radius-sm);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
transition: all 0.2s ease;
&:hover {
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
@@ -0,0 +1,40 @@
"use client";
import { Anchor, Group, Stack, Text } from "@mantine/core";
import { IconMapPin } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.css";
export default function DawarichPlacesWidget({ integrationIds, options }: WidgetComponentProps<"dawarichPlaces">) {
const t = useI18n();
const { data: places } = clientApi.widget.dawarich.getPlaces.useQuery(
{
integrationId: integrationIds[0] ?? "",
limit: options.maxPlaces,
},
{ staleTime: 5 * 60 * 1000 },
);
if (!places || places.length === 0) return <WidgetEmptyState />;
return (
<Stack gap="xs" h="100%" p="md">
{places.map((place) => (
<Group key={place.id} gap="sm" align="center" className={classes.placeItem}>
<IconMapPin size={16} />
<Text size="sm" fw={500} style={{ flex: 1 }}>
{place.name}
</Text>
<Text size="xs" c="dimmed">
{place.visits_count} {t("widget.dawarichPlaces.visits")}
</Text>
</Group>
))}
</Stack>
);
}
@@ -0,0 +1,16 @@
import { IconMapPin } from "@tabler/icons-react";
import z from "zod";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("dawarichPlaces", {
icon: IconMapPin,
supportedIntegrations: ["dawarich"],
integrationsRequired: true,
createOptions() {
return optionsBuilder.from((factory) => ({
maxPlaces: factory.number({ defaultValue: 10, validate: z.number().min(1).max(50) }),
}));
},
}).withDynamicImport(() => import("./component"));
@@ -0,0 +1,10 @@
.statItem {
padding: 12px;
border-radius: var(--mantine-radius-md);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
transition: all 0.2s ease;
&:hover {
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
@@ -0,0 +1,88 @@
"use client";
import { Group, Stack, Text } from "@mantine/core";
import { IconMap, IconMapPin, IconRoute, IconWorld, IconBuildingSkyscraper } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.css";
export default function DawarichStatisticsWidget({
integrationIds,
options,
}: WidgetComponentProps<"dawarichStatistics">) {
const t = useI18n();
const { data: stats } = clientApi.widget.dawarich.getStatistics.useQuery(
{
integrationId: integrationIds[0] ?? "",
},
{ staleTime: 5 * 60 * 1000 },
);
if (!stats) return <WidgetEmptyState />;
return (
<Stack gap="md" h="100%" p="md">
{options.showTotalDistance && (
<StatItem
icon={<IconRoute size={20} />}
label={t("widget.dawarichStatistics.totalDistance")}
value={`${stats.totalDistanceKm.toLocaleString(undefined, { maximumFractionDigits: 1 })} km`}
/>
)}
{options.showTotalPoints && (
<StatItem
icon={<IconMap size={20} />}
label={t("widget.dawarichStatistics.totalPoints")}
value={stats.totalPointsTracked.toLocaleString()}
/>
)}
{options.showReverseGeocoded && (
<StatItem
icon={<IconMapPin size={20} />}
label={t("widget.dawarichStatistics.reverseGeocoded")}
value={stats.totalReverseGeocodedPoints.toLocaleString()}
/>
)}
{options.showCountries && (
<StatItem
icon={<IconWorld size={20} />}
label={t("widget.dawarichStatistics.countries")}
value={stats.totalCountriesVisited.toLocaleString()}
/>
)}
{options.showCities && (
<StatItem
icon={<IconBuildingSkyscraper size={20} />}
label={t("widget.dawarichStatistics.cities")}
value={stats.totalCitiesVisited.toLocaleString()}
/>
)}
</Stack>
);
}
interface StatItemProps {
icon: React.ReactNode;
label: string;
value: string | number;
}
function StatItem({ icon, label, value }: StatItemProps) {
return (
<Group justify="space-between" align="center" className={classes.statItem}>
<Group gap="sm" align="center">
{icon}
<Text size="sm" fw={500}>
{label}
</Text>
</Group>
<Text size="sm" fw={700} c="var(--mantine-primary-color)">
{value}
</Text>
</Group>
);
}
@@ -0,0 +1,19 @@
import { IconChartBar } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("dawarichStatistics", {
icon: IconChartBar,
supportedIntegrations: ["dawarich"],
integrationsRequired: true,
createOptions() {
return optionsBuilder.from((factory) => ({
showTotalDistance: factory.switch({ defaultValue: true }),
showTotalPoints: factory.switch({ defaultValue: true }),
showReverseGeocoded: factory.switch({ defaultValue: true }),
showCountries: factory.switch({ defaultValue: true }),
showCities: factory.switch({ defaultValue: true }),
}));
},
}).withDynamicImport(() => import("./component"));
+32 -1
View File
@@ -1,3 +1,4 @@
import type React from "react";
import type { LoaderComponent } from "next/dynamic";
import type { QueryClient } from "@tanstack/react-query";
import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import";
@@ -9,7 +10,35 @@ import type { stringOrTranslation } from "@homarr/translation";
import type { TablerIcon } from "@homarr/ui";
import type { WidgetImports } from ".";
import type { inferOptionsFromCreator, WidgetOptionsRecord } from "./options";
import type { inferOptionsFromCreator, inferOptionsFromDefinition, WidgetOptionsRecord } from "./options";
export interface WidgetContextMenuAction {
key: string;
label: stringOrTranslation;
icon?: TablerIcon;
onClick: () => void;
hidden?: boolean;
disabled?: boolean;
color?: string;
}
export interface WidgetContextMenuContext {
isEditMode: boolean;
boardId: string | undefined;
itemId: string | undefined;
}
export interface WidgetContextActionProps<
TKind extends WidgetKind,
TOptions extends WidgetOptionsRecord = WidgetOptionsRecord,
> {
kind: TKind;
options: inferOptionsFromDefinition<TOptions>;
setOptions: (partial: Partial<inferOptionsFromDefinition<TOptions>>) => void;
integrationIds: string[];
context: WidgetContextMenuContext;
widgetStateRef: React.MutableRefObject<Record<string, unknown> | null>;
}
const createWithDynamicImport =
<TKind extends WidgetKind, TDefinition extends WidgetDefinition>(kind: TKind, definition: TDefinition) =>
@@ -56,6 +85,7 @@ export interface WidgetDefinition {
}
>
>;
contextActions?: (props: Omit<WidgetContextActionProps<WidgetKind>, "kind">) => WidgetContextMenuAction[];
}
export interface WidgetProps<TKind extends WidgetKind> {
@@ -70,6 +100,7 @@ export type WidgetComponentProps<TKind extends WidgetKind> = WidgetProps<TKind>
setOptions: ({ newOptions }: { newOptions: Partial<inferOptionsFromCreator<WidgetOptionsRecordOf<TKind>>> }) => void;
width: number;
height: number;
widgetStateRef?: React.MutableRefObject<Record<string, unknown> | null>;
};
export type WidgetOptionsRecordOf<TKind extends WidgetKind> = WidgetImports[TKind]["definition"]["createOptions"];
+10 -1
View File
@@ -60,8 +60,15 @@ import * as beszelSystemStats from "./beszel-system-stats";
import * as video from "./video";
import * as weather from "./weather";
import * as customApi from "./custom-api";
import * as dawarichStatistics from "./dawarich-statistics";
import * as dawarichPlaces from "./dawarich-places";
export type { WidgetDefinition, WidgetOptionsSettings } from "./definition";
export type {
WidgetDefinition,
WidgetContextMenuAction,
WidgetContextActionProps,
WidgetOptionsSettings,
} from "./definition";
export type { WidgetComponentProps };
export type { WidgetOptionDefinition, WidgetOptionType } from "./options";
@@ -116,6 +123,8 @@ export const widgetImports = {
beszelAlerts,
beszelSystemStats,
customApi,
dawarichStatistics,
dawarichPlaces,
} satisfies WidgetImportRecord;
export type WidgetImports = typeof widgetImports;
@@ -1,6 +1,7 @@
import { IconZoomQuestion } from "@tabler/icons-react";
import { IconSearch, IconZoomQuestion } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { openMediaRequestSearch } from "@homarr/spotlight";
import { createWidgetDefinition } from "../../definition";
import { optionsBuilder } from "../../options";
@@ -14,5 +15,15 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaRequ
}),
}));
},
contextActions: ({ integrationIds }) => [
{
key: "search",
label: (t) => t("search.mode.media.action.search.label"),
icon: IconSearch,
onClick: () => {
openMediaRequestSearch({ integrationIds });
},
},
],
supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"),
}).withDynamicImport(() => import("./component"));
@@ -1,6 +1,7 @@
import { IconChartBar } from "@tabler/icons-react";
import { IconChartBar, IconSearch } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { openMediaRequestSearch } from "@homarr/spotlight";
import { createWidgetDefinition } from "../../definition";
@@ -9,5 +10,15 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaRequ
createOptions() {
return {};
},
contextActions: ({ integrationIds }) => [
{
key: "search",
label: (t) => t("search.mode.media.action.search.label"),
icon: IconSearch,
onClick: () => {
openMediaRequestSearch({ integrationIds });
},
},
],
supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"),
}).withDynamicImport(() => import("./component"));
+2
View File
@@ -13,6 +13,7 @@ import type { ReleasesRepository } from "./releases/releases-repository";
interface CommonInput<TType> {
defaultValue?: TType;
withDescription?: boolean;
skipContextMenu?: boolean;
}
interface TextInput extends CommonInput<string> {
@@ -91,6 +92,7 @@ const optionsFactory = {
type: "switch" as const,
defaultValue: input?.defaultValue ?? false,
withDescription: input?.withDescription ?? false,
skipContextMenu: input?.skipContextMenu ?? false,
}),
text: (input?: TextInput) => ({
type: "text" as const,
+29 -14
View File
@@ -337,8 +337,8 @@ catalogs:
specifier: ^0.2.9
version: 0.2.9
dockerode:
specifier: ^5.0.0
version: 5.0.0
specifier: ^5.0.1
version: 5.0.1
dotenv:
specifier: ^17.4.2
version: 17.4.2
@@ -445,8 +445,8 @@ catalogs:
specifier: ^1.18.0
version: 1.18.0
posthog-js:
specifier: ^1.393.4
version: 1.393.4
specifier: ^1.393.5
version: 1.395.0
posthog-node:
specifier: ^4.18.0
version: 4.18.0
@@ -1012,7 +1012,7 @@ importers:
version: 1.18.0(postcss@8.5.15)
posthog-js:
specifier: 'catalog:'
version: 1.393.4
version: 1.395.0
prismjs:
specifier: 'catalog:'
version: 1.30.0
@@ -1771,7 +1771,7 @@ importers:
version: link:../core
dockerode:
specifier: 'catalog:'
version: 5.0.0
version: 5.0.1
devDependencies:
'@homarr/tsconfig':
specifier: workspace:^0.1.0
@@ -6418,8 +6418,8 @@ packages:
'@polka/url@1.0.0-next.25':
resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==}
'@posthog/core@1.37.3':
resolution: {integrity: sha512-Dvw4CTlRVH4K5ag/B8YIHgG4E27w43MCUsXpn1iKQHIggIMN3Shq9whG4Omm3OvXPF+VikBTmpyMKUjckPxw+g==}
'@posthog/core@1.38.0':
resolution: {integrity: sha512-QLrJh0hMVpEJXHNyiJR9YhvIO5tzLDedw4UHdvB+ub4fXHMtHCA8H44qgl11HWR3ajpjxtJ8hs3HyOGVF3Zc1w==}
'@posthog/types@1.391.1':
resolution: {integrity: sha512-ASwd7Nf4pViqdYRYaNRyPYRVKWa1CcHUAUWR0XeQJLGdNnsWACBwe0sSieb/cHnKsRXjRwO/23KIY83lm/Ccpw==}
@@ -9678,6 +9678,10 @@ packages:
resolution: {integrity: sha512-C52mvJ+7lcyhWNfrzVfFsbTrBfy/ezE9FGEYLpu17FUeBcCkxERk9nN7uDl/478ynDiQ4U+5DbQC2vENHkVEtQ==}
engines: {node: '>= 14.17'}
dockerode@5.0.1:
resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==}
engines: {node: '>= 14.17'}
docusaurus-plugin-image-zoom@3.0.1:
resolution: {integrity: sha512-mQrqA99VpoMQJNbi02qkWAMVNC4+kwc6zLLMNzraHAJlwn+HrlUmZSEDcTwgn+H4herYNxHKxveE2WsYy73eGw==}
peerDependencies:
@@ -13116,8 +13120,8 @@ packages:
resolution: {integrity: sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==}
engines: {node: '>=10.15.1'}
posthog-js@1.393.4:
resolution: {integrity: sha512-mOBSVQczEfyLnW3aFRbLXZEuE0Rfbmhqm9UCnuLeMFUUz0uYTkbnyhl5+Uc+BLSGfyiXHLf3nm2BqxYC+G/PXA==}
posthog-js@1.395.0:
resolution: {integrity: sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==}
posthog-node@4.18.0:
resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==}
@@ -17951,7 +17955,7 @@ snapshots:
'@esbuild-kit/core-utils@3.3.2':
dependencies:
esbuild: 0.28.0
esbuild: 0.28.1
source-map-support: 0.5.21
'@esbuild-kit/esm-loader@2.6.5':
@@ -19897,7 +19901,7 @@ snapshots:
'@polka/url@1.0.0-next.25': {}
'@posthog/core@1.37.3':
'@posthog/core@1.38.0':
dependencies:
'@posthog/types': 1.391.1
@@ -23479,6 +23483,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
dockerode@5.0.1:
dependencies:
'@balena/dockerignore': 1.0.2
'@grpc/grpc-js': 1.12.5
'@grpc/proto-loader': 0.7.13
docker-modem: 5.0.7
protobufjs: 7.4.0
tar-fs: 2.1.4
transitivePeerDependencies:
- supports-color
docusaurus-plugin-image-zoom@3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)):
dependencies:
'@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3(@swc/helpers@0.5.15))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@swc/helpers@0.5.15)(esbuild@0.28.1)(postcss@8.5.15))(@rspack/core@1.7.11(@swc/helpers@0.5.15))(@swc/core@1.15.3(@swc/helpers@0.5.15))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
@@ -27405,9 +27420,9 @@ snapshots:
posthog-docusaurus@2.0.5: {}
posthog-js@1.393.4:
posthog-js@1.395.0:
dependencies:
'@posthog/core': 1.37.3
'@posthog/core': 1.38.0
'@posthog/types': 1.391.1
core-js: 3.49.0
dompurify: 3.4.11
+2 -2
View File
@@ -92,7 +92,7 @@ catalog:
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.393.4
posthog-js: ^1.393.5
posthog-node: ^4.18.0
"@vitejs/plugin-react": ^6.0.3
"@vitest/coverage-v8": ^4.1.9
@@ -118,7 +118,7 @@ catalog:
dayjs: ^1.11.21
deepmerge: 4.3.1
dns-caching: ^0.2.9
dockerode: ^5.0.0
dockerode: ^5.0.1
dotenv: ^17.4.2
dotenv-cli: ^11.0.0
drizzle-kit: ^0.31.10
+1 -1
View File
@@ -7,7 +7,7 @@ runs:
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
node-version: 24.17.0
node-version: 24.18.0
cache: "pnpm"
- shell: bash