diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 000000000..492d56ecc
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,97 @@
+# Changes
+
+## Goal
+
+Remove widget-only services from Homarr's global Integration system.
+
+The affected services are release providers, Search.ch, and ArchiveTeam Warrior. They are only used by specific widgets, so keeping them as global integrations added unnecessary setup, UI, database rows, docs pages, and integration package code.
+
+## Why
+
+These services do not need shared integration state.
+
+- Release providers are selected per Releases widget repository.
+- Search.ch is only used by the Timetable widget.
+- ArchiveTeam Warrior is only used by the ArchiveTeam Warrior widget.
+
+Moving them into widget options makes configuration simpler and removes dead-weight integration code from `packages/integrations`.
+
+## What Changed
+
+### Release Providers
+
+- Added release provider definitions in `packages/definitions/src/release-provider.ts`.
+- Removed release providers from `IntegrationKind`.
+- Moved release-fetching code out of `packages/integrations` and into `packages/request-handler/src/release-providers.ts`.
+- Updated the Releases widget to store provider configuration directly in widget options:
+ - `provider`
+ - `identifier`
+ - `versionFilter`
+ - `providerUrl`
+- Updated the Releases widget editor to use a provider select instead of an integration select.
+- Updated Docker import mapping so `ghcr.io` maps to `gitHubContainerRegistry`.
+- GitHub Container Registry now uses the public OCI registry API and accepts full image references such as `ghcr.io/homarr-labs/homarr:latest`.
+
+### Timetable / Search.ch
+
+- Removed Search.ch as an integration.
+- Added a Timetable widget `baseUrl` option, defaulting to `https://search.ch`.
+- Moved Search.ch request/parsing logic into `packages/request-handler/src/timetable.ts`.
+- Updated the Timetable API to receive `baseUrl` directly from widget options.
+
+### ArchiveTeam Warrior
+
+- Removed ArchiveTeam Warrior as an integration.
+- Added an ArchiveTeam Warrior widget `url` option, defaulting to `http://localhost:8001`.
+- Moved ArchiveTeam Warrior status/WebSocket handling into `packages/request-handler/src/archive-team-warrior.ts`.
+- Updated the widget API and component to use the widget `url` option.
+
+### Integration Cleanup
+
+- Removed creators, exports, docs references, and implementation files for the deleted integrations.
+- Removed unused dependencies from `packages/integrations/package.json`.
+- Added required request-handler dependencies for the moved WebSocket logic.
+
+### Database Migration
+
+- Added `packages/db/migrations/custom/0003_remove_widget_only_integrations.ts`.
+- The migration moves old integration-linked widget config into widget options, then deletes the old widget-only integration rows.
+- Updated the earlier release widget migration to use the new `provider` field instead of `providerIntegrationId`.
+
+### Docs
+
+- Updated Releases widget docs to describe providers instead of integrations.
+- Updated Timetable docs to remove Search.ch integration setup.
+- Removed obsolete integration docs pages for the deleted integration kinds.
+- Updated docs sitemap and integration docs mappings.
+
+### Translations
+
+- Added English labels for the new widget options:
+ - `widget.archiveTeamWarrior.option.url.label`
+ - `widget.timetable.option.baseUrl.label`
+ - `widget.releases.option.repositories.providerUrl.label`
+
+## How It Works Now
+
+Release provider configuration lives on each Releases widget repository entry. The request handler resolves provider metadata and fetches releases directly from the provider-specific endpoint. GHCR uses anonymous OCI registry access for public packages.
+
+Timetable and ArchiveTeam Warrior now pass their widget URL options directly to their API routers and request handlers. No global integration lookup is needed.
+
+## Deliberate Non-Goals
+
+- No provider secret/token support was added.
+- No new secret storage model was introduced.
+- No compatibility layer was kept in `packages/integrations`.
+
+Widget options are client-visible, so storing provider credentials there would be wrong. If authenticated provider requests are needed later, they need a separate secret-storage design.
+
+## Verification So Far
+
+- Focused typechecks passed for affected packages.
+- Full `pnpm turbo typecheck` passed.
+- Full `pnpm lint` passed with existing warnings.
+- Focused release provider normalization tests passed.
+- Widget translation test passed.
+- Live GHCR smoke checks passed for `ghcr.io/muchobien/pocketbase:latest` and `ghcr.io/homarr-labs/homarr:latest`.
+- `pnpm test packages/integrations/test/pi-hole.spec.ts` passed earlier, but failed on rebased reruns because Pi-hole reported `domainsBeingBlocked` as `-2`/`0` instead of `> 1`.
diff --git a/apps/docs/docs/getting-started/after-the-installation.mdx b/apps/docs/docs/getting-started/after-the-installation.mdx
index 5f923cec8..e4fe8e725 100644
--- a/apps/docs/docs/getting-started/after-the-installation.mdx
+++ b/apps/docs/docs/getting-started/after-the-installation.mdx
@@ -28,6 +28,7 @@ Read our [glossary](/docs/getting-started/glossary) if you are new to Homarr.
Congratulations on installing Homarr. Now it's time to get yourself familiar with the concepts of Homarr and how to use it.
## Onboarding
+
The onboard will show the first time you start Homarr.
This process will help you with your first steps and adding some initial data, such as the administrator account.
@@ -45,6 +46,7 @@ You can either import a previous dashboard using the ZIP archive or start from s
You may refresh anytime and continue from a different device - the onboarding is saved immediately on the server for your convenience.
### Starting from scratch
+
Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form.
If credentials provider was selected, you'll be prompted to create an admin user for Homarr.
This user cannot be deleted and has full control over Homarr and all of it's boards.
@@ -52,43 +54,60 @@ You must choose an username and password (both can later be changed).
We enforce strict password requirements, when you enter your password, to protect your instance if you accidentially
expose it to the Internet.
-
+
If you selected external providers (such as LDAP or OIDC), you'll be prompted to create an admin group.
-This group will be used to synchronize users from your provider to Homarr.
+This group will be used to synchronize users from your provider to Homarr.
When a user is logged in through the provider the groups of him that match groups with any in Homarr will be added to the user.
The name of the group has to match exactly the name of the group in the provider.
-
+
Next, you can configure some basic settings for Homarr. We recommend to leave all settings at default for most users.
Here you can also disable analytics. Please note that our analytics are completly anonymous and is fully compliant with GDPR and PECR.
-
+
### Setting up integrations
After configuring server settings, the onboarding guides you through setting up your integrations.
#### Docker service discovery
+
If Docker is available (socket mounted), Homarr automatically detects running containers and matches them to known integration types.
Matching integrations are pre-selected in the integration list, saving you from manually picking each one.
#### URL templating
+
You can define a base URL template to auto-fill integration URLs in bulk.
Two modes are supported:
+
- **Subdomain mode** - e.g. `{app}.example.com` where `{app}` is replaced with the integration name
- **Port mode** - e.g. `192.168.1.1:{port}` where `{port}` is replaced with the integration's default port
#### API key helper links
+
For integrations that require API keys (such as Sonarr, Radarr, SABnzbd, or Seerr), a helper link opens the relevant settings page in the target application so you can quickly copy your key.
#### Creating apps from Docker containers
+
You can optionally auto-create bookmark apps for all discovered Docker containers, making them immediately available on your board.
:::note
-Some niche and registry integrations (GitHub, npm, Docker Hub, GitLab, Codeberg, LinuxServer.io, GHCR, Quay, Search.ch) are hidden from the onboarding selection grid but remain available on the manage integrations page.
+Release providers and Search.ch are configured directly in their widgets instead of the integrations page.
:::
@@ -97,30 +116,41 @@ The onboarding can no longer be started again.
From here you can click on one of the provided links to continue your journey.
We recommend you to continue with the section [creating your first board on this page](#creating-your-first-board).
-
+
### Importing a ZIP from version before 1.0.0
+
This is recommended when you used Homarr before and want to re-import your data.
It is important that you first migrate correctly using [the list of breaking changes for 1.0](/blog/2024/09/23/version-1.0).
-
+
**How to export and import data**
-1. Navigate to your old Homarr, open your management pages, open the tools submenu and open ``Migrate to 1.0``.
+
+1. Navigate to your old Homarr, open your management pages, open the tools submenu and open `Migrate to 1.0`.
2. Check the contents that you want to export. We recommend you to export all data.
3. Click on the export data button
4. Save the ZIP file, that is being downloaded, on your device. If you selected "integrations" or "users", you also get an encryption / decryption key.
-Copy it and do not loose it!
-5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose ``Import from Homarr before 1.0``
+ Copy it and do not loose it!
+5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose `Import from Homarr before 1.0`
6. Drag and drop your previously downloaded ZIP archive into the file upload:
7. Next, choose the data that you want to import and how to import it. Since screen sizes were removed in 1.0, we create a new board for every screen size by default. We recommend you to only keep the best layout and adjust it later in 1.0.0 - but you can also import all of them and pick later.
8. If you previously exported integrations or users, you will be asked to decrypt the data safely using your obtained encryption token. Paste into the modal and confirm your action.
9. Next, you can [follow the same setup as when starting from scratch](#starting-from-scratch). After you completed the onboarding, all of your data will be migrated and available.
## Creating your first board
+
In Homarr, you organize your apps and widgets on boards.
Boards can be organized using the interactive drag and drop.
-Let's create your first board by navigating here: ``Manage`` > ``Boards`` > ``New board``:
+Let's create your first board by navigating here: `Manage` > `Boards` > `New board`:

A board must always have a name and can either be public or private.
@@ -133,12 +163,13 @@ Congratulations - you've just created your first board. You can see boards, that

## Add your first app to Homarr
+
To add links to your dashboard, you must first add them as an "app".
Navigate to this page to add an app:

-Next, click on the ``New app`` button. You'll need to provide a name and an icon to create an app.
+Next, click on the `New app` button. You'll need to provide a name and an icon to create an app.
The name must be at least one character long but you can provide any image URL for the icon.
Homarr ships with over 11K icons - so you can just search by entering a search text instead.
@@ -149,8 +180,9 @@ After you created the app, you'll see in on the apps page:

## Arrange and organize your board
+
Let's add your freshly created app, from the previous step, to the board.
-To do this, edit your previously created board by clicking on the big ``Open board`` button:
+To do this, edit your previously created board by clicking on the big `Open board` button:

@@ -165,23 +197,38 @@ Here you have the following options in the header:
When entering the edit mode, you'll get some additional buttons to work with:
-
+
- **Add item**: Click this button to add new elements to your page.
- **Exit edit mode**: Click this btuton to save your changes and exit edit mode.
To continue, click the option "New Item":
-
+
Next, you can choose what item you'd like to add.
We will discuss the other options in the following sections - for now choose "App" to continue.
-
+
As soon as you chose the app, the modal will close and you'll get the following item on your board:
-
+
This is expected, because you haven't told Homarr what app it should show in this item.
To configure this, click on the small 3 dots at the top of the item in the edit mode and choose "Edit item" in the menu.
@@ -228,17 +275,18 @@ Be sure to save your changes by exiting edit mode!

## Adding your first integration
+
Now it's time to integrate some basic data into your board.
To do this, we first need to create an integration.
An integration is a connection between Homarr and a third party application - usually something that you run and deploy yourself.
-Navigate to ``Manage`` > ``Integrations`` (for example via your user menu).
+Navigate to `Manage` > `Integrations` (for example via your user menu).
As an example, we are going to use the public demo instance of **Dash.** - a popular application to monitor CPU / hardware usage.
Choose the **Dash.** integration from the list of available integrations:

-Next, choose a name (or leave it at default), enter ``https://dash.mauz.dev/`` as the URL in the form and click on ``Test connection and create``.
+Next, choose a name (or leave it at default), enter `https://dash.mauz.dev/` as the URL in the form and click on `Test connection and create`.
Pressing the save button will perform a test request to the integration, to confirm that the networking setup is working as expected.
Sometimes invalid configurations, firewalls and other mistakes can lead to connectivity problems - [follow this guide here](/docs/management/integrations/#troubleshooting-connection) if you experience issues.
@@ -254,13 +302,14 @@ the target application or your router.
:::
## Embed integration data using widgets
+
Let's add your integration, that you have configured in the section above, as a widget to your board.
First, navigate back to your board that you want to add it to.
Next, enter edit mode again and add another item to your board:

-Instead of choosing an app, let's choose ``System Health Monitoring`` this time.
+Instead of choosing an app, let's choose `System Health Monitoring` this time.
Click on "Add to board":

@@ -290,10 +339,12 @@ As with every change, don't forget to save your changes using the exit edit mode

## Grouping your items
+
As the last step, let's organize your freshly created items using two different options.
You can choose either or both of them on your boards.
### Using dynamic sections
+
Dynamic sections are a simple, resizable visual group to group items.

@@ -303,6 +354,7 @@ After adding a dynamic section, you can simply drag and drop items into the sect

### Using categories
+
Categories are more advanced and complicated than dynamic sections.
To create a category, open the add item modal and choose "new category":
@@ -329,22 +381,27 @@ You cannot reduce their horizontal width.
After completing the onboarding, Homarr provides interactive guided tours to help you discover key features.
### Management tour
+
When you first visit the management area (`/manage`), Homarr walks you through the main pages: boards, apps, integrations, and users (admin only).
Each page highlights the list view and the create action so you know where to find and add things.
### Board tour
+
When you first open a board, a tour highlights the board header elements: search, edit mode, settings, board switcher, and user menu.
### Tour controls
+
- Press **Enter** (or click **Next**) to advance through tour steps
- Tours auto-start on first visit and do not repeat once completed
- Tours are skipped on mobile screens (below `sm` breakpoint)
- You can **reset tours** from the user settings page under **Manage > Users > [Your user] > General**
## Next steps
+
You're done with the basic setup 🔥. Now it's time to organize and customize even more.
We highly recommend you to experiment with different options, styles and configurations.
See these curated pages for your next steps:
+
- Create new apps using the [apps management](/docs/management/apps/)
- Customize your board or add more boards using the [boards management](/docs/management/boards/)
- Integrate software that you use using the [integrations management](/docs/management/integrations/)
diff --git a/apps/docs/docs/integrations/codeberg/index.mdx b/apps/docs/docs/integrations/codeberg/index.mdx
deleted file mode 100644
index 98ca314c2..000000000
--- a/apps/docs/docs/integrations/codeberg/index.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: 'Codeberg'
-description: "Codeberg is a non-profit, community-driven platform for hosting and collaborating on software projects."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { codebergIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/codeberg/index.ts b/apps/docs/docs/integrations/codeberg/index.ts
deleted file mode 100644
index f2a43ad48..000000000
--- a/apps/docs/docs/integrations/codeberg/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const codebergIntegration: IntegrationDefinition = {
- name: "Codeberg",
- description:
- "Codeberg is a non-profit, community-driven platform for hosting and collaborating on software projects.",
- iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/codeberg.svg",
- path: "../../integrations/codeberg",
-};
diff --git a/apps/docs/docs/integrations/docker-hub/index.mdx b/apps/docs/docs/integrations/docker-hub/index.mdx
deleted file mode 100644
index a49d03047..000000000
--- a/apps/docs/docs/integrations/docker-hub/index.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: 'Docker Hub'
-description: "Docker Hub is a cloud-based registry service for sharing and managing Docker images."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { dockerHubIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/docker-hub/index.ts b/apps/docs/docs/integrations/docker-hub/index.ts
deleted file mode 100644
index 1a49feeae..000000000
--- a/apps/docs/docs/integrations/docker-hub/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const dockerHubIntegration: IntegrationDefinition = {
- name: "Docker Hub",
- description: "Docker Hub is a cloud-based registry service for sharing and managing Docker images.",
- iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/docker.svg",
- path: "../../integrations/docker-hub",
-};
diff --git a/apps/docs/docs/integrations/github-containerregistry/index.mdx b/apps/docs/docs/integrations/github-containerregistry/index.mdx
deleted file mode 100644
index f4cb5b310..000000000
--- a/apps/docs/docs/integrations/github-containerregistry/index.mdx
+++ /dev/null
@@ -1,42 +0,0 @@
----
-title: 'GitHub Container Registry'
-description: "GitHub Container Registry is a platform for hosting and managing packages."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { gitHubContainerRegistryIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/github-containerregistry/index.ts b/apps/docs/docs/integrations/github-containerregistry/index.ts
deleted file mode 100644
index b88f426d3..000000000
--- a/apps/docs/docs/integrations/github-containerregistry/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const gitHubContainerRegistryIntegration: IntegrationDefinition = {
- name: "GitHub Container Registry",
- description: "GitHub Container Registry is a platform for hosting and managing packages.",
- iconUrl: {
- light: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github.svg",
- dark: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github-light.svg",
- },
- path: "../../integrations/github-packages",
-};
diff --git a/apps/docs/docs/integrations/github/index.mdx b/apps/docs/docs/integrations/github/index.mdx
deleted file mode 100644
index 39c9777b3..000000000
--- a/apps/docs/docs/integrations/github/index.mdx
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: 'GitHub'
-description: "GitHub is a web-based platform for version control and collaboration."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { githubIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/github/index.ts b/apps/docs/docs/integrations/github/index.ts
deleted file mode 100644
index 7bb9b6b5d..000000000
--- a/apps/docs/docs/integrations/github/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const githubIntegration: IntegrationDefinition = {
- name: "GitHub",
- description: "GitHub is a web-based platform for version control and collaboration.",
- iconUrl: {
- light: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github.svg",
- dark: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github-light.svg",
- },
- path: "../../integrations/github",
-};
diff --git a/apps/docs/docs/integrations/gitlab/index.mdx b/apps/docs/docs/integrations/gitlab/index.mdx
deleted file mode 100644
index edc77e1cd..000000000
--- a/apps/docs/docs/integrations/gitlab/index.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: 'GitLab'
-description: "GitLab is a web-based DevOps lifecycle tool that provides a Git repository manager."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { gitlabIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/gitlab/index.ts b/apps/docs/docs/integrations/gitlab/index.ts
deleted file mode 100644
index c252cd6ff..000000000
--- a/apps/docs/docs/integrations/gitlab/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const gitlabIntegration: IntegrationDefinition = {
- name: "GitLab",
- description: "GitLab is a web-based DevOps lifecycle tool that provides a Git repository manager.",
- iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitlab.svg",
- path: "../../integrations/gitlab",
-};
diff --git a/apps/docs/docs/integrations/linux-server-io/index.mdx b/apps/docs/docs/integrations/linux-server-io/index.mdx
deleted file mode 100644
index 426dc7f68..000000000
--- a/apps/docs/docs/integrations/linux-server-io/index.mdx
+++ /dev/null
@@ -1,34 +0,0 @@
----
-title: 'LinuxServer.io'
-description: "LinuxServer.io is a platform for managing and deploying containerized applications."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { linuxServerIoIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/linux-server-io/index.ts b/apps/docs/docs/integrations/linux-server-io/index.ts
deleted file mode 100644
index 82bc0fac6..000000000
--- a/apps/docs/docs/integrations/linux-server-io/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const linuxServerIoIntegration: IntegrationDefinition = {
- name: "LinuxServer.io",
- description: "LinuxServer.io is a platform for managing and deploying containerized applications.",
- iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg",
- path: "../../integrations/linux-server-io",
-};
diff --git a/apps/docs/docs/integrations/npm/index.mdx b/apps/docs/docs/integrations/npm/index.mdx
deleted file mode 100644
index c4d21135e..000000000
--- a/apps/docs/docs/integrations/npm/index.mdx
+++ /dev/null
@@ -1,34 +0,0 @@
----
-title: 'NPM'
-description: "NPM is a package manager for the JavaScript programming language, primarily used for managing Node.js packages."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { npmIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/npm/index.ts b/apps/docs/docs/integrations/npm/index.ts
deleted file mode 100644
index 34fbafb5c..000000000
--- a/apps/docs/docs/integrations/npm/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const npmIntegration: IntegrationDefinition = {
- name: "NPM",
- description:
- "NPM is a package manager for the JavaScript programming language, primarily used for managing Node.js packages.",
- iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/npm.svg",
- path: "../../integrations/npm",
-};
diff --git a/apps/docs/docs/integrations/quay/index.mdx b/apps/docs/docs/integrations/quay/index.mdx
deleted file mode 100644
index 9033f81cf..000000000
--- a/apps/docs/docs/integrations/quay/index.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: 'Quay'
-description: "Quay is a container registry for storing and managing container images."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { quayIntegration } from '.';
-import { releasesWidget } from '@site/docs/widgets/releases';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
-
-### Secrets
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/quay/index.ts b/apps/docs/docs/integrations/quay/index.ts
deleted file mode 100644
index 315732bff..000000000
--- a/apps/docs/docs/integrations/quay/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-
-export const quayIntegration: IntegrationDefinition = {
- name: "Quay",
- description: "Quay is a container registry for storing and managing container images.",
- iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png",
- path: "../../integrations/quay",
-};
diff --git a/apps/docs/docs/integrations/search-ch/index.mdx b/apps/docs/docs/integrations/search-ch/index.mdx
deleted file mode 100644
index 6286d5cca..000000000
--- a/apps/docs/docs/integrations/search-ch/index.mdx
+++ /dev/null
@@ -1,28 +0,0 @@
----
-title: 'Search.ch'
-description: "Search.ch supports searching for timetables in Switzerland."
-hide_title: true
----
-
-import { IntegrationHeader } from '@site/src/components/integrations/header';
-import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
-import { AddingIntegration } from '@site/src/components/integrations/adding';
-import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
-import { IconDownload } from "@tabler/icons-react";
-import { searchChIntegration } from '.';
-import Alert from '@theme/Admonition';
-import { timetableWidget } from '@site/docs/widgets/timetable';
-
-
-
-
-### Widgets & Capabilities
-
-
-### Adding the integration
-
\ No newline at end of file
diff --git a/apps/docs/docs/integrations/search-ch/index.ts b/apps/docs/docs/integrations/search-ch/index.ts
deleted file mode 100644
index 6c5819d67..000000000
--- a/apps/docs/docs/integrations/search-ch/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { IntegrationDefinition } from "@site/src/types";
-export const searchChIntegration: IntegrationDefinition = {
- name: "Search.ch",
- description: "Search.ch supports searching for timetables in Switzerland.",
- iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png",
- path: "../../integrations/search-ch",
-};
diff --git a/apps/docs/docs/widgets/immich-album-carousel/index.ts b/apps/docs/docs/widgets/immich-album-carousel/index.ts
index 327314c3d..6f74e9c23 100644
--- a/apps/docs/docs/widgets/immich-album-carousel/index.ts
+++ b/apps/docs/docs/widgets/immich-album-carousel/index.ts
@@ -9,9 +9,8 @@ export const immichAlbumCarouselWidget: WidgetDefinition = {
configuration: {
items: [
{
- name: "Album ID",
- description:
- "This is a unique ID which refers to your Immich album. You can find this id by opening an album and copying the unique string from the URL, for example 'a21aca09-9f05-47dc-b1a1-7b53c547226e'",
+ name: "Album",
+ description: "Select the Immich album to show in the slideshow.",
values: { type: "string" },
defaultValue: "",
},
@@ -27,6 +26,12 @@ export const immichAlbumCarouselWidget: WidgetDefinition = {
values: { type: "boolean" },
defaultValue: "yes",
},
+ {
+ name: "Randomize photos",
+ description: "Show photos in a random order instead of always starting with the same album order.",
+ values: { type: "boolean" },
+ defaultValue: "no",
+ },
],
},
};
diff --git a/apps/docs/docs/widgets/releases/index.mdx b/apps/docs/docs/widgets/releases/index.mdx
index 7808756f0..faf386a13 100644
--- a/apps/docs/docs/widgets/releases/index.mdx
+++ b/apps/docs/docs/widgets/releases/index.mdx
@@ -1,69 +1,44 @@
---
-title: 'Releases'
-description: 'Displays a list of the current version of the given repositories with the given version regex.'
+title: "Releases"
+description: "Displays a list of the current version of the given repositories with the given version regex."
hide_title: true
---
-import { AddingWidget } from '@site/src/components/widgets/adding';
-import { WidgetConfig } from '@site/src/components/widgets/configuration';
-import { WidgetHeader } from '@site/src/components/widgets/header';
-import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
-import { releasesWidget } from '.';
-import { githubIntegration } from '@site/docs/integrations/github';
-import { gitlabIntegration } from '@site/docs/integrations/gitlab';
-import { dockerHubIntegration } from '@site/docs/integrations/docker-hub';
-import { npmIntegration } from '@site/docs/integrations/npm';
-import { codebergIntegration } from '@site/docs/integrations/codeberg';
+import { AddingWidget } from "@site/src/components/widgets/adding";
+import { WidgetConfig } from "@site/src/components/widgets/configuration";
+import { WidgetHeader } from "@site/src/components/widgets/header";
+import { releasesWidget } from ".";
-
+
-The releases widget enables you to track a list of repositories and see their respective latest release.
-It also supports importing releases from your local docker containers.
+The releases widget enables you to track a list of repositories and see their respective latest release.
+It also supports importing releases from your local docker containers.
### Screenshots
-import collapsibleCard from './img/collapsible.png';
-import fullCard from './img/full.png';
-import noDetailsCard from './img/no-details.png';
-import { quayIntegration } from '@site/docs/integrations/quay';
-import { gitHubContainerRegistryIntegration } from '@site/docs/integrations/github-containerregistry';
-import { linuxServerIoIntegration } from '@site/docs/integrations/linux-server-io';
+import collapsibleCard from "./img/collapsible.png";
+import fullCard from "./img/full.png";
+import noDetailsCard from "./img/no-details.png";
-
+
-### Supported Integrations
+### Supported Providers
-
+GitHub, Docker Hub, GitLab, NPM, Codeberg, GitHub Container Registry, Quay, and LinuxServer.io are configured per repository in the widget.
+
+For GitHub Container Registry, use `owner/package` identifiers such as `homarr-labs/homarr`.
+Full image references such as `ghcr.io/homarr-labs/homarr:latest` are also accepted.
### Adding the widget
### Configuration
+
:::note
@@ -74,4 +49,3 @@ Setting the Prefix to `v`, precision to `2` and the Suffix to `-alpha`, will mat
Setting the Prefix to `v`, precision to `3`, will match `v5.2.0` but not `v1.0.2-alpha` or `Beta v5.2.1`.
:::
-
diff --git a/apps/docs/docs/widgets/releases/index.ts b/apps/docs/docs/widgets/releases/index.ts
index dbe3c1271..90fff3491 100644
--- a/apps/docs/docs/widgets/releases/index.ts
+++ b/apps/docs/docs/widgets/releases/index.ts
@@ -54,16 +54,22 @@ export const releasesWidget: WidgetDefinition = {
items: [
{
name: "Provider",
- description: "Integration to fetch the repository from.",
- values: "Select from the available integrations.",
+ description: "Provider to fetch the repository or package releases from.",
+ values: "Select from the available providers.",
defaultValue: "-",
},
{
name: "Identifier",
- description: 'The identifier of the repository, e.g. "homarr-labs/homarr".',
- values: "Any valid repository identifier.",
+ description: 'The identifier of the repository or package, e.g. "homarr-labs/homarr".',
+ values: 'Any valid provider identifier. GHCR also accepts full image refs like "ghcr.io/owner/package:tag".',
defaultValue: "-",
},
+ {
+ name: "Provider URL",
+ description: "Base URL for the provider.",
+ values: "Any valid URL.",
+ defaultValue: "Provider default",
+ },
{
name: "Name",
description: "Display name of the repository.",
diff --git a/apps/docs/docs/widgets/timetable/index.mdx b/apps/docs/docs/widgets/timetable/index.mdx
index fe9a1b11c..e9bdfd326 100644
--- a/apps/docs/docs/widgets/timetable/index.mdx
+++ b/apps/docs/docs/widgets/timetable/index.mdx
@@ -1,44 +1,40 @@
---
-title: 'Timetable'
-description: 'Displays departure times for a station.'
+title: "Timetable"
+description: "Displays departure times for a station."
hide_title: true
---
-import { AddingWidget } from '@site/src/components/widgets/adding';
-import { WidgetConfig } from '@site/src/components/widgets/configuration';
-import { WidgetHeader } from '@site/src/components/widgets/header';
-import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
-import { timetableWidget } from '.';
-import { searchChIntegration } from '@site/docs/integrations/search-ch';
+import { AddingWidget } from "@site/src/components/widgets/adding";
+import { WidgetConfig } from "@site/src/components/widgets/configuration";
+import { WidgetHeader } from "@site/src/components/widgets/header";
+import { timetableWidget } from ".";
-
-
+
The Timetable widget provides real-time departure times for a selected station.
### Screenshots
-import trainDepartures from './img/train-departures.png';
-import streetDepartures from './img/street-departures.png';
+import trainDepartures from "./img/train-departures.png";
+import streetDepartures from "./img/street-departures.png";
### Adding the widget
-### Supported Integrations
-
-
-
### Configuration
+
diff --git a/apps/docs/docs/widgets/timetable/index.ts b/apps/docs/docs/widgets/timetable/index.ts
index 6f36349ee..1cefb6bcd 100644
--- a/apps/docs/docs/widgets/timetable/index.ts
+++ b/apps/docs/docs/widgets/timetable/index.ts
@@ -8,6 +8,12 @@ export const timetableWidget: WidgetDefinition = {
path: "../../widgets/timetable",
configuration: {
items: [
+ {
+ name: "Search.ch URL",
+ description: "Base URL for the Search.ch timetable API.",
+ values: "Any valid URL.",
+ defaultValue: "https://search.ch",
+ },
{
name: "Station",
description: "Select the station",
diff --git a/apps/nextjs/src/components/integration/integration-grid-shared.ts b/apps/nextjs/src/components/integration/integration-grid-shared.ts
index 2fc7228c8..7451af1e7 100644
--- a/apps/nextjs/src/components/integration/integration-grid-shared.ts
+++ b/apps/nextjs/src/components/integration/integration-grid-shared.ts
@@ -48,10 +48,8 @@ export const categoryTranslationKeys: Record
= {
search: "integration.category.search",
mediaTranscoding: "integration.category.mediaTranscoding",
networkController: "integration.category.networkController",
- releasesProvider: "integration.category.releasesProvider",
notifications: "integration.category.notifications",
firewall: "integration.category.firewall",
- timetable: "integration.category.timetable",
photoService: "integration.category.photoService",
notes: "integration.category.notes",
mediaMonitoring: "integration.category.mediaMonitoring",
diff --git a/packages/api/src/router/test/integration/integration-test-connection.spec.ts b/packages/api/src/router/test/integration/integration-test-connection.spec.ts
index f2519c533..725c9393a 100644
--- a/packages/api/src/router/test/integration/integration-test-connection.spec.ts
+++ b/packages/api/src/router/test/integration/integration-test-connection.spec.ts
@@ -271,77 +271,4 @@ describe("testConnectionAsync should run test connection of integration", () =>
});
});
- test("with input of existing github app", async () => {
- // Arrange
- const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
- const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
- factorySpy.mockReturnValue(
- Promise.resolve({
- testConnectionAsync: async () => await Promise.resolve({ success: true }),
- } as homarrIntegrations.PiHoleIntegrationV6),
- );
- optionsSpy.mockReturnValue([[], ["githubAppId", "githubInstallationId", "privateKey"]]);
-
- const integration = {
- id: "new",
- name: "GitHub",
- url: "https://api.github.com",
- kind: "github" as const,
- secrets: [
- {
- kind: "githubAppId" as const,
- value: "345",
- },
- {
- kind: "githubInstallationId" as const,
- value: "456",
- },
- {
- kind: "privateKey" as const,
- value: null,
- },
- ],
- };
-
- const dbSecrets = [
- {
- kind: "githubAppId" as const,
- value: "123.encrypted" as const,
- },
- {
- kind: "githubInstallationId" as const,
- value: "234.encrypted" as const,
- },
- {
- kind: "privateKey" as const,
- value: "privateKey.encrypted" as const,
- },
- ];
-
- // Act
- await testConnectionAsync(integration, dbSecrets);
-
- // Assert
- expect(factorySpy).toHaveBeenCalledWith({
- id: "new",
- name: "GitHub",
- url: "https://api.github.com",
- kind: "github" as const,
- decryptedSecrets: [
- expect.objectContaining({
- kind: "githubAppId",
- value: "345",
- }),
- expect.objectContaining({
- kind: "githubInstallationId",
- value: "456",
- }),
- expect.objectContaining({
- kind: "privateKey",
- value: "privateKey",
- }),
- ],
- externalUrl: null,
- });
- });
});
diff --git a/packages/api/src/router/widgets/archive-team-warrior.ts b/packages/api/src/router/widgets/archive-team-warrior.ts
index 0824c165a..14dd6b681 100644
--- a/packages/api/src/router/widgets/archive-team-warrior.ts
+++ b/packages/api/src/router/widgets/archive-team-warrior.ts
@@ -1,39 +1,37 @@
import { observable } from "@trpc/server/observable";
+import { z } from "zod/v4";
-import type { ArchiveTeamWarriorStatus } from "@homarr/integrations";
+import type { ArchiveTeamWarriorStatus } from "@homarr/request-handler/archive-team-warrior";
import { archiveTeamWarriorRequestHandler } from "@homarr/request-handler/archive-team-warrior";
-import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
+const inputSchema = z.object({ url: z.string().url() });
+
export const archiveTeamWarriorRouter = createTRPCRouter({
- getStatus: publicProcedure
- .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior"))
- .query(async ({ ctx }) => {
- const handler = archiveTeamWarriorRequestHandler.handler(ctx.integration, {});
- const { data, timestamp } = await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
+ getStatus: publicProcedure.input(inputSchema).query(async ({ input }) => {
+ const handler = archiveTeamWarriorRequestHandler.handler({ url: input.url });
+ const { data, timestamp } = await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
- return {
- status: data,
- updatedAt: timestamp,
- };
- }),
+ return {
+ status: data,
+ updatedAt: timestamp,
+ };
+ }),
- subscribeStatus: publicProcedure
- .concat(createOneIntegrationMiddleware("query", "archiveTeamWarrior"))
- .subscription(({ ctx }) => {
- return observable<{ status: ArchiveTeamWarriorStatus; updatedAt: Date }>((emit) => {
- const handler = archiveTeamWarriorRequestHandler.handler(ctx.integration, {});
- const unsubscribe = handler.subscribe((status) => {
- emit.next({
- status,
- updatedAt: new Date(),
- });
+ subscribeStatus: publicProcedure.input(inputSchema).subscription(({ input }) => {
+ return observable<{ status: ArchiveTeamWarriorStatus; updatedAt: Date }>((emit) => {
+ const handler = archiveTeamWarriorRequestHandler.handler({ url: input.url });
+ const unsubscribe = handler.subscribe((status) => {
+ emit.next({
+ status,
+ updatedAt: new Date(),
});
-
- return () => {
- unsubscribe();
- };
});
- }),
+
+ return () => {
+ unsubscribe();
+ };
+ });
+ }),
});
diff --git a/packages/api/src/router/widgets/releases.ts b/packages/api/src/router/widgets/releases.ts
index 3d355b26c..b403b87cb 100644
--- a/packages/api/src/router/widgets/releases.ts
+++ b/packages/api/src/router/widgets/releases.ts
@@ -1,10 +1,9 @@
import { escapeForRegEx } from "@tiptap/react";
import { z } from "zod/v4";
-import { getIntegrationKindsByCategory } from "@homarr/definitions";
+import { releaseProviderKinds } from "@homarr/definitions";
import { releasesRequestHandler } from "@homarr/request-handler/releases";
-import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const formatVersionFilterRegex = (versionFilter: z.infer | undefined) => {
@@ -25,26 +24,29 @@ const releaseVersionFilterSchema = z.object({
export const releasesRouter = createTRPCRouter({
getLatest: publicProcedure
- .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("releasesProvider")))
.input(
z.object({
repositories: z.array(
z.object({
id: z.string(),
+ provider: z.enum(releaseProviderKinds),
identifier: z.string(),
versionFilter: releaseVersionFilterSchema.optional(),
+ providerUrl: z.string().url().optional(),
}),
),
}),
)
- .query(async ({ ctx, input }) => {
+ .query(async ({ input }) => {
return await Promise.all(
input.repositories.map(async (repository) => {
const response = await releasesRequestHandler
- .handler(ctx.integration, {
+ .handler({
id: repository.id,
+ provider: repository.provider,
identifier: repository.identifier,
versionRegex: formatVersionFilterRegex(repository.versionFilter),
+ providerUrl: repository.providerUrl,
})
.getCachedOrUpdatedDataAsync({
forceUpdate: false,
@@ -52,7 +54,7 @@ export const releasesRouter = createTRPCRouter({
return {
id: repository.id,
- integration: { name: ctx.integration.name, kind: ctx.integration.kind },
+ provider: repository.provider,
timestamp: response.timestamp,
...response.data,
};
diff --git a/packages/api/src/router/widgets/timetable.ts b/packages/api/src/router/widgets/timetable.ts
index 787be4c37..189338a13 100644
--- a/packages/api/src/router/widgets/timetable.ts
+++ b/packages/api/src/router/widgets/timetable.ts
@@ -1,21 +1,26 @@
import z from "zod";
-import { getIntegrationKindsByCategory } from "@homarr/definitions";
-import { timetableOptionsSchema } from "@homarr/integrations/types";
import {
timetableGetTimetableRequestHandler,
timetableSearchStationsRequestHandler,
} from "@homarr/request-handler/timetable";
-import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
+const baseUrlSchema = z.string().url();
+
export const timetableRouter = createTRPCRouter({
getTimetable: publicProcedure
- .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable")))
- .input(timetableOptionsSchema)
- .query(async ({ input, ctx }) => {
- const innerHandler = timetableGetTimetableRequestHandler.handler(ctx.integration, {
+ .input(
+ z.object({
+ baseUrl: baseUrlSchema,
+ stationId: z.string(),
+ limit: z.number(),
+ }),
+ )
+ .query(async ({ input }) => {
+ const innerHandler = timetableGetTimetableRequestHandler.handler({
+ baseUrl: input.baseUrl,
stationId: input.stationId,
limit: input.limit,
});
@@ -25,14 +30,15 @@ export const timetableRouter = createTRPCRouter({
return data;
}),
searchStations: publicProcedure
- .concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("timetable")))
.input(
z.object({
+ baseUrl: baseUrlSchema,
query: z.string(),
}),
)
- .query(async ({ input, ctx }) => {
- const innerHandler = timetableSearchStationsRequestHandler.handler(ctx.integration, {
+ .query(async ({ input }) => {
+ const innerHandler = timetableSearchStationsRequestHandler.handler({
+ baseUrl: input.baseUrl,
query: input.query,
});
diff --git a/packages/cron-jobs/src/jobs/integrations/timetable.ts b/packages/cron-jobs/src/jobs/integrations/timetable.ts
index b3964dc38..8cda973e3 100644
--- a/packages/cron-jobs/src/jobs/integrations/timetable.ts
+++ b/packages/cron-jobs/src/jobs/integrations/timetable.ts
@@ -1,20 +1,39 @@
+import SuperJSON from "superjson";
+
+import { createLogger } from "@homarr/core/infrastructure/logs";
+import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
-import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
+import { db, eq } from "@homarr/db";
+import { items } from "@homarr/db/schema";
import { timetableGetTimetableRequestHandler } from "@homarr/request-handler/timetable";
+import type { WidgetComponentProps } from "../../../../widgets";
import { createCronJob } from "../../lib";
-export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback(
- createRequestIntegrationJobHandler(timetableGetTimetableRequestHandler.handler, {
- widgetKinds: ["timetable"],
- getInput: {
- timetable: (options) =>
- options.station
- ? {
- stationId: options.station.value,
- limit: 10,
- }
- : [],
- },
- }),
-);
+const logger = createLogger({ module: "timetableJobs" });
+
+export const timetableJob = createCronJob("timetable", EVERY_5_MINUTES).withCallback(async () => {
+ const timetableItems = await db.query.items.findMany({
+ where: eq(items.kind, "timetable"),
+ });
+
+ const parsedItems = timetableItems.map((item) => ({
+ id: item.id,
+ options: SuperJSON.parse["options"]>(item.options),
+ }));
+
+ for (const item of parsedItems) {
+ if (!item.options.station) continue;
+
+ try {
+ const innerHandler = timetableGetTimetableRequestHandler.handler({
+ baseUrl: item.options.baseUrl,
+ stationId: item.options.station.value,
+ limit: 10,
+ });
+ await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
+ } catch (error) {
+ logger.error(new ErrorWithMetadata("Failed to update timetable", { id: item.id }, { cause: error }));
+ }
+ }
+});
diff --git a/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts b/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts
index 951831482..8cec43188 100644
--- a/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts
+++ b/packages/db/migrations/custom/0000_release_widget_provider_to_options.ts
@@ -1,8 +1,7 @@
import SuperJSON from "superjson";
import { createId } from "@homarr/common";
-import type { IntegrationKind } from "@homarr/definitions";
-import { getIntegrationKindsByCategory } from "@homarr/definitions";
+import { releaseProviderKinds } from "@homarr/definitions";
import { eq } from "../..";
import type { Database } from "../..";
@@ -13,19 +12,6 @@ export async function migrateReleaseWidgetProviderToOptionsAsync(db: Database) {
where: (items, { eq }) => eq(items.kind, "releases"),
});
- const integrationKinds = getIntegrationKindsByCategory("releasesProvider");
- const providerIntegrations = await db.query.integrations.findMany({
- where: (integrations, { inArray }) => inArray(integrations.kind, integrationKinds),
- columns: {
- id: true,
- kind: true,
- },
- });
-
- const providerIntegrationMap = new Map(
- providerIntegrations.map((integration) => [integration.kind, integration.id]),
- );
-
const updates: {
id: string;
options: object;
@@ -44,7 +30,7 @@ export async function migrateReleaseWidgetProviderToOptionsAsync(db: Database) {
return {
id: createId(),
- providerIntegrationId: providerIntegrationMap.get(provider as IntegrationKind) ?? null,
+ provider: releaseProviderKinds.find((kind) => kind === provider),
...otherFields,
};
},
@@ -68,5 +54,5 @@ export async function migrateReleaseWidgetProviderToOptionsAsync(db: Database) {
.where(eq(items.id, update.id));
}
- console.log(`Migrated release widget providers to integrations count="${updates.length}"`);
+ console.log(`Migrated release widget providers to options count="${updates.length}"`);
}
diff --git a/packages/db/migrations/custom/0003_remove_widget_only_integrations.ts b/packages/db/migrations/custom/0003_remove_widget_only_integrations.ts
new file mode 100644
index 000000000..030525639
--- /dev/null
+++ b/packages/db/migrations/custom/0003_remove_widget_only_integrations.ts
@@ -0,0 +1,121 @@
+import SuperJSON from "superjson";
+
+import { eq, inArray } from "../..";
+import type { Database } from "../..";
+import { integrationItems, integrations, items } from "../../schema";
+
+const removedIntegrationKinds = [
+ "github",
+ "dockerHub",
+ "gitlab",
+ "npm",
+ "codeberg",
+ "linuxServerIO",
+ "gitHubContainerRegistry",
+ "quay",
+ "searchCh",
+ "archiveTeamWarrior",
+] as const;
+
+type RemovedIntegrationKind = (typeof removedIntegrationKinds)[number];
+
+const releaseProviderKinds = [
+ "github",
+ "dockerHub",
+ "gitlab",
+ "npm",
+ "codeberg",
+ "linuxServerIO",
+ "gitHubContainerRegistry",
+ "quay",
+] as const;
+
+const isReleaseProviderKind = (kind: RemovedIntegrationKind): kind is (typeof releaseProviderKinds)[number] =>
+ releaseProviderKinds.some((provider) => provider === kind);
+
+export async function migrateWidgetOnlyIntegrationsToOptionsAsync(db: Database) {
+ const oldIntegrations = await db.query.integrations.findMany({
+ columns: { id: true, kind: true, url: true },
+ });
+ const oldIntegrationById = new Map(
+ oldIntegrations
+ .filter((integration) => removedIntegrationKinds.includes(integration.kind as RemovedIntegrationKind))
+ .map((integration) => [
+ integration.id,
+ { kind: integration.kind as RemovedIntegrationKind, url: integration.url },
+ ]),
+ );
+
+ if (oldIntegrationById.size === 0) return;
+
+ const oldIntegrationIds = Array.from(oldIntegrationById.keys());
+ const oldIntegrationItems = await db.query.integrationItems.findMany();
+ const oldIntegrationItemByItemId = new Map(
+ oldIntegrationItems
+ .filter((item) => oldIntegrationById.has(item.integrationId))
+ .map((item) => [item.itemId, oldIntegrationById.get(item.integrationId)]),
+ );
+ const oldIntegrationItemIdsByItemId = oldIntegrationItems
+ .filter((item) => oldIntegrationById.has(item.integrationId))
+ .reduce