Compare commits

..
Author SHA1 Message Date
Thomas 76ea2d3df6 Merge branch 'dev' into feat/widget-right-click-context-menu 2026-06-30 17:39:05 +02:00
Thomas Camlong 6c122c70e4 fix: use explicit React type imports instead of global namespace 2026-06-30 17:34:30 +02:00
ajnart aca0f56dfe style: auto-fix lint and format 2026-06-30 15:28:11 +00:00
Thomas Camlong 07ad643f4d 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.
2026-06-30 17:26:52 +02:00
ajnart f5b4625c8e style: auto-fix lint and format 2026-06-29 19:10:48 +00:00
ajnart ae8ad9785e chore: revert unrelated sitemap change from auto-fix 2026-06-29 21:09:18 +02:00
ajnart b72b70d2a3 style: auto-fix lint and format 2026-06-29 13:27:25 +00:00
ajnart 8d22df5123 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
2026-06-29 15:26:09 +02:00
ajnart 2f7bdbfe1c style: auto-fix lint and format 2026-06-29 13:12:33 +00:00
ajnart 1136b3a99b 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
2026-06-29 15:10:34 +02:00
ajnart bf26f17fcd style: auto-fix lint and format 2026-06-29 15:10:34 +02:00
ajnart 83b93e17da 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
2026-06-29 15:10:34 +02:00
Thomas 597b5fbc8d Merge pull request #6097 from homarr-labs/docs/update-helm-docs
Update Helm chart documentation
2026-06-27 13:24:44 +02:00
github-actions[bot] 5412577519 docs(helm): Update Helm chart documentation 2026-06-27 11:22:16 +00:00
36 changed files with 43 additions and 501 deletions

No files matched your search

