Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d10b8ad
feat: inital integration working
StrandedTurtle Feb 12, 2026
77c3ae4
feat: activitty and violations
StrandedTurtle Feb 12, 2026
42ed6ea
feat better live view
StrandedTurtle Feb 12, 2026
615f27d
feat: violations simple
StrandedTurtle Feb 12, 2026
03b9512
Merge branch 'homarr-labs:dev' into tracearr-integration
StrandedTurtle Feb 12, 2026
ebfad85
fix: final changes and cleanup
StrandedTurtle Feb 14, 2026
101837a
fix: prettier
StrandedTurtle Feb 14, 2026
bb90e1e
Merge branch 'dev' into tracearr-integration
StrandedTurtle Feb 14, 2026
061922f
fix: PR suggestions
StrandedTurtle Feb 18, 2026
111f6e4
Merge branch 'tracearr-integration' of https://github.com/StrandedTur…
StrandedTurtle Feb 18, 2026
4f786e9
Merge branch 'homarr-labs:dev' into tracearr-integration
StrandedTurtle Feb 18, 2026
77eb37b
Merge branch 'homarr-labs:dev' into tracearr-integration
StrandedTurtle Feb 19, 2026
f0ea20d
feat: fix cmments from PR
StrandedTurtle Feb 19, 2026
e6aa4ab
Merge branch 'dev' into tracearr-integration
StrandedTurtle Feb 27, 2026
b178fe0
fix: pr suggestions
StrandedTurtle Feb 27, 2026
71c504b
Merge branch 'dev' of https://github.com/homarr-labs/homarr into trac…
StrandedTurtle Mar 4, 2026
cb8f185
Merge branch 'dev' of https://github.com/homarr-labs/homarr into trac…
StrandedTurtle Mar 8, 2026
77c668e
fix: remove new item from mock integration
StrandedTurtle Mar 10, 2026
bacd322
Merge branch 'homarr-labs:dev' into tracearr-integration
StrandedTurtle Mar 10, 2026
be0c31b
fix: add back media monitoring
StrandedTurtle Mar 10, 2026
3f49e7e
Merge branch 'dev' into tracearr-integration
StrandedTurtle Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/api/src/router/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ export const widgetRouter = createTRPCRouter({
networkController: lazy(() => import("./network-controller").then((mod) => mod.networkControllerRouter)),
firewall: lazy(() => import("./firewall").then((mod) => mod.firewallRouter)),
notifications: lazy(() => import("./notifications").then((mod) => mod.notificationsRouter)),
tracearr: lazy(() => import("./tracearr").then((mod) => mod.tracearrRouter)),
});
48 changes: 48 additions & 0 deletions packages/api/src/router/widgets/tracearr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { observable } from "@trpc/server/observable";

import type { TracearrDashboardData } from "@homarr/integrations/types";
import { tracearrRequestHandler } from "@homarr/request-handler/tracearr";

import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";

export const tracearrRouter = createTRPCRouter({
getDashboard: publicProcedure.concat(createManyIntegrationMiddleware("query", "tracearr")).query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = tracearrRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });

return {
integrationId: integration.id,
integrationName: integration.name,
integrationUrl: integration.url,
dashboard: data,
updatedAt: timestamp,
};
}),
);

return results;
}),
subscribeToDashboard: publicProcedure
.concat(createManyIntegrationMiddleware("query", "tracearr"))
.subscription(({ ctx }) => {
return observable<{ integrationId: string; dashboard: TracearrDashboardData; timestamp: Date }>((emit) => {
const unsubscribes = ctx.integrations.map((integration) => {
const innerHandler = tracearrRequestHandler.handler(integration, {});
return innerHandler.subscribe((dashboard) => {
emit.next({
integrationId: integration.id,
dashboard,
timestamp: new Date(),
});
});
});

return () => {
unsubscribes.forEach((unsubscribe) => unsubscribe());
};
});
}),
});
11 changes: 11 additions & 0 deletions packages/common/src/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ dayjs.extend(isBetween);

const validUnits = ["h", "d", "w", "M", "y"] as UnitTypeShort[];