-62
View File
@@ -163,65 +163,3 @@ 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.18.0
node-version: 24.17.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.18.0"
node = "24.17.0"
+1 -1
View File
@@ -1 +1 @@
24.18.0
24.17.0
-7
View File
@@ -1,10 +1,3 @@
## [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
+15 -1
View File
@@ -1,4 +1,4 @@
FROM node:24.18.0-alpine AS base
FROM node:24.17.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -20,6 +20,17 @@ 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
@@ -64,6 +75,9 @@ 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"]
+4
View File
@@ -31,6 +31,8 @@ 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 */
@@ -41,6 +43,8 @@ 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 -1
View File
@@ -1,4 +1,4 @@
FROM node:24.18.0-trixie AS base
FROM node:24.17.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.18.0",
"node": ">=24.17.0",
"pnpm": ">=11.9.0"
},
"packageManager": "pnpm@11.9.0"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.68.1",
"version": "1.68.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -61,7 +61,7 @@
"vitest": "catalog:"
},
"engines": {
"node": ">=24.18.0",
"node": ">=24.17.0",
"pnpm": ">=11.9.0"
},
"packageManager": "pnpm@11.9.0",
@@ -1,27 +0,0 @@
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,5 +39,4 @@ 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,7 +343,6 @@ 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,6 +59,5 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
peaNut: "peanut",
beszel: "beszel",
archiveTeamWarrior: null,
dawarich: "dawarich",
mock: null,
};
@@ -51,6 +51,4 @@ 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,13 +504,6 @@ 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",
@@ -624,7 +617,6 @@ export const integrationCategories = [
"documents",
"mediaLibrary",
"uptimeMonitoring",
"geolocationTracking",
] as const;
export type IntegrationCategory = (typeof integrationCategories)[number];
@@ -40,8 +40,6 @@ 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,8 +49,6 @@ export const widgetKinds = [
"beszelAlerts",
"beszelSystemStats",
"customApi",
"dawarichStatistics",
"dawarichPlaces",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
@@ -60,7 +60,6 @@ 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 },
@@ -144,7 +143,6 @@ 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> = {
@@ -1,52 +0,0 @@
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());
}
}
@@ -1,31 +0,0 @@
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);
@@ -1,5 +0,0 @@
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,7 +41,6 @@ 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";
@@ -87,7 +86,6 @@ 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,4 +25,3 @@ 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
@@ -1,32 +0,0 @@
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",
});
-36
View File
@@ -3545,42 +3545,6 @@
"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",
@@ -1,10 +0,0 @@
.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));
}
}
@@ -1,40 +0,0 @@
"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>
);
}
@@ -1,16 +0,0 @@
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"));
@@ -1,10 +0,0 @@
.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));
}
}
@@ -1,88 +0,0 @@
"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>
);
}
@@ -1,19 +0,0 @@
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"));
-4
View File
@@ -60,8 +60,6 @@ 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,
@@ -123,8 +121,6 @@ export const widgetImports = {
beszelAlerts,
beszelSystemStats,
customApi,
dawarichStatistics,
dawarichPlaces,
} satisfies WidgetImportRecord;
export type WidgetImports = typeof widgetImports;
+14 -29
View File
@@ -337,8 +337,8 @@ catalogs:
specifier: ^0.2.9
version: 0.2.9
dockerode:
specifier: ^5.0.1
version: 5.0.1
specifier: ^5.0.0
version: 5.0.0
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.5
version: 1.395.0
specifier: ^1.393.4
version: 1.393.4
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.395.0
version: 1.393.4
prismjs:
specifier: 'catalog:'
version: 1.30.0
@@ -1771,7 +1771,7 @@ importers:
version: link:../core
dockerode:
specifier: 'catalog:'
version: 5.0.1
version: 5.0.0
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.38.0':
resolution: {integrity: sha512-QLrJh0hMVpEJXHNyiJR9YhvIO5tzLDedw4UHdvB+ub4fXHMtHCA8H44qgl11HWR3ajpjxtJ8hs3HyOGVF3Zc1w==}
'@posthog/core@1.37.3':
resolution: {integrity: sha512-Dvw4CTlRVH4K5ag/B8YIHgG4E27w43MCUsXpn1iKQHIggIMN3Shq9whG4Omm3OvXPF+VikBTmpyMKUjckPxw+g==}
'@posthog/types@1.391.1':
resolution: {integrity: sha512-ASwd7Nf4pViqdYRYaNRyPYRVKWa1CcHUAUWR0XeQJLGdNnsWACBwe0sSieb/cHnKsRXjRwO/23KIY83lm/Ccpw==}
@@ -9678,10 +9678,6 @@ 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:
@@ -13120,8 +13116,8 @@ packages:
resolution: {integrity: sha512-Ray65LYEJrMMqDtsBUBXunEVP/g4wtATvq/xz9rchUoLy/9mSkkFgUko/8DVtGxgiP3vivpFMgfb9HpCuDrBHg==}
engines: {node: '>=10.15.1'}
posthog-js@1.395.0:
resolution: {integrity: sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==}
posthog-js@1.393.4:
resolution: {integrity: sha512-mOBSVQczEfyLnW3aFRbLXZEuE0Rfbmhqm9UCnuLeMFUUz0uYTkbnyhl5+Uc+BLSGfyiXHLf3nm2BqxYC+G/PXA==}
posthog-node@4.18.0:
resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==}
@@ -17955,7 +17951,7 @@ snapshots:
'@esbuild-kit/core-utils@3.3.2':
dependencies:
esbuild: 0.28.1
esbuild: 0.28.0
source-map-support: 0.5.21
'@esbuild-kit/esm-loader@2.6.5':
@@ -19901,7 +19897,7 @@ snapshots:
'@polka/url@1.0.0-next.25': {}
'@posthog/core@1.38.0':
'@posthog/core@1.37.3':
dependencies:
'@posthog/types': 1.391.1
@@ -23483,17 +23479,6 @@ 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)
@@ -27420,9 +27405,9 @@ snapshots:
posthog-docusaurus@2.0.5: {}
posthog-js@1.395.0:
posthog-js@1.393.4:
dependencies:
'@posthog/core': 1.38.0
'@posthog/core': 1.37.3
'@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.5
posthog-js: ^1.393.4
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.1
dockerode: ^5.0.0
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.18.0
node-version: 24.17.0
cache: "pnpm"
- shell: bash