export function formatDuration(milliseconds: number) {
const totalSeconds = Math.floor(milliseconds / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) {
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
return `${minutes}:${String(seconds).padStart(2, "0")}`;
}

export const isDateWithin = (date: Date, relativeDate: string): boolean => {
if (relativeDate.length < 2) {
throw new Error("Relative date must be at least 2 characters long");
Expand Down
2 changes: 2 additions & 0 deletions packages/cron-jobs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { mediaServerJob } from "./jobs/integrations/media-server";
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
import { networkControllerJob } from "./jobs/integrations/network-controller";
import { refreshNotificationsJob } from "./jobs/integrations/notifications";
import { tracearrJob } from "./jobs/integrations/tracearr";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import { rssFeedsJob } from "./jobs/rss-feeds";
Expand Down Expand Up @@ -50,6 +51,7 @@ export const jobGroup = createCronJobGroup({
firewallInterfaces: firewallInterfacesJob,
refreshNotifications: refreshNotificationsJob,
weather: weatherJob,
tracearr: tracearrJob,
});

export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
14 changes: 14 additions & 0 deletions packages/cron-jobs/src/jobs/integrations/tracearr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { tracearrRequestHandler } from "@homarr/request-handler/tracearr";

import { createCronJob } from "../../lib";

export const tracearrJob = createCronJob("tracearr", EVERY_5_SECONDS).withCallback(
createRequestIntegrationJobHandler(tracearrRequestHandler.handler, {
widgetKinds: ["tracearr"],
getInput: {
tracearr: () => ({}),
},
}),
);
9 changes: 9 additions & 0 deletions packages/definitions/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ export const integrationDefs = {
// @ts-expect-error TS2345
documentationUrl: createDocumentationLink("/docs/integrations/immich"),
},
tracearr: {
name: "Tracearr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/tracearr.svg",
category: ["mediaMonitoring"],
// @ts-expect-error - docs page will be created when integration is merged
documentationUrl: createDocumentationLink("/docs/integrations/tracearr"),
},
// 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",
Expand Down Expand Up @@ -429,6 +437,7 @@ export const integrationCategories = [
"notifications",
"firewall",
"photoService",
"mediaMonitoring",
] as const;

export type IntegrationCategory = (typeof integrationCategories)[number];
1 change: 1 addition & 0 deletions packages/definitions/src/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,6 @@ export const widgetKinds = [
"systemDisks",
"immich-serverStats",
"immich-albumCarousel",
"tracearr",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
2 changes: 2 additions & 0 deletions packages/integrations/src/base/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import { QuayIntegration } from "../quay/quay-integration";
import { SeerrIntegration } from "../seerr/seerr-integration";
import { TracearrIntegration } from "../tracearr/tracearr-integration";
import { TrueNasIntegration } from "../truenas/truenas-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import { UnraidIntegration } from "../unraid/unraid-integration";
Expand Down Expand Up @@ -111,6 +112,7 @@ export const integrationCreators = {
truenas: TrueNasIntegration,
unraid: UnraidIntegration,
coolify: CoolifyIntegration,
tracearr: TracearrIntegration,
glances: GlancesIntegration,
immich: ImmichIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
Expand Down
12 changes: 9 additions & 3 deletions packages/integrations/src/base/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,24 +58,30 @@ export abstract class Integration {
private createUrl(
inputUrl: string,
path: `/${string}`,
queryParams?: Record<string, string | Date | number | boolean>,
queryParams?: Record<string, string | Date | number | boolean | null | undefined>,
) {
const baseUrl = removeTrailingSlash(inputUrl);
const url = new URL(`${baseUrl}${path}`);

if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value === null || value === undefined) {
continue;
}
url.searchParams.set(key, value instanceof Date ? value.toISOString() : value.toString());
}
}

return url;
}
protected url(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
protected url(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean | null | undefined>) {
return this.createUrl(this.integration.url, path, queryParams);
}

protected externalUrl(path: `/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
protected externalUrl(
path: `/${string}`,
queryParams?: Record<string, string | Date | number | boolean | null | undefined>,
) {
return this.createUrl(this.integration.externalUrl ?? this.integration.url, path, queryParams);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/integrations/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
export { ICalIntegration } from "./ical/ical-integration";
export { CoolifyIntegration } from "./coolify/coolify-integration";
export { ImmichIntegration } from "./immich/immich-integration";
export { TracearrIntegration } from "./tracearr/tracearr-integration";

// Types
export type { IntegrationInput } from "./base/integration";
Expand All @@ -56,6 +57,7 @@ export type {
export type { ReleasesRepository, ReleaseResponse } from "./interfaces/releases-providers/releases-providers-types";
export type { Notification } from "./interfaces/notifications/notification-types";
export type { ImmichServerStats, ImmichAlbum, ImmichAsset } from "./immich/immich-integration";
export type { TracearrDashboardData } from "./tracearr/tracearr-types";

// Schemas
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
Expand Down
Loading
Loading