├── .deepsource.toml
├── .dockerignore
├── .env.example
├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── feature_request.yml
│ ├── integration.yml
│ └── widget.yml
├── pull_request_template.md
├── renovate.json5
└── workflows
│ ├── automatic-approval.yml
│ ├── code-quality.yml
│ ├── conventions-semantic-commits.yml
│ ├── conventions-semantic-pull-requests.yml
│ ├── crowdin-schedule-download.yml
│ ├── crowdin-upload.yml
│ ├── deployment-docker-image.yml
│ ├── deployment-weekly-release.yml
│ └── update-contributors.yml
├── .gitignore
├── .npmrc
├── .nvmrc
├── .releaserc.json
├── .run
├── All Tests.run.xml
├── db_migration_mysql_generate.run.xml
├── db_migration_sqlite_generate.run.xml
├── db_push.run.xml
├── db_studio.run.xml
├── dev.run.xml
├── docker_dev.run.xml
├── format.run.xml
├── format_fix.run.xml
├── test.run.xml
├── test_ui.run.xml
└── typecheck.run.xml
├── .vscode
├── extensions.json
├── i18n-ally-custom-framework.yml
├── launch.json
└── settings.json
├── CHANGELOG.md
├── CODEOWNERS
├── Dockerfile
├── LICENSE
├── SECURITY.md
├── apps
├── nextjs
│ ├── README.md
│ ├── eslint.config.js
│ ├── next.config.ts
│ ├── package.json
│ ├── postcss.config.cjs
│ ├── public
│ │ ├── favicon.ico
│ │ ├── images
│ │ │ ├── apps
│ │ │ │ ├── imdb.svg
│ │ │ │ ├── lastfm.svg
│ │ │ │ ├── lidarr.svg
│ │ │ │ ├── nextcloud.svg
│ │ │ │ ├── radarr.svg
│ │ │ │ ├── readarr.svg
│ │ │ │ ├── sonarr.svg
│ │ │ │ ├── the-tvdb.svg
│ │ │ │ ├── tmdb.svg
│ │ │ │ ├── truenas.svg
│ │ │ │ ├── unraid-alt.svg
│ │ │ │ └── vgmdb.svg
│ │ │ ├── kubernetes
│ │ │ │ ├── configmaps.svg
│ │ │ │ ├── ingresses.svg
│ │ │ │ ├── namespaces.svg
│ │ │ │ ├── nodes.svg
│ │ │ │ ├── pods.svg
│ │ │ │ ├── secrets.svg
│ │ │ │ ├── services.svg
│ │ │ │ └── volumes.svg
│ │ │ └── pwa
│ │ │ │ ├── 192.maskable.png
│ │ │ │ └── 512.maskable.png
│ │ └── logo
│ │ │ └── logo.png
│ ├── src
│ │ ├── app
│ │ │ ├── [locale]
│ │ │ │ ├── (home)
│ │ │ │ │ ├── (board)
│ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── not-found.tsx
│ │ │ │ ├── _client-providers
│ │ │ │ │ ├── dayjs-loader.tsx
│ │ │ │ │ ├── jotai.tsx
│ │ │ │ │ ├── mantine.tsx
│ │ │ │ │ ├── session.tsx
│ │ │ │ │ └── trpc.tsx
│ │ │ │ ├── auth
│ │ │ │ │ ├── invite
│ │ │ │ │ │ └── [id]
│ │ │ │ │ │ │ ├── _registration-form.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── login
│ │ │ │ │ │ ├── _login-form.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ ├── boards
│ │ │ │ │ ├── (content)
│ │ │ │ │ │ ├── (home)
│ │ │ │ │ │ │ ├── _definition.ts
│ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── [name]
│ │ │ │ │ │ │ ├── (board)
│ │ │ │ │ │ │ │ ├── _definition.tsx
│ │ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ └── not-found.tsx
│ │ │ │ │ │ ├── _client.tsx
│ │ │ │ │ │ ├── _creator.tsx
│ │ │ │ │ │ ├── _custom-css.tsx
│ │ │ │ │ │ ├── _dynamic-client.tsx
│ │ │ │ │ │ ├── _header-actions.tsx
│ │ │ │ │ │ ├── _ready-context.tsx
│ │ │ │ │ │ ├── _theme.tsx
│ │ │ │ │ │ └── not-found.tsx
│ │ │ │ │ ├── [name]
│ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ └── settings
│ │ │ │ │ │ │ ├── _appereance.tsx
│ │ │ │ │ │ │ ├── _background.tsx
│ │ │ │ │ │ │ ├── _behavior.tsx
│ │ │ │ │ │ │ ├── _board-access.tsx
│ │ │ │ │ │ │ ├── _customCss.tsx
│ │ │ │ │ │ │ ├── _danger.tsx
│ │ │ │ │ │ │ ├── _general.tsx
│ │ │ │ │ │ │ ├── _layout.tsx
│ │ │ │ │ │ │ ├── _shared.tsx
│ │ │ │ │ │ │ ├── customcss.module.css
│ │ │ │ │ │ │ ├── danger.module.css
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── _header-actions.tsx
│ │ │ │ │ ├── _layout-creator.tsx
│ │ │ │ │ └── _types.ts
│ │ │ │ ├── compose.tsx
│ │ │ │ ├── init
│ │ │ │ │ ├── _steps
│ │ │ │ │ │ ├── back.tsx
│ │ │ │ │ │ ├── finish
│ │ │ │ │ │ │ └── init-finish.tsx
│ │ │ │ │ │ ├── group
│ │ │ │ │ │ │ └── init-group.tsx
│ │ │ │ │ │ ├── import
│ │ │ │ │ │ │ ├── file-info-card.tsx
│ │ │ │ │ │ │ ├── import-dropzone.tsx
│ │ │ │ │ │ │ └── init-import.tsx
│ │ │ │ │ │ ├── settings
│ │ │ │ │ │ │ └── init-settings.tsx
│ │ │ │ │ │ ├── start
│ │ │ │ │ │ │ ├── init-start.tsx
│ │ │ │ │ │ │ └── next-button.tsx
│ │ │ │ │ │ └── user
│ │ │ │ │ │ │ ├── init-user-form.tsx
│ │ │ │ │ │ │ └── init-user.tsx
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── manage
│ │ │ │ │ ├── [...not-found]
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── _components
│ │ │ │ │ │ ├── hero-banner.module.css
│ │ │ │ │ │ └── hero-banner.tsx
│ │ │ │ │ ├── about
│ │ │ │ │ │ ├── about.module.css
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── apps
│ │ │ │ │ │ ├── _app-delete-button.tsx
│ │ │ │ │ │ ├── edit
│ │ │ │ │ │ │ └── [id]
│ │ │ │ │ │ │ │ ├── _app-edit-form.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── new
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── boards
│ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ ├── board-card-menu-dropdown.tsx
│ │ │ │ │ │ │ └── create-board-button.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── integrations
│ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ ├── integration-access-settings.tsx
│ │ │ │ │ │ │ ├── secrets
│ │ │ │ │ │ │ │ ├── integration-secret-card.tsx
│ │ │ │ │ │ │ │ ├── integration-secret-icons.ts
│ │ │ │ │ │ │ │ └── integration-secret-inputs.tsx
│ │ │ │ │ │ │ └── test-connection
│ │ │ │ │ │ │ │ ├── integration-test-connection-error.tsx
│ │ │ │ │ │ │ │ ├── test-connection-certificate.tsx
│ │ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── _integration-buttons.tsx
│ │ │ │ │ │ ├── edit
│ │ │ │ │ │ │ └── [id]
│ │ │ │ │ │ │ │ ├── _integration-edit-form.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── new
│ │ │ │ │ │ │ ├── _integration-new-dropdown.tsx
│ │ │ │ │ │ │ ├── _integration-new-form.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── page.module.css
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ ├── medias
│ │ │ │ │ │ ├── _actions
│ │ │ │ │ │ │ ├── copy-media.tsx
│ │ │ │ │ │ │ ├── delete-media.tsx
│ │ │ │ │ │ │ ├── show-all.tsx
│ │ │ │ │ │ │ └── upload-media.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── not-found.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── search-engines
│ │ │ │ │ │ ├── _form.tsx
│ │ │ │ │ │ ├── _search-engine-delete-button.tsx
│ │ │ │ │ │ ├── edit
│ │ │ │ │ │ │ └── [id]
│ │ │ │ │ │ │ │ ├── _search-engine-edit-form.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── new
│ │ │ │ │ │ │ ├── _search-engine-new-form.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── settings
│ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ ├── analytics.settings.tsx
│ │ │ │ │ │ │ ├── appearance-settings-form.tsx
│ │ │ │ │ │ │ ├── board-settings-form.tsx
│ │ │ │ │ │ │ ├── common-form.tsx
│ │ │ │ │ │ │ ├── crawling-and-indexing.settings.tsx
│ │ │ │ │ │ │ ├── culture-settings-form.tsx
│ │ │ │ │ │ │ ├── search-settings-form.tsx
│ │ │ │ │ │ │ └── setting-switch.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ ├── tools
│ │ │ │ │ │ ├── api
│ │ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ │ ├── api-keys.tsx
│ │ │ │ │ │ │ │ ├── copy-api-key-modal.tsx
│ │ │ │ │ │ │ │ └── swagger-ui.tsx
│ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ ├── swagger-ui-dark.css
│ │ │ │ │ │ │ ├── swagger-ui-overrides.css
│ │ │ │ │ │ │ └── swagger-ui.css
│ │ │ │ │ │ ├── certificates
│ │ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ │ ├── add-certificate.tsx
│ │ │ │ │ │ │ │ └── remove-certificate.tsx
│ │ │ │ │ │ │ ├── hostnames
│ │ │ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ │ │ └── remove-hostname.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── docker
│ │ │ │ │ │ │ ├── docker-table.tsx
│ │ │ │ │ │ │ ├── error.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── kubernetes
│ │ │ │ │ │ │ ├── cluster-dashboard
│ │ │ │ │ │ │ │ ├── cluster-dashboard.tsx
│ │ │ │ │ │ │ │ ├── error.tsx
│ │ │ │ │ │ │ │ ├── header-card
│ │ │ │ │ │ │ │ │ ├── header-card.module.css
│ │ │ │ │ │ │ │ │ ├── header-card.tsx
│ │ │ │ │ │ │ │ │ └── header-icon.tsx
│ │ │ │ │ │ │ │ ├── resource-gauge
│ │ │ │ │ │ │ │ │ ├── resource-gauge.module.css
│ │ │ │ │ │ │ │ │ ├── resource-gauge.tsx
│ │ │ │ │ │ │ │ │ └── resource-icon.tsx
│ │ │ │ │ │ │ │ └── resource-tile
│ │ │ │ │ │ │ │ │ ├── resource-tile.module.css
│ │ │ │ │ │ │ │ │ └── resource-tile.tsx
│ │ │ │ │ │ │ ├── configmaps
│ │ │ │ │ │ │ │ ├── configmaps-table.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── ingresses
│ │ │ │ │ │ │ │ ├── ingresses-table.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── namespaces
│ │ │ │ │ │ │ │ ├── namespaces-table.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── nodes
│ │ │ │ │ │ │ │ ├── nodes-table.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ ├── pods
│ │ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ │ └── pods-table.tsx
│ │ │ │ │ │ │ ├── secrets
│ │ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ │ └── secrets-table.tsx
│ │ │ │ │ │ │ ├── services
│ │ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ │ └── services-table.tsx
│ │ │ │ │ │ │ └── volumes
│ │ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ │ └── volumes-table.tsx
│ │ │ │ │ │ ├── logs
│ │ │ │ │ │ │ ├── client.tsx
│ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ ├── terminal.module.css
│ │ │ │ │ │ │ └── terminal.tsx
│ │ │ │ │ │ └── tasks
│ │ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ └── jobs-list.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ └── users
│ │ │ │ │ │ ├── [userId]
│ │ │ │ │ │ ├── access.ts
│ │ │ │ │ │ ├── general
│ │ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ │ ├── _change-home-board.tsx
│ │ │ │ │ │ │ │ ├── _change-search-preferences.tsx
│ │ │ │ │ │ │ │ ├── _delete-user-button.tsx
│ │ │ │ │ │ │ │ ├── _first-day-of-week.tsx
│ │ │ │ │ │ │ │ ├── _ping-icons-enabled.tsx
│ │ │ │ │ │ │ │ ├── _profile-avatar-form.tsx
│ │ │ │ │ │ │ │ └── _profile-form.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ └── security
│ │ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ └── _change-password-form.tsx
│ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── _components
│ │ │ │ │ │ └── user-list.tsx
│ │ │ │ │ │ ├── create
│ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ ├── create-user-stepper.tsx
│ │ │ │ │ │ │ └── stepper-navigation.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── groups
│ │ │ │ │ │ ├── [id]
│ │ │ │ │ │ │ ├── _delete-group.tsx
│ │ │ │ │ │ │ ├── _navigation.tsx
│ │ │ │ │ │ │ ├── _rename-group-form.tsx
│ │ │ │ │ │ │ ├── _reserved-group-alert.tsx
│ │ │ │ │ │ │ ├── _transfer-group-ownership.tsx
│ │ │ │ │ │ │ ├── layout.tsx
│ │ │ │ │ │ │ ├── members
│ │ │ │ │ │ │ │ ├── _add-group-member.tsx
│ │ │ │ │ │ │ │ ├── _remove-group-member.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ ├── page.tsx
│ │ │ │ │ │ │ ├── permissions
│ │ │ │ │ │ │ │ ├── _group-permission-form.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ │ └── settings
│ │ │ │ │ │ │ │ ├── _group-home-boards.tsx
│ │ │ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── _client.tsx
│ │ │ │ │ │ ├── _groups-table.tsx
│ │ │ │ │ │ ├── groups.module.css
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ ├── invites
│ │ │ │ │ │ ├── _components
│ │ │ │ │ │ │ └── invite-list.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ │ │ └── page.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ └── widgets
│ │ │ │ │ └── [kind]
│ │ │ │ │ ├── _content.tsx
│ │ │ │ │ ├── _dimension-modal.tsx
│ │ │ │ │ ├── layout.tsx
│ │ │ │ │ └── page.tsx
│ │ │ ├── api
│ │ │ │ ├── [...trpc]
│ │ │ │ │ └── route.ts
│ │ │ │ ├── about
│ │ │ │ │ └── contributors
│ │ │ │ │ │ ├── crowdin
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ │ └── github
│ │ │ │ │ │ └── route.ts
│ │ │ │ ├── auth
│ │ │ │ │ └── [...nextauth]
│ │ │ │ │ │ └── route.ts
│ │ │ │ ├── health
│ │ │ │ │ ├── live
│ │ │ │ │ │ └── route.ts
│ │ │ │ │ └── ready
│ │ │ │ │ │ └── route.ts
│ │ │ │ ├── openapi
│ │ │ │ │ └── route.ts
│ │ │ │ ├── trpc
│ │ │ │ │ └── [trpc]
│ │ │ │ │ │ └── route.ts
│ │ │ │ └── user-medias
│ │ │ │ │ └── [id]
│ │ │ │ │ └── route.ts
│ │ │ └── manifest.ts
│ │ ├── components
│ │ │ ├── access
│ │ │ │ ├── access-settings.tsx
│ │ │ │ ├── access-table-rows.tsx
│ │ │ │ ├── context.tsx
│ │ │ │ ├── form.ts
│ │ │ │ ├── group-access-form.tsx
│ │ │ │ ├── group-select-modal.tsx
│ │ │ │ ├── inherit-access-table.tsx
│ │ │ │ ├── user-access-form.tsx
│ │ │ │ └── user-select-modal.tsx
│ │ │ ├── active-tab-accordion.tsx
│ │ │ ├── board
│ │ │ │ ├── board-select.tsx
│ │ │ │ ├── items
│ │ │ │ │ ├── actions
│ │ │ │ │ │ ├── create-item.ts
│ │ │ │ │ │ ├── duplicate-item.ts
│ │ │ │ │ │ ├── empty-position.ts
│ │ │ │ │ │ ├── move-and-resize-item.ts
│ │ │ │ │ │ ├── move-item-to-section.ts
│ │ │ │ │ │ ├── remove-item.ts
│ │ │ │ │ │ ├── section-elements.ts
│ │ │ │ │ │ └── test
│ │ │ │ │ │ │ ├── create-item.spec.ts
│ │ │ │ │ │ │ ├── duplicate-item.spec.ts
│ │ │ │ │ │ │ ├── empty-position.spec.ts
│ │ │ │ │ │ │ ├── mocks
│ │ │ │ │ │ │ ├── board-mock.ts
│ │ │ │ │ │ │ ├── category-section-mock.ts
│ │ │ │ │ │ │ ├── dynamic-section-mock.ts
│ │ │ │ │ │ │ ├── empty-section-mock.ts
│ │ │ │ │ │ │ ├── item-mock.ts
│ │ │ │ │ │ │ └── layout-mock.ts
│ │ │ │ │ │ │ ├── move-and-resize-item.spec.ts
│ │ │ │ │ │ │ ├── move-item-to-section.spec.ts
│ │ │ │ │ │ │ └── remove-item.spec.ts
│ │ │ │ │ ├── item-actions.tsx
│ │ │ │ │ ├── item-content.module.css
│ │ │ │ │ ├── item-content.tsx
│ │ │ │ │ ├── item-menu.tsx
│ │ │ │ │ ├── item-move-modal.tsx
│ │ │ │ │ └── item-select-modal.tsx
│ │ │ │ ├── modals
│ │ │ │ │ └── board-rename-modal.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ ├── permissions
│ │ │ │ │ ├── client.ts
│ │ │ │ │ └── server.ts
│ │ │ │ └── sections
│ │ │ │ │ ├── category-section.tsx
│ │ │ │ │ ├── category
│ │ │ │ │ ├── actions
│ │ │ │ │ │ ├── move-category.ts
│ │ │ │ │ │ ├── remove-category.ts
│ │ │ │ │ │ └── test
│ │ │ │ │ │ │ ├── move-category.spec.ts
│ │ │ │ │ │ │ └── remove-category.spec.ts
│ │ │ │ │ ├── category-actions.ts
│ │ │ │ │ ├── category-edit-modal.tsx
│ │ │ │ │ ├── category-menu-actions.tsx
│ │ │ │ │ ├── category-menu.tsx
│ │ │ │ │ └── filter.ts
│ │ │ │ │ ├── content.tsx
│ │ │ │ │ ├── dynamic-section.tsx
│ │ │ │ │ ├── dynamic
│ │ │ │ │ ├── actions
│ │ │ │ │ │ ├── add-dynamic-section.ts
│ │ │ │ │ │ └── remove-dynamic-section.ts
│ │ │ │ │ ├── dynamic-actions.ts
│ │ │ │ │ ├── dynamic-edit-modal.tsx
│ │ │ │ │ └── dynamic-menu.tsx
│ │ │ │ │ ├── empty-section.tsx
│ │ │ │ │ ├── gridstack
│ │ │ │ │ ├── gridstack-item.tsx
│ │ │ │ │ ├── gridstack.tsx
│ │ │ │ │ ├── init-gridstack.ts
│ │ │ │ │ └── use-gridstack.ts
│ │ │ │ │ ├── item.module.css
│ │ │ │ │ ├── section-actions.tsx
│ │ │ │ │ ├── section-context.ts
│ │ │ │ │ └── use-section-items.ts
│ │ │ ├── color-scheme
│ │ │ │ └── current-color-scheme-combobox.tsx
│ │ │ ├── language
│ │ │ │ ├── current-language-combobox.tsx
│ │ │ │ ├── language-combobox.module.css
│ │ │ │ └── language-combobox.tsx
│ │ │ ├── layout
│ │ │ │ ├── analytics.tsx
│ │ │ │ ├── background.tsx
│ │ │ │ ├── header.tsx
│ │ │ │ ├── header
│ │ │ │ │ ├── burger.tsx
│ │ │ │ │ ├── button.tsx
│ │ │ │ │ ├── search.module.css
│ │ │ │ │ ├── search.tsx
│ │ │ │ │ ├── update.tsx
│ │ │ │ │ └── user.tsx
│ │ │ │ ├── logo
│ │ │ │ │ ├── board-logo.tsx
│ │ │ │ │ ├── homarr-logo.tsx
│ │ │ │ │ └── logo.tsx
│ │ │ │ ├── navigation-link.tsx
│ │ │ │ ├── navigation.tsx
│ │ │ │ ├── search-engine-optimization.tsx
│ │ │ │ └── shell.tsx
│ │ │ ├── manage
│ │ │ │ ├── danger-zone.tsx
│ │ │ │ ├── manage-container.tsx
│ │ │ │ └── mobile-affix-button.tsx
│ │ │ ├── navigation
│ │ │ │ └── dynamic-breadcrumb.tsx
│ │ │ ├── no-results.tsx
│ │ │ ├── user-avatar-menu.tsx
│ │ │ └── user-avatar.tsx
│ │ ├── constants.ts
│ │ ├── errors
│ │ │ └── trpc-catch-error.ts
│ │ ├── metadata.ts
│ │ ├── middleware.ts
│ │ ├── react-app-env.d.ts
│ │ ├── styles
│ │ │ ├── gridstack.scss
│ │ │ ├── prismjs.scss
│ │ │ └── scroll-area.scss
│ │ ├── theme
│ │ │ └── color-scheme.ts
│ │ └── versions
│ │ │ └── package-reader.ts
│ └── tsconfig.json
├── tasks
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── main.ts
│ │ └── undici-log-agent-override.ts
│ └── tsconfig.json
└── websocket
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ └── main.ts
│ └── tsconfig.json
├── crowdin.yml
├── development
├── build.cmd
├── development.docker-compose.yml
└── docker-run.cmd
├── docs
├── README.md
├── banner.png
├── installation-button.png
├── section-contribute.png
├── section-features.png
├── section-installation.png
├── section-preview.png
└── section-widgets-and-integrations.png
├── e2e
├── health-checks.spec.ts
├── home.spec.ts
├── lldap.spec.ts
├── onboarding.spec.ts
└── shared
│ ├── actions
│ └── onboarding-actions.ts
│ ├── assertions
│ └── onboarding-assertions.ts
│ ├── create-homarr-container.ts
│ └── e2e-db.ts
├── nginx.conf
├── package.json
├── packages
├── analytics
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ └── send-server-analytics.ts
│ └── tsconfig.json
├── api
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── client.ts
│ │ ├── env.ts
│ │ ├── index.ts
│ │ ├── middlewares
│ │ │ ├── docker.ts
│ │ │ ├── integration.ts
│ │ │ ├── item.ts
│ │ │ └── kubernetes.ts
│ │ ├── open-api.ts
│ │ ├── root.ts
│ │ ├── router
│ │ │ ├── apiKeys.ts
│ │ │ ├── app.ts
│ │ │ ├── app
│ │ │ │ └── app-access-control.ts
│ │ │ ├── board.ts
│ │ │ ├── board
│ │ │ │ ├── board-access.ts
│ │ │ │ ├── grid-algorithm.ts
│ │ │ │ └── test
│ │ │ │ │ └── grid-algorithm.spec.ts
│ │ │ ├── certificates
│ │ │ │ └── certificate-router.ts
│ │ │ ├── cron-jobs.ts
│ │ │ ├── docker
│ │ │ │ └── docker-router.ts
│ │ │ ├── group.ts
│ │ │ ├── home.ts
│ │ │ ├── icons.ts
│ │ │ ├── import
│ │ │ │ └── import-router.ts
│ │ │ ├── integration
│ │ │ │ ├── integration-access.ts
│ │ │ │ ├── integration-router.ts
│ │ │ │ ├── integration-test-connection.ts
│ │ │ │ └── map-test-connection-error.ts
│ │ │ ├── invite.ts
│ │ │ ├── invite
│ │ │ │ └── checks.ts
│ │ │ ├── kubernetes
│ │ │ │ ├── kubernetes-client.ts
│ │ │ │ ├── resource-parser
│ │ │ │ │ ├── cpu-resource-parser.ts
│ │ │ │ │ ├── memory-resource-parser.ts
│ │ │ │ │ └── resource-parser.ts
│ │ │ │ └── router
│ │ │ │ │ ├── cluster.ts
│ │ │ │ │ ├── configMaps.ts
│ │ │ │ │ ├── ingresses.ts
│ │ │ │ │ ├── kubernetes-router.ts
│ │ │ │ │ ├── namespaces.ts
│ │ │ │ │ ├── nodes.ts
│ │ │ │ │ ├── pods.ts
│ │ │ │ │ ├── secrets.ts
│ │ │ │ │ ├── services.ts
│ │ │ │ │ └── volumes.ts
│ │ │ ├── location.ts
│ │ │ ├── log.ts
│ │ │ ├── medias
│ │ │ │ └── media-router.ts
│ │ │ ├── onboard
│ │ │ │ ├── onboard-queries.ts
│ │ │ │ └── onboard-router.ts
│ │ │ ├── search-engine
│ │ │ │ └── search-engine-router.ts
│ │ │ ├── section
│ │ │ │ └── section-router.ts
│ │ │ ├── serverSettings.ts
│ │ │ ├── test
│ │ │ │ ├── app.spec.ts
│ │ │ │ ├── board.spec.ts
│ │ │ │ ├── board
│ │ │ │ │ └── board-access.spec.ts
│ │ │ │ ├── docker
│ │ │ │ │ └── docker-router.spec.ts
│ │ │ │ ├── group.spec.ts
│ │ │ │ ├── helper.ts
│ │ │ │ ├── integration
│ │ │ │ │ ├── integration-access.spec.ts
│ │ │ │ │ ├── integration-router.spec.ts
│ │ │ │ │ └── integration-test-connection.spec.ts
│ │ │ │ ├── invite.spec.ts
│ │ │ │ ├── kubernetes
│ │ │ │ │ └── resource-parser
│ │ │ │ │ │ ├── cpu-resource-parser.spec.ts
│ │ │ │ │ │ └── memory-resource-parser.spec.ts
│ │ │ │ ├── serverSettings.spec.ts
│ │ │ │ ├── user.spec.ts
│ │ │ │ └── widgets
│ │ │ │ │ └── app.spec.ts
│ │ │ ├── update-checker.ts
│ │ │ ├── user.ts
│ │ │ ├── user
│ │ │ │ └── change-search-preferences.ts
│ │ │ └── widgets
│ │ │ │ ├── app.ts
│ │ │ │ ├── calendar.ts
│ │ │ │ ├── dns-hole.ts
│ │ │ │ ├── downloads.ts
│ │ │ │ ├── health-monitoring.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── indexer-manager.ts
│ │ │ │ ├── media-requests.ts
│ │ │ │ ├── media-server.ts
│ │ │ │ ├── media-transcoding.ts
│ │ │ │ ├── minecraft.ts
│ │ │ │ ├── network-controller.ts
│ │ │ │ ├── notebook.ts
│ │ │ │ ├── options.ts
│ │ │ │ ├── releases.ts
│ │ │ │ ├── rssFeed.ts
│ │ │ │ ├── smart-home.ts
│ │ │ │ ├── stocks.ts
│ │ │ │ └── weather.ts
│ │ ├── schema-merger.ts
│ │ ├── server.ts
│ │ ├── shared.ts
│ │ ├── test
│ │ │ └── open-api.spec.ts
│ │ ├── trpc.ts
│ │ └── websocket.ts
│ └── tsconfig.json
├── auth
│ ├── adapter.ts
│ ├── callbacks.ts
│ ├── client.ts
│ ├── configuration.ts
│ ├── env.ts
│ ├── eslint.config.js
│ ├── events.ts
│ ├── index.ts
│ ├── next.ts
│ ├── package.json
│ ├── permissions
│ │ ├── board-permissions.ts
│ │ ├── index.ts
│ │ ├── integration-permissions.ts
│ │ ├── integration-provider.tsx
│ │ ├── integration-query-permissions.ts
│ │ ├── integrations-with-permissions.ts
│ │ └── test
│ │ │ ├── board-permissions.spec.ts
│ │ │ ├── integration-permissions.spec.ts
│ │ │ └── integration-query-permissions.spec.ts
│ ├── providers
│ │ ├── check-provider.ts
│ │ ├── credentials
│ │ │ ├── authorization
│ │ │ │ ├── basic-authorization.ts
│ │ │ │ └── ldap-authorization.ts
│ │ │ ├── credentials-provider.ts
│ │ │ └── ldap-client.ts
│ │ ├── empty
│ │ │ └── empty-provider.ts
│ │ ├── filter-providers.ts
│ │ ├── oidc
│ │ │ └── oidc-provider.ts
│ │ └── test
│ │ │ ├── basic-authorization.spec.ts
│ │ │ └── ldap-authorization.spec.ts
│ ├── redirect.ts
│ ├── security.ts
│ ├── server.ts
│ ├── session.ts
│ ├── shared.ts
│ ├── test
│ │ ├── adapter.spec.ts
│ │ ├── callbacks.spec.ts
│ │ ├── events.spec.ts
│ │ ├── redirect.spec.ts
│ │ ├── security.spec.ts
│ │ └── session.spec.ts
│ └── tsconfig.json
├── boards
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── context.tsx
│ │ ├── edit-mode.tsx
│ │ └── updater.ts
│ └── tsconfig.json
├── certificates
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ └── server.ts
│ └── tsconfig.json
├── cli
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── commands
│ │ │ ├── fix-usernames.ts
│ │ │ ├── recreate-admin.ts
│ │ │ └── reset-password.ts
│ │ └── index.ts
│ └── tsconfig.json
├── common
│ ├── env.ts
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── array.ts
│ │ ├── client.ts
│ │ ├── cookie.ts
│ │ ├── date.ts
│ │ ├── encryption.ts
│ │ ├── env-validation.ts
│ │ ├── error.ts
│ │ ├── errors
│ │ │ ├── http
│ │ │ │ ├── handlers
│ │ │ │ │ ├── axios-http-error-handler.ts
│ │ │ │ │ ├── fetch-http-error-handler.ts
│ │ │ │ │ ├── http-error-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── ofetch-http-error-handler.ts
│ │ │ │ │ └── tsdav-http-error-handler.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── request-error.ts
│ │ │ │ └── response-error.ts
│ │ │ ├── index.ts
│ │ │ └── parse
│ │ │ │ ├── handlers
│ │ │ │ ├── index.ts
│ │ │ │ ├── json-parse-error-handler.ts
│ │ │ │ ├── parse-error-handler.ts
│ │ │ │ └── zod-parse-error-handler.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── parse-error.ts
│ │ ├── fetch-agent.ts
│ │ ├── fetch-with-timeout.ts
│ │ ├── function.ts
│ │ ├── hooks.ts
│ │ ├── index.ts
│ │ ├── number.ts
│ │ ├── object.ts
│ │ ├── revalidate-path-action.ts
│ │ ├── security.ts
│ │ ├── server.ts
│ │ ├── stopwatch.ts
│ │ ├── string.ts
│ │ ├── test
│ │ │ ├── array.spec.ts
│ │ │ ├── date.spec.ts
│ │ │ ├── error.spec.ts
│ │ │ ├── fetch-agent.spec.ts
│ │ │ ├── object.spec.ts
│ │ │ ├── string.spec.ts
│ │ │ └── url.spec.ts
│ │ ├── theme.ts
│ │ ├── types.ts
│ │ ├── url.ts
│ │ └── user-agent.ts
│ └── tsconfig.json
├── cron-job-runner
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── register.ts
│ └── tsconfig.json
├── cron-job-status
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── publisher.ts
│ └── tsconfig.json
├── cron-jobs-core
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── creator.ts
│ │ ├── expressions.ts
│ │ ├── group.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ ├── registry.ts
│ │ └── validation.ts
│ └── tsconfig.json
├── cron-jobs
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── jobs
│ │ │ ├── analytics.ts
│ │ │ ├── docker.ts
│ │ │ ├── icons-updater.ts
│ │ │ ├── integrations
│ │ │ │ ├── dns-hole.ts
│ │ │ │ ├── downloads.ts
│ │ │ │ ├── health-monitoring.ts
│ │ │ │ ├── home-assistant.ts
│ │ │ │ ├── indexer-manager.ts
│ │ │ │ ├── media-organizer.ts
│ │ │ │ ├── media-requests.ts
│ │ │ │ ├── media-server.ts
│ │ │ │ ├── media-transcoding.ts
│ │ │ │ └── network-controller.ts
│ │ │ ├── minecraft-server-status.ts
│ │ │ ├── ping.ts
│ │ │ ├── rss-feeds.ts
│ │ │ ├── session-cleanup.ts
│ │ │ └── update-checker.ts
│ │ └── lib
│ │ │ └── index.ts
│ └── tsconfig.json
├── db
│ ├── client.ts
│ ├── collection.ts
│ ├── configs
│ │ ├── mysql.config.ts
│ │ └── sqlite.config.ts
│ ├── driver.ts
│ ├── env.ts
│ ├── eslint.config.js
│ ├── index.ts
│ ├── migrations
│ │ ├── mysql
│ │ │ ├── 0000_harsh_photon.sql
│ │ │ ├── 0001_wild_alex_wilder.sql
│ │ │ ├── 0002_flimsy_deathbird.sql
│ │ │ ├── 0003_freezing_black_panther.sql
│ │ │ ├── 0004_noisy_giant_girl.sql
│ │ │ ├── 0005_soft_microbe.sql
│ │ │ ├── 0006_young_micromax.sql
│ │ │ ├── 0007_boring_nocturne.sql
│ │ │ ├── 0008_far_lifeguard.sql
│ │ │ ├── 0009_wakeful_tenebrous.sql
│ │ │ ├── 0010_melted_pestilence.sql
│ │ │ ├── 0011_freezing_banshee.sql
│ │ │ ├── 0012_abnormal_wendell_vaughn.sql
│ │ │ ├── 0013_youthful_vulture.sql
│ │ │ ├── 0014_bizarre_red_shift.sql
│ │ │ ├── 0015_unknown_firedrake.sql
│ │ │ ├── 0016_change_all_to_snake_case.sql
│ │ │ ├── 0017_tired_penance.sql
│ │ │ ├── 0018_mighty_shaman.sql
│ │ │ ├── 0019_crazy_marvel_zombies.sql
│ │ │ ├── 0020_salty_doorman.sql
│ │ │ ├── 0021_fluffy_jocasta.sql
│ │ │ ├── 0022_famous_otto_octavius.sql
│ │ │ ├── 0023_fix_on_delete_actions.sql
│ │ │ ├── 0024_mean_vin_gonzales.sql
│ │ │ ├── 0025_add-group-home-board-settings.sql
│ │ │ ├── 0026_add-border-radius.sql
│ │ │ ├── 0027_acoustic_karma.sql
│ │ │ ├── 0028_add_app_ping_url.sql
│ │ │ ├── 0029_add_layouts.sql
│ │ │ ├── 0030_migrate_item_and_section_for_layouts.sql
│ │ │ ├── 0031_add_dynamic_section_options.sql
│ │ │ ├── 0032_add_trusted_certificate_hostnames.sql
│ │ │ ├── meta
│ │ │ │ ├── 0000_snapshot.json
│ │ │ │ ├── 0001_snapshot.json
│ │ │ │ ├── 0002_snapshot.json
│ │ │ │ ├── 0003_snapshot.json
│ │ │ │ ├── 0004_snapshot.json
│ │ │ │ ├── 0005_snapshot.json
│ │ │ │ ├── 0006_snapshot.json
│ │ │ │ ├── 0007_snapshot.json
│ │ │ │ ├── 0008_snapshot.json
│ │ │ │ ├── 0009_snapshot.json
│ │ │ │ ├── 0010_snapshot.json
│ │ │ │ ├── 0011_snapshot.json
│ │ │ │ ├── 0012_snapshot.json
│ │ │ │ ├── 0013_snapshot.json
│ │ │ │ ├── 0014_snapshot.json
│ │ │ │ ├── 0015_snapshot.json
│ │ │ │ ├── 0016_snapshot.json
│ │ │ │ ├── 0017_snapshot.json
│ │ │ │ ├── 0018_snapshot.json
│ │ │ │ ├── 0019_snapshot.json
│ │ │ │ ├── 0020_snapshot.json
│ │ │ │ ├── 0021_snapshot.json
│ │ │ │ ├── 0022_snapshot.json
│ │ │ │ ├── 0023_snapshot.json
│ │ │ │ ├── 0024_snapshot.json
│ │ │ │ ├── 0025_snapshot.json
│ │ │ │ ├── 0026_snapshot.json
│ │ │ │ ├── 0027_snapshot.json
│ │ │ │ ├── 0028_snapshot.json
│ │ │ │ ├── 0029_snapshot.json
│ │ │ │ ├── 0030_snapshot.json
│ │ │ │ ├── 0031_snapshot.json
│ │ │ │ ├── 0032_snapshot.json
│ │ │ │ └── _journal.json
│ │ │ └── migrate.ts
│ │ ├── run-seed.ts
│ │ ├── seed.ts
│ │ └── sqlite
│ │ │ ├── 0000_talented_ben_parker.sql
│ │ │ ├── 0001_mixed_titanium_man.sql
│ │ │ ├── 0002_cooing_sumo.sql
│ │ │ ├── 0003_adorable_raider.sql
│ │ │ ├── 0004_peaceful_red_ghost.sql
│ │ │ ├── 0005_lean_random.sql
│ │ │ ├── 0006_windy_doctor_faustus.sql
│ │ │ ├── 0007_known_ultragirl.sql
│ │ │ ├── 0008_third_thor.sql
│ │ │ ├── 0009_stale_roulette.sql
│ │ │ ├── 0010_gorgeous_stingray.sql
│ │ │ ├── 0011_classy_angel.sql
│ │ │ ├── 0012_ambiguous_black_panther.sql
│ │ │ ├── 0013_faithful_hex.sql
│ │ │ ├── 0014_colorful_cargill.sql
│ │ │ ├── 0015_superb_psylocke.sql
│ │ │ ├── 0016_change_all_to_snake_case.sql
│ │ │ ├── 0017_small_rumiko_fujikawa.sql
│ │ │ ├── 0018_cheerful_tattoo.sql
│ │ │ ├── 0019_steady_darkhawk.sql
│ │ │ ├── 0020_empty_hellfire_club.sql
│ │ │ ├── 0021_famous_bruce_banner.sql
│ │ │ ├── 0022_modern_sunfire.sql
│ │ │ ├── 0023_fix_on_delete_actions.sql
│ │ │ ├── 0024_bitter_scrambler.sql
│ │ │ ├── 0025_add-group-home-board-settings.sql
│ │ │ ├── 0026_add-border-radius.sql
│ │ │ ├── 0027_wooden_blizzard.sql
│ │ │ ├── 0028_add_app_ping_url.sql
│ │ │ ├── 0029_add_layouts.sql
│ │ │ ├── 0030_migrate_item_and_section_for_layouts.sql
│ │ │ ├── 0031_add_dynamic_section_options.sql
│ │ │ ├── 0032_add_trusted_certificate_hostnames.sql
│ │ │ ├── meta
│ │ │ ├── 0000_snapshot.json
│ │ │ ├── 0001_snapshot.json
│ │ │ ├── 0002_snapshot.json
│ │ │ ├── 0003_snapshot.json
│ │ │ ├── 0004_snapshot.json
│ │ │ ├── 0005_snapshot.json
│ │ │ ├── 0006_snapshot.json
│ │ │ ├── 0007_snapshot.json
│ │ │ ├── 0008_snapshot.json
│ │ │ ├── 0009_snapshot.json
│ │ │ ├── 0010_snapshot.json
│ │ │ ├── 0011_snapshot.json
│ │ │ ├── 0012_snapshot.json
│ │ │ ├── 0013_snapshot.json
│ │ │ ├── 0014_snapshot.json
│ │ │ ├── 0015_snapshot.json
│ │ │ ├── 0016_snapshot.json
│ │ │ ├── 0017_snapshot.json
│ │ │ ├── 0018_snapshot.json
│ │ │ ├── 0019_snapshot.json
│ │ │ ├── 0020_snapshot.json
│ │ │ ├── 0021_snapshot.json
│ │ │ ├── 0022_snapshot.json
│ │ │ ├── 0023_snapshot.json
│ │ │ ├── 0024_snapshot.json
│ │ │ ├── 0025_snapshot.json
│ │ │ ├── 0026_snapshot.json
│ │ │ ├── 0027_snapshot.json
│ │ │ ├── 0028_snapshot.json
│ │ │ ├── 0029_snapshot.json
│ │ │ ├── 0030_snapshot.json
│ │ │ ├── 0031_snapshot.json
│ │ │ ├── 0032_snapshot.json
│ │ │ └── _journal.json
│ │ │ └── migrate.ts
│ ├── package.json
│ ├── queries
│ │ ├── group.ts
│ │ ├── index.ts
│ │ ├── item.ts
│ │ └── server-setting.ts
│ ├── schema
│ │ ├── index.ts
│ │ ├── mysql.ts
│ │ └── sqlite.ts
│ ├── test
│ │ ├── db-mock.ts
│ │ ├── index.ts
│ │ ├── mysql-migration.spec.ts
│ │ └── schema.spec.ts
│ ├── transactions.ts
│ ├── tsconfig.json
│ └── validationSchemas.ts
├── definitions
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── _definition.ts
│ │ ├── auth.ts
│ │ ├── board.ts
│ │ ├── cookie.ts
│ │ ├── docker.ts
│ │ ├── docs
│ │ │ ├── codegen.ts
│ │ │ ├── homarr-docs-sitemap.ts
│ │ │ └── index.ts
│ │ ├── emptysuperjson.ts
│ │ ├── group.ts
│ │ ├── index.ts
│ │ ├── integration.ts
│ │ ├── kubernetes.ts
│ │ ├── onboarding.ts
│ │ ├── permissions.ts
│ │ ├── search-engine.ts
│ │ ├── section.ts
│ │ ├── test
│ │ │ ├── docs.spec.ts
│ │ │ ├── integration.spec.ts
│ │ │ └── permissions.spec.ts
│ │ ├── user.ts
│ │ └── widget.ts
│ └── tsconfig.json
├── docker
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── env.ts
│ │ ├── index.ts
│ │ ├── shared.ts
│ │ └── singleton.ts
│ └── tsconfig.json
├── env
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ └── schemas.ts
│ └── tsconfig.json
├── form
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── messages.ts
│ │ └── types.ts
│ └── tsconfig.json
├── forms-collection
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── icon-picker
│ │ │ ├── icon-picker.module.css
│ │ │ └── icon-picker.tsx
│ │ ├── index.tsx
│ │ ├── new-app
│ │ │ ├── _app-new-form.tsx
│ │ │ ├── _form.tsx
│ │ │ └── icon-matcher.ts
│ │ └── upload-media
│ │ │ └── upload-media.tsx
│ └── tsconfig.json
├── icons
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── auto-icon-searcher.ts
│ │ ├── icons-fetcher.ts
│ │ ├── local.ts
│ │ ├── repositories
│ │ │ ├── github.icon-repository.ts
│ │ │ ├── icon-repository.ts
│ │ │ ├── jsdelivr.icon-repository.ts
│ │ │ └── local.icon-repository.ts
│ │ └── types
│ │ │ ├── icon-repository-license.ts
│ │ │ ├── index.ts
│ │ │ ├── repository-icon-group.ts
│ │ │ └── repository-icon.ts
│ └── tsconfig.json
├── integrations
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── adguard-home
│ │ │ ├── adguard-home-integration.ts
│ │ │ └── adguard-home-types.ts
│ │ ├── base
│ │ │ ├── creator.ts
│ │ │ ├── errors
│ │ │ │ ├── decorator.ts
│ │ │ │ ├── handler.ts
│ │ │ │ ├── http
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── integration-http-error-handler.ts
│ │ │ │ │ ├── integration-request-error.ts
│ │ │ │ │ └── integration-response-error.ts
│ │ │ │ ├── integration-error.ts
│ │ │ │ ├── integration-unknown-error.ts
│ │ │ │ └── parse
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── integration-parse-error-handler.ts
│ │ │ │ │ └── integration-parse-error.ts
│ │ │ ├── integration.ts
│ │ │ ├── searchable-integration.ts
│ │ │ ├── session-store.ts
│ │ │ ├── test-connection
│ │ │ │ ├── index.ts
│ │ │ │ ├── test-connection-error.ts
│ │ │ │ └── test-connection-service.ts
│ │ │ └── types.ts
│ │ ├── calendar-types.ts
│ │ ├── dashdot
│ │ │ └── dashdot-integration.ts
│ │ ├── download-client
│ │ │ ├── aria2
│ │ │ │ ├── aria2-integration.ts
│ │ │ │ └── aria2-types.ts
│ │ │ ├── deluge
│ │ │ │ └── deluge-integration.ts
│ │ │ ├── nzbget
│ │ │ │ ├── nzbget-integration.ts
│ │ │ │ └── nzbget-types.ts
│ │ │ ├── qbittorrent
│ │ │ │ └── qbittorrent-integration.ts
│ │ │ ├── sabnzbd
│ │ │ │ ├── sabnzbd-integration.ts
│ │ │ │ └── sabnzbd-schema.ts
│ │ │ └── transmission
│ │ │ │ └── transmission-integration.ts
│ │ ├── emby
│ │ │ └── emby-integration.ts
│ │ ├── homeassistant
│ │ │ ├── homeassistant-integration.ts
│ │ │ └── homeassistant-types.ts
│ │ ├── index.ts
│ │ ├── interfaces
│ │ │ ├── dns-hole-summary
│ │ │ │ ├── dns-hole-summary-integration.ts
│ │ │ │ └── dns-hole-summary-types.ts
│ │ │ ├── downloads
│ │ │ │ ├── download-client-data.ts
│ │ │ │ ├── download-client-integration.ts
│ │ │ │ ├── download-client-items.ts
│ │ │ │ └── download-client-status.ts
│ │ │ ├── health-monitoring
│ │ │ │ └── healt-monitoring.ts
│ │ │ ├── indexer-manager
│ │ │ │ └── indexer.ts
│ │ │ ├── media-requests
│ │ │ │ └── media-request.ts
│ │ │ ├── media-server
│ │ │ │ └── session.ts
│ │ │ ├── media-transcoding
│ │ │ │ ├── queue.ts
│ │ │ │ ├── statistics.ts
│ │ │ │ └── workers.ts
│ │ │ └── network-controller-summary
│ │ │ │ ├── network-controller-summary-integration.ts
│ │ │ │ └── network-controller-summary-types.ts
│ │ ├── jellyfin
│ │ │ └── jellyfin-integration.ts
│ │ ├── jellyseerr
│ │ │ └── jellyseerr-integration.ts
│ │ ├── media-organizer
│ │ │ ├── lidarr
│ │ │ │ └── lidarr-integration.ts
│ │ │ ├── media-organizer-integration.ts
│ │ │ ├── radarr
│ │ │ │ └── radarr-integration.ts
│ │ │ ├── readarr
│ │ │ │ └── readarr-integration.ts
│ │ │ └── sonarr
│ │ │ │ └── sonarr-integration.ts
│ │ ├── media-transcoding
│ │ │ ├── tdarr-integration.ts
│ │ │ └── tdarr-validation-schemas.ts
│ │ ├── nextcloud
│ │ │ └── nextcloud.integration.ts
│ │ ├── openmediavault
│ │ │ ├── openmediavault-integration.ts
│ │ │ └── openmediavault-types.ts
│ │ ├── overseerr
│ │ │ └── overseerr-integration.ts
│ │ ├── pi-hole
│ │ │ ├── pi-hole-integration-factory.ts
│ │ │ ├── v5
│ │ │ │ ├── pi-hole-integration-v5.ts
│ │ │ │ └── pi-hole-schemas-v5.ts
│ │ │ └── v6
│ │ │ │ ├── pi-hole-integration-v6.ts
│ │ │ │ └── pi-hole-schemas-v6.ts
│ │ ├── plex
│ │ │ ├── interface.ts
│ │ │ └── plex-integration.ts
│ │ ├── prowlarr
│ │ │ ├── prowlarr-integration.ts
│ │ │ └── prowlarr-types.ts
│ │ ├── proxmox
│ │ │ ├── proxmox-error-handler.ts
│ │ │ ├── proxmox-integration.ts
│ │ │ ├── proxmox-types.ts
│ │ │ └── test
│ │ │ │ └── proxmox-error-handler.spec.ts
│ │ ├── types.ts
│ │ └── unifi-controller
│ │ │ ├── unifi-controller-integration.ts
│ │ │ └── unifi-controller-types.ts
│ ├── test
│ │ ├── aria2.spec.ts
│ │ ├── base.spec.ts
│ │ ├── home-assistant.spec.ts
│ │ ├── nzbget.spec.ts
│ │ ├── pi-hole.spec.ts
│ │ ├── sabnzbd.spec.ts
│ │ └── volumes
│ │ │ ├── home-assistant-config.zip
│ │ │ └── usenet
│ │ │ ├── sabnzbd.ini
│ │ │ └── test_download_100MB.nzb
│ └── tsconfig.json
├── log
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── env.ts
│ │ ├── error.ts
│ │ ├── index.ts
│ │ ├── metadata.ts
│ │ └── redis-transport.ts
│ └── tsconfig.json
├── modals-collection
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── apps
│ │ │ ├── app-select-modal.tsx
│ │ │ ├── index.ts
│ │ │ └── quick-add-app
│ │ │ │ └── quick-add-app-modal.tsx
│ │ ├── boards
│ │ │ ├── add-board-modal.tsx
│ │ │ ├── duplicate-board-modal.tsx
│ │ │ ├── import-board-modal.tsx
│ │ │ └── index.ts
│ │ ├── certificates
│ │ │ ├── add-certificate-modal.tsx
│ │ │ └── index.ts
│ │ ├── docker
│ │ │ ├── add-docker-app-to-homarr.tsx
│ │ │ └── index.ts
│ │ ├── groups
│ │ │ ├── add-group-modal.tsx
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── invites
│ │ │ ├── index.ts
│ │ │ ├── invite-copy-modal.tsx
│ │ │ └── invite-create-modal.tsx
│ │ └── search-engines
│ │ │ ├── index.ts
│ │ │ └── request-media-modal.tsx
│ └── tsconfig.json
├── modals
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── confirm-modal.tsx
│ │ ├── creator.ts
│ │ ├── index.tsx
│ │ ├── reducer.tsx
│ │ └── type.ts
│ └── tsconfig.json
├── notifications
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── index.tsx
│ │ └── styles.css
│ └── tsconfig.json
├── old-import
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── analyse
│ │ │ ├── analyse-oldmarr-import.ts
│ │ │ ├── index.ts
│ │ │ ├── input.ts
│ │ │ └── types.ts
│ │ ├── components
│ │ │ ├── index.ts
│ │ │ ├── initial-oldmarr-import.tsx
│ │ │ ├── initial
│ │ │ │ ├── board-selection-card.tsx
│ │ │ │ ├── import-settings-card.tsx
│ │ │ │ ├── import-summary-card.tsx
│ │ │ │ └── token-modal.tsx
│ │ │ └── shared
│ │ │ │ ├── apps-section.tsx
│ │ │ │ └── sidebar-behaviour-select.tsx
│ │ ├── fix-section-issues.ts
│ │ ├── import-error.ts
│ │ ├── import-sections.ts
│ │ ├── import
│ │ │ ├── collections
│ │ │ │ ├── board-collection.ts
│ │ │ │ ├── integration-collection.ts
│ │ │ │ └── user-collection.ts
│ │ │ ├── import-initial-oldmarr.ts
│ │ │ ├── import-single-oldmarr.ts
│ │ │ ├── index.ts
│ │ │ ├── input.ts
│ │ │ ├── test
│ │ │ │ └── board-collection.spec.ts
│ │ │ └── validate-token.ts
│ │ ├── index.ts
│ │ ├── mappers
│ │ │ ├── map-app.ts
│ │ │ ├── map-board.ts
│ │ │ ├── map-breakpoint.ts
│ │ │ ├── map-colors.ts
│ │ │ ├── map-column-count.ts
│ │ │ ├── map-integration.ts
│ │ │ ├── map-item.ts
│ │ │ ├── map-section.ts
│ │ │ ├── map-user.ts
│ │ │ └── test
│ │ │ │ └── map-integration.spec.ts
│ │ ├── move-widgets-and-apps-merge.ts
│ │ ├── prepare
│ │ │ ├── prepare-apps.ts
│ │ │ ├── prepare-boards.ts
│ │ │ ├── prepare-integrations.ts
│ │ │ ├── prepare-items.ts
│ │ │ ├── prepare-multiple.ts
│ │ │ ├── prepare-sections.ts
│ │ │ └── prepare-single.ts
│ │ ├── settings.ts
│ │ ├── shared.ts
│ │ ├── user-schema.ts
│ │ └── widgets
│ │ │ ├── definitions
│ │ │ ├── bookmark.ts
│ │ │ ├── calendar.ts
│ │ │ ├── common.ts
│ │ │ ├── dashdot.ts
│ │ │ ├── date.ts
│ │ │ ├── dlspeed.ts
│ │ │ ├── dns-hole-controls.ts
│ │ │ ├── dns-hole-summary.ts
│ │ │ ├── health-monitoring.ts
│ │ │ ├── iframe.ts
│ │ │ ├── index.ts
│ │ │ ├── indexer-manager.ts
│ │ │ ├── media-requests-list.ts
│ │ │ ├── media-requests-stats.ts
│ │ │ ├── media-server.ts
│ │ │ ├── media-transcoding.ts
│ │ │ ├── notebook.ts
│ │ │ ├── rss.ts
│ │ │ ├── smart-home-entity-state.ts
│ │ │ ├── smart-home-trigger-automation.ts
│ │ │ ├── torrent-status.ts
│ │ │ ├── usenet.ts
│ │ │ ├── video-stream.ts
│ │ │ └── weather.ts
│ │ │ └── options.ts
│ └── tsconfig.json
├── old-schema
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── app.ts
│ │ ├── config.ts
│ │ ├── index.ts
│ │ ├── setting.ts
│ │ ├── tile.ts
│ │ └── widget.ts
│ └── tsconfig.json
├── ping
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── redis
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── redis.conf
│ ├── src
│ │ ├── index.ts
│ │ └── lib
│ │ │ ├── channel-subscription-tracker.ts
│ │ │ ├── channel.ts
│ │ │ └── connection.ts
│ └── tsconfig.json
├── request-handler
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── calendar.ts
│ │ ├── dns-hole.ts
│ │ ├── docker.ts
│ │ ├── downloads.ts
│ │ ├── health-monitoring.ts
│ │ ├── indexer-manager.ts
│ │ ├── lib
│ │ │ ├── cached-integration-request-handler.ts
│ │ │ ├── cached-request-handler.ts
│ │ │ ├── cached-request-integration-job-handler.ts
│ │ │ └── cached-widget-request-handler.ts
│ │ ├── media-request-list.ts
│ │ ├── media-request-stats.ts
│ │ ├── media-server.ts
│ │ ├── media-transcoding.ts
│ │ ├── minecraft-server-status.ts
│ │ ├── network-controller.ts
│ │ ├── releases-providers.ts
│ │ ├── releases.ts
│ │ ├── rss-feeds.ts
│ │ ├── smart-home-entity-state.ts
│ │ ├── stock-price.ts
│ │ └── update-checker.ts
│ └── tsconfig.json
├── server-settings
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── tsconfig.json
├── settings
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── context.tsx
│ │ └── creator.ts
│ └── tsconfig.json
├── spotlight
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── components
│ │ │ ├── actions
│ │ │ │ ├── children-actions.tsx
│ │ │ │ ├── group-actions.tsx
│ │ │ │ ├── groups
│ │ │ │ │ └── action-group.tsx
│ │ │ │ └── items
│ │ │ │ │ ├── action-item.module.css
│ │ │ │ │ ├── children-action-item.tsx
│ │ │ │ │ └── group-action-item.tsx
│ │ │ ├── no-results.tsx
│ │ │ └── spotlight.tsx
│ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── children.ts
│ │ │ ├── group.ts
│ │ │ ├── interaction.ts
│ │ │ └── mode.ts
│ │ ├── modes
│ │ │ ├── app-integration-board
│ │ │ │ ├── apps-search-group.tsx
│ │ │ │ ├── boards-search-group.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── integrations-search-group.tsx
│ │ │ ├── command
│ │ │ │ ├── children
│ │ │ │ │ ├── language.tsx
│ │ │ │ │ └── new-integration.tsx
│ │ │ │ ├── context-specific-group.tsx
│ │ │ │ ├── global-group.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── external
│ │ │ │ ├── index.tsx
│ │ │ │ └── search-engines-search-group.tsx
│ │ │ ├── home
│ │ │ │ ├── context-specific-group.tsx
│ │ │ │ ├── context.tsx
│ │ │ │ ├── home-search-engine-group.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── index.tsx
│ │ │ ├── page
│ │ │ │ ├── index.tsx
│ │ │ │ └── pages-search-group.tsx
│ │ │ └── user-group
│ │ │ │ ├── groups-search-group.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ └── users-search-group.tsx
│ │ ├── spotlight-store.ts
│ │ └── styles.css
│ └── tsconfig.json
├── translation
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── client
│ │ │ ├── index.ts
│ │ │ ├── use-change-locale.ts
│ │ │ └── use-current-locale.ts
│ │ ├── config.ts
│ │ ├── dayjs.ts
│ │ ├── index.ts
│ │ ├── lang
│ │ │ ├── ca.json
│ │ │ ├── cn.json
│ │ │ ├── cr.json
│ │ │ ├── cs.json
│ │ │ ├── da.json
│ │ │ ├── de-CH.json
│ │ │ ├── de.json
│ │ │ ├── el.json
│ │ │ ├── en-gb.json
│ │ │ ├── en.json
│ │ │ ├── es.json
│ │ │ ├── et.json
│ │ │ ├── fr.json
│ │ │ ├── he.json
│ │ │ ├── hr.json
│ │ │ ├── hu.json
│ │ │ ├── it.json
│ │ │ ├── ja.json
│ │ │ ├── ko.json
│ │ │ ├── lt.json
│ │ │ ├── lv.json
│ │ │ ├── nl.json
│ │ │ ├── no.json
│ │ │ ├── pl.json
│ │ │ ├── pt.json
│ │ │ ├── ro.json
│ │ │ ├── ru.json
│ │ │ ├── sk.json
│ │ │ ├── sl.json
│ │ │ ├── sv.json
│ │ │ ├── tr.json
│ │ │ ├── uk.json
│ │ │ ├── vi.json
│ │ │ └── zh.json
│ │ ├── mantine-react-table
│ │ │ ├── ca.json
│ │ │ ├── en.json
│ │ │ ├── lt.json
│ │ │ ├── lv.json
│ │ │ └── sl.json
│ │ ├── mapping.ts
│ │ ├── middleware.ts
│ │ ├── request.ts
│ │ ├── routing.ts
│ │ ├── server.ts
│ │ └── type.ts
│ └── tsconfig.json
├── ui
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ │ ├── components
│ │ │ ├── beta-badge.tsx
│ │ │ ├── count-badge.module.css
│ │ │ ├── count-badge.tsx
│ │ │ ├── index.tsx
│ │ │ ├── integration-avatar.tsx
│ │ │ ├── masked-image.module.css
│ │ │ ├── masked-image.tsx
│ │ │ ├── masked-or-normal-image.tsx
│ │ │ ├── overflow-badge.tsx
│ │ │ ├── password-input
│ │ │ │ ├── password-input.tsx
│ │ │ │ ├── password-requirement.tsx
│ │ │ │ └── password-requirements-popover.tsx
│ │ │ ├── search-input.tsx
│ │ │ ├── select-with-custom-items.tsx
│ │ │ ├── select-with-description-and-badge.tsx
│ │ │ ├── select-with-description.tsx
│ │ │ ├── table-pagination.tsx
│ │ │ ├── text-multi-select.tsx
│ │ │ ├── user-avatar-group.tsx
│ │ │ └── user-avatar.tsx
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ └── use-translated-mantine-react-table.ts
│ │ ├── index.ts
│ │ ├── styles.css
│ │ ├── theme.ts
│ │ └── theme
│ │ │ └── colors
│ │ │ ├── primary.ts
│ │ │ └── secondary.ts
│ └── tsconfig.json
├── validation
│ ├── eslint.config.js
│ ├── package.json
│ ├── src
│ │ ├── app.ts
│ │ ├── board.ts
│ │ ├── certificates.ts
│ │ ├── common.ts
│ │ ├── enums.ts
│ │ ├── form
│ │ │ └── i18n.ts
│ │ ├── group.ts
│ │ ├── icons.ts
│ │ ├── integration.ts
│ │ ├── media.ts
│ │ ├── permissions.ts
│ │ ├── search-engine.ts
│ │ ├── settings.ts
│ │ ├── shared.ts
│ │ ├── user.ts
│ │ └── widgets
│ │ │ └── media-request.ts
│ └── tsconfig.json
└── widgets
│ ├── eslint.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src
│ ├── _inputs
│ │ ├── common.tsx
│ │ ├── form.ts
│ │ ├── index.ts
│ │ ├── widget-app-input.tsx
│ │ ├── widget-location-input.tsx
│ │ ├── widget-multi-text-input.tsx
│ │ ├── widget-multiReleasesRepositories-input.tsx
│ │ ├── widget-multiselect-input.tsx
│ │ ├── widget-number-input.tsx
│ │ ├── widget-select-input.tsx
│ │ ├── widget-slider-input.tsx
│ │ ├── widget-sortable-item-list-input.tsx
│ │ ├── widget-switch-input.tsx
│ │ └── widget-text-input.tsx
│ ├── app
│ │ ├── app.module.css
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── ping
│ │ │ ├── ping-dot.tsx
│ │ │ └── ping-indicator.tsx
│ │ └── prefetch.ts
│ ├── bookmarks
│ │ ├── add-button.tsx
│ │ ├── app-select-modal.tsx
│ │ ├── bookmark.module.css
│ │ ├── component.tsx
│ │ ├── index.tsx
│ │ └── prefetch.ts
│ ├── calendar
│ │ ├── calendar-event-list.module.css
│ │ ├── calendar-event-list.tsx
│ │ ├── calender-day.tsx
│ │ ├── component.module.css
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── clock
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── definition.ts
│ ├── dns-hole
│ │ ├── controls
│ │ │ ├── TimerModal.tsx
│ │ │ ├── component.module.css
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ │ └── summary
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ ├── docker
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── downloads
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── errors
│ │ ├── base-component.tsx
│ │ ├── base.ts
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── no-data-integration.tsx
│ │ └── no-integration-selected.tsx
│ ├── health-monitoring
│ │ ├── cluster
│ │ │ ├── cluster-health.tsx
│ │ │ ├── resource-accordion-item.tsx
│ │ │ ├── resource-popover.tsx
│ │ │ └── resource-table.tsx
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── rings
│ │ │ ├── cpu-ring.tsx
│ │ │ ├── cpu-temp-ring.tsx
│ │ │ └── memory-ring.tsx
│ │ ├── system-health.module.css
│ │ └── system-health.tsx
│ ├── iframe
│ │ ├── component.module.css
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── import.ts
│ ├── index.tsx
│ ├── indexer-manager
│ │ ├── component.module.css
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── media-requests
│ │ ├── list
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ │ └── stats
│ │ │ ├── component.module.css
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ ├── media-server
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── media-transcoding
│ │ ├── component.tsx
│ │ ├── health-check-status.tsx
│ │ ├── index.ts
│ │ └── panels
│ │ │ ├── queue.panel.tsx
│ │ │ ├── statistics.panel.tsx
│ │ │ └── workers.panel.tsx
│ ├── minecraft
│ │ └── server-status
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ ├── modals
│ │ ├── index.ts
│ │ ├── widget-advanced-options-modal.tsx
│ │ └── widget-edit-modal.tsx
│ ├── network-controller
│ │ ├── network-status
│ │ │ ├── component.tsx
│ │ │ ├── index.ts
│ │ │ └── variants
│ │ │ │ ├── stat-row.tsx
│ │ │ │ ├── wifi-variant.tsx
│ │ │ │ └── wired-variant.tsx
│ │ └── summary
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ ├── notebook
│ │ ├── component.tsx
│ │ ├── default-content.ts
│ │ ├── index.ts
│ │ ├── notebook.css
│ │ └── notebook.tsx
│ ├── options.ts
│ ├── prefetch.ts
│ ├── releases
│ │ ├── component.module.scss
│ │ ├── component.tsx
│ │ ├── index.ts
│ │ ├── releases-providers.ts
│ │ └── releases-repository.ts
│ ├── rssFeed
│ │ ├── component.module.scss
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── smart-home
│ │ ├── entity-state
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ │ └── execute-automation
│ │ │ ├── component.tsx
│ │ │ └── index.ts
│ ├── stocks
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── test
│ │ └── translation.spec.ts
│ ├── video
│ │ ├── component.module.css
│ │ ├── component.tsx
│ │ └── index.ts
│ ├── weather
│ │ ├── component.tsx
│ │ ├── icon.tsx
│ │ └── index.ts
│ ├── widget-integration-select.module.css
│ ├── widget-integration-select.tsx
│ └── widgets-common.css
│ └── tsconfig.json
├── patches
└── @types__node-unifi.patch
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── scripts
├── entrypoint.sh
├── run.sh
└── update-contributors.mjs
├── static-data
├── contributors.json
└── translators.json
├── tooling
├── eslint
│ ├── base.js
│ ├── eslint.config.js
│ ├── nextjs.js
│ ├── package.json
│ ├── react.js
│ ├── tsconfig.json
│ └── types.d.ts
├── github
│ ├── package.json
│ └── setup
│ │ └── action.yml
├── prettier
│ ├── index.mjs
│ ├── package.json
│ └── tsconfig.json
└── typescript
│ ├── base.json
│ └── package.json
├── tsconfig.e2e.json
├── turbo.json
├── turbo
└── generators
│ ├── config.ts
│ └── templates
│ ├── eslint.config.js.hbs
│ ├── package.json.hbs
│ └── tsconfig.json.hbs
├── vitest.config.mts
└── vitest.setup.ts
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | [[analyzers]]
4 | name = "javascript"
5 |
6 | [analyzers.meta]
7 | plugins = ["react"]
8 | environment = ["nodejs"]
9 |
10 | [[transformers]]
11 | name = "prettier"
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | node_modules
4 | **/node_modules
5 | npm-debug.log
6 | README.md
7 | .next
8 | .git
9 | dev
10 | .build
11 | e2e
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | open_collective: homarr
4 |
--------------------------------------------------------------------------------
/.github/renovate.json5:
--------------------------------------------------------------------------------
1 | {
2 | $schema: "https://docs.renovatebot.com/renovate-schema.json",
3 | extends: ["config:recommended"],
4 | packageRules: [
5 | {
6 | matchPackagePatterns: ["^@homarr/"],
7 | enabled: false,
8 | },
9 | {
10 | matchUpdateTypes: ["minor", "patch", "pin", "digest"],
11 | automerge: true,
12 | },
13 | ],
14 | updateInternalDeps: true,
15 | rangeStrategy: "bump",
16 | automerge: false,
17 | baseBranches: ["dev"],
18 | dependencyDashboard: false,
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/conventions-semantic-commits.yml:
--------------------------------------------------------------------------------
1 | # https://github.com/webiny/action-conventional-commits?tab=readme-ov-file
2 | name: "[Conventions] Semantic Commits"
3 |
4 | on:
5 | pull_request:
6 | branches: [ dev ]
7 |
8 | jobs:
9 | build:
10 | name: Conventional Commits
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 | - uses: webiny/action-conventional-commits@v1.3.0
--------------------------------------------------------------------------------
/.github/workflows/conventions-semantic-pull-requests.yml:
--------------------------------------------------------------------------------
1 | name: "[Conventions] Semantic PRs"
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - synchronize
9 |
10 | permissions:
11 | pull-requests: read
12 |
13 | jobs:
14 | validate-pull-request-title:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: amannn/action-semantic-pull-request@v5
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # Expo doesn't play nice with pnpm by default.
2 | # The symbolic links of pnpm break the rules of Expo monorepos.
3 | # @link https://docs.expo.dev/guides/monorepos/#common-issues
4 | node-linker=hoisted
5 | strict-peer-dependencies=false
6 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.16.0
2 |
--------------------------------------------------------------------------------
/.run/All Tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.run/db_migration_mysql_generate.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/db_migration_sqlite_generate.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/db_push.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/db_studio.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/dev.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/docker_dev.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/format.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/format_fix.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/test.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/test_ui.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.run/typecheck.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "esbenp.prettier-vscode",
5 | "yoavbls.pretty-ts-errors",
6 | "million.million-lint",
7 | "lokalise.i18n-ally"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @homarr-labs/maintainers
2 |
--------------------------------------------------------------------------------
/apps/nextjs/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 | import nextjsConfig from "@homarr/eslint-config/nextjs";
3 | import reactConfig from "@homarr/eslint-config/react";
4 |
5 | /** @type {import('typescript-eslint').Config} */
6 | export default [
7 | {
8 | ignores: [".next/**"],
9 | },
10 | ...baseConfig,
11 | ...reactConfig,
12 | ...nextjsConfig,
13 | ];
14 |
--------------------------------------------------------------------------------
/apps/nextjs/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | "postcss-preset-mantine": {},
4 | "postcss-simple-vars": {
5 | variables: {
6 | "mantine-breakpoint-xs": "36em",
7 | "mantine-breakpoint-sm": "48em",
8 | "mantine-breakpoint-md": "62em",
9 | "mantine-breakpoint-lg": "75em",
10 | "mantine-breakpoint-xl": "88em",
11 | },
12 | },
13 | },
14 | };
15 |
--------------------------------------------------------------------------------
/apps/nextjs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/apps/nextjs/public/favicon.ico
--------------------------------------------------------------------------------
/apps/nextjs/public/images/apps/truenas.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/nextjs/public/images/apps/unraid-alt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/nextjs/public/images/pwa/192.maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/apps/nextjs/public/images/pwa/192.maskable.png
--------------------------------------------------------------------------------
/apps/nextjs/public/images/pwa/512.maskable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/apps/nextjs/public/images/pwa/512.maskable.png
--------------------------------------------------------------------------------
/apps/nextjs/public/logo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/apps/nextjs/public/logo/logo.png
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/(home)/(board)/layout.tsx:
--------------------------------------------------------------------------------
1 | import definition from "../../boards/(content)/(home)/_definition";
2 |
3 | const { layout } = definition;
4 |
5 | export default layout;
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/(home)/(board)/page.tsx:
--------------------------------------------------------------------------------
1 | import definition from "../../boards/(content)/(home)/_definition";
2 |
3 | const { generateMetadataAsync: generateMetadata, page } = definition;
4 |
5 | export default page;
6 |
7 | export { generateMetadata };
8 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/(home)/not-found.tsx:
--------------------------------------------------------------------------------
1 | import HomeBoardNotFoundPage from "../boards/(content)/not-found";
2 |
3 | export default HomeBoardNotFoundPage;
4 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/_client-providers/dayjs-loader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { PropsWithChildren } from "react";
4 |
5 | import { useSuspenseDayJsLocalization } from "@homarr/translation/dayjs";
6 |
7 | export const DayJsLoader = ({ children }: PropsWithChildren) => {
8 | // Load the dayjs localization for the current locale with suspense
9 | useSuspenseDayJsLocalization();
10 |
11 | return children;
12 | };
13 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { PropsWithChildren } from "react";
4 | import { Provider } from "jotai";
5 |
6 | export const JotaiProvider = ({ children }: PropsWithChildren) => {
7 | return {children};
8 | };
9 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/(home)/_definition.ts:
--------------------------------------------------------------------------------
1 | import { api } from "@homarr/api/server";
2 |
3 | import { createBoardContentPage } from "../_creator";
4 |
5 | export default createBoardContentPage<{ locale: string }>({
6 | async getInitialBoardAsync() {
7 | return await api.board.getHomeBoard();
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/(home)/layout.tsx:
--------------------------------------------------------------------------------
1 | import definition from "./_definition";
2 |
3 | const { layout } = definition;
4 |
5 | export default layout;
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/(home)/page.tsx:
--------------------------------------------------------------------------------
1 | import definition from "./_definition";
2 |
3 | const { generateMetadataAsync: generateMetadata, page } = definition;
4 |
5 | export default page;
6 |
7 | export { generateMetadata };
8 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/[name]/(board)/_definition.tsx:
--------------------------------------------------------------------------------
1 | import { api } from "@homarr/api/server";
2 |
3 | import { createBoardContentPage } from "../../_creator";
4 |
5 | export default createBoardContentPage<{ locale: string; name: string }>({
6 | async getInitialBoardAsync({ name }) {
7 | return await api.board.getBoardByName({ name });
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/[name]/(board)/layout.tsx:
--------------------------------------------------------------------------------
1 | import definition from "./_definition";
2 |
3 | const { layout } = definition;
4 |
5 | export default layout;
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/[name]/(board)/page.tsx:
--------------------------------------------------------------------------------
1 | import definition from "./_definition";
2 |
3 | const { generateMetadataAsync: generateMetadata, page } = definition;
4 |
5 | export default page;
6 |
7 | export { generateMetadata };
8 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/[name]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { IconLayoutOff } from "@tabler/icons-react";
2 |
3 | import { getScopedI18n } from "@homarr/translation/server";
4 |
5 | import { BoardNotFound } from "~/components/board/not-found";
6 |
7 | export default async function BoardNotFoundPage() {
8 | const tNotFound = await getScopedI18n("board.error.notFound");
9 | return (
10 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/_custom-css.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRequiredBoard } from "@homarr/boards/context";
4 |
5 | export const CustomCss = () => {
6 | const board = useRequiredBoard();
7 |
8 | return ;
9 | };
10 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/(content)/_dynamic-client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import dynamic from "next/dynamic";
4 |
5 | export const DynamicClientBoard = dynamic(() => import("./_client").then((mod) => mod.ClientBoard), {
6 | ssr: false,
7 | });
8 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/[name]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { api } from "@homarr/api/server";
2 |
3 | import { BoardOtherHeaderActions } from "../_header-actions";
4 | import { createBoardLayout } from "../_layout-creator";
5 |
6 | export default createBoardLayout<{ locale: string; name: string }>({
7 | headerActions: ,
8 | async getInitialBoardAsync({ name }) {
9 | return await api.board.getBoardByName({ name });
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/[name]/settings/_shared.tsx:
--------------------------------------------------------------------------------
1 | import { clientApi } from "@homarr/api/client";
2 |
3 | import type { Board } from "../../_types";
4 |
5 | export const useSavePartialSettingsMutation = (board: Board) => {
6 | const utils = clientApi.useUtils();
7 | return clientApi.board.savePartialBoardSettings.useMutation({
8 | onSettled() {
9 | void utils.board.getBoardByName.invalidate({ name: board.name });
10 | void utils.board.getHomeBoard.invalidate();
11 | },
12 | });
13 | };
14 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/[name]/settings/danger.module.css:
--------------------------------------------------------------------------------
1 | @media (min-width: 36em) {
2 | .dangerZoneGroup {
3 | --group-wrap: nowrap !important;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { IconLayoutBoard } from "@tabler/icons-react";
4 |
5 | import { useRequiredBoard } from "@homarr/boards/context";
6 |
7 | import { HeaderButton } from "~/components/layout/header/button";
8 |
9 | export const BoardOtherHeaderActions = () => {
10 | const board = useRequiredBoard();
11 |
12 | return (
13 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/compose.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | type PropsWithChildren = Required;
4 |
5 | export const composeWrappers = (
6 | wrappers: React.FunctionComponent[],
7 | ): React.FunctionComponent => {
8 | return wrappers.reverse().reduce((Acc, Current): React.FunctionComponent => {
9 | return (props) => (
10 |
11 |
12 |
13 | );
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@mantine/core";
2 |
3 | import { InitUserForm } from "./init-user-form";
4 |
5 | export const InitUser = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Center, Loader } from "@mantine/core";
2 |
3 | export default function CommonLoading() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/[...not-found]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation";
2 |
3 | export default function NotFound() {
4 | return notFound();
5 | }
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/about/about.module.css:
--------------------------------------------------------------------------------
1 | .contributorCard {
2 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
3 | }
4 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts:
--------------------------------------------------------------------------------
1 | import { IconGrid3x3, IconKey, IconPassword, IconServer, IconUser } from "@tabler/icons-react";
2 |
3 | import type { IntegrationSecretKind } from "@homarr/definitions";
4 | import type { TablerIcon } from "@homarr/ui";
5 |
6 | export const integrationSecretIcons = {
7 | username: IconUser,
8 | apiKey: IconKey,
9 | password: IconPassword,
10 | realm: IconServer,
11 | tokenId: IconGrid3x3,
12 | } satisfies Record;
13 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/types.ts:
--------------------------------------------------------------------------------
1 | import type { RouterOutputs } from "@homarr/api";
2 |
3 | export type AnyMappedTestConnectionError = Exclude["error"];
4 | export type MappedTestConnectionCertificateError = Extract;
5 | export type MappedCertificate = MappedTestConnectionCertificateError["data"]["certificate"];
6 | export type MappedError = Exclude;
7 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Center, Stack, Text, Title } from "@mantine/core";
2 |
3 | import { getScopedI18n } from "@homarr/translation/server";
4 |
5 | export default async function NotFound() {
6 | const t = await getScopedI18n("management.notFound");
7 | return (
8 |
9 |
10 |
11 | {t("title")}
12 |
13 | {t("text")}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/tools/api/swagger-ui-overrides.css:
--------------------------------------------------------------------------------
1 | .swagger-ui .info {
2 | margin: 0 !important;
3 | margin-bottom: 20px !important;
4 | }
5 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-icon.tsx:
--------------------------------------------------------------------------------
1 | import { IconBrandGit, IconCloudShare, IconGeometry } from "@tabler/icons-react";
2 |
3 | import type { HeaderTypes } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card";
4 |
5 | interface HeaderIconProps {
6 | type: HeaderTypes;
7 | }
8 |
9 | export function HeaderIcon({ type }: HeaderIconProps) {
10 | switch (type) {
11 | case "providers":
12 | return ;
13 | case "version":
14 | return ;
15 | default:
16 | return ;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-gauge/resource-gauge.module.css:
--------------------------------------------------------------------------------
1 | .paper {
2 | position: relative;
3 | overflow: visible;
4 | padding: var(--mantine-spacing-xl);
5 | padding-top: calc(var(--mantine-spacing-xl) * 1.5 + 20px);
6 | }
7 |
8 | .icon {
9 | position: absolute;
10 | top: -20px;
11 | left: calc(50% - 30px);
12 | border: groove white;
13 | }
14 |
15 | .title {
16 | font-family:
17 | Greycliff CF,
18 | var(--mantine-font-family);
19 | line-height: 1;
20 | }
21 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-tile/resource-tile.module.css:
--------------------------------------------------------------------------------
1 | .cardContainer {
2 | transition:
3 | box-shadow 150ms ease,
4 | transform 100ms ease;
5 |
6 | @mixin hover {
7 | box-shadow: var(--mantine-shadow-md);
8 | transform: scale(1.02);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/tools/logs/client.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import dynamic from "next/dynamic";
4 |
5 | export const ClientSideTerminalComponent = dynamic(
6 | () => import("./terminal").then(({ TerminalComponent }) => TerminalComponent),
7 | {
8 | ssr: false,
9 | },
10 | );
11 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/tools/logs/terminal.module.css:
--------------------------------------------------------------------------------
1 | .outerTerminal > div {
2 | height: 100%;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/users/[userId]/access.ts:
--------------------------------------------------------------------------------
1 | import type { Session } from "@homarr/auth";
2 |
3 | export const canAccessUserEditPage = (session: Session | null, userId: string) => {
4 | if (!session) {
5 | return false;
6 | }
7 |
8 | if (session.user.id === userId) {
9 | return true;
10 | }
11 |
12 | if (session.user.permissions.includes("admin")) {
13 | return true;
14 | }
15 |
16 | return false;
17 | };
18 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/_navigation.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { ReactNode } from "react";
4 | import Link from "next/link";
5 | import { usePathname } from "next/navigation";
6 | import { NavLink } from "@mantine/core";
7 |
8 | interface NavigationLinkProps {
9 | href: string;
10 | label: string;
11 | icon: ReactNode;
12 | }
13 |
14 | export const NavigationLink = ({ href, icon, label }: NavigationLinkProps) => {
15 | const pathName = usePathname();
16 |
17 | return ;
18 | };
19 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/manage/users/groups/groups.module.css:
--------------------------------------------------------------------------------
1 | .everyoneGroup {
2 | background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
3 | }
4 |
5 | .everyoneGroup:hover {
6 | background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
7 | }
8 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/[locale]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Center } from "@mantine/core";
2 |
3 | export default function CommonNotFound() {
4 | return 404;
5 | }
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/api/about/contributors/crowdin/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | import crowdinContributors from "../../../../../../../../static-data/translators.json";
4 |
5 | export const GET = () => {
6 | return NextResponse.json(crowdinContributors);
7 | };
8 |
9 | export const dynamic = "force-static";
10 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/api/about/contributors/github/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | import githubContributors from "../../../../../../../../static-data/contributors.json";
4 |
5 | export const GET = () => {
6 | return NextResponse.json(githubContributors);
7 | };
8 |
9 | export const dynamic = "force-static";
10 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/api/health/ready/route.ts:
--------------------------------------------------------------------------------
1 | export function GET() {
2 | return new Response(undefined, {
3 | status: 200,
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/apps/nextjs/src/app/api/openapi/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | import { openApiDocument } from "@homarr/api";
4 | import { extractBaseUrlFromHeaders } from "@homarr/common";
5 |
6 | export function GET(request: Request) {
7 | return NextResponse.json(openApiDocument(extractBaseUrlFromHeaders(request.headers)));
8 | }
9 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/access/form.ts:
--------------------------------------------------------------------------------
1 | import { createFormContext } from "@homarr/form";
2 |
3 | export interface AccessFormType {
4 | items: {
5 | principalId: string;
6 | permission: TPermission;
7 | }[];
8 | }
9 |
10 | export const [FormProvider, useFormContext, useForm] = createFormContext>();
11 |
12 | export type HandleCountChange = (callback: (prev: number) => number) => void;
13 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/items/actions/remove-item.ts:
--------------------------------------------------------------------------------
1 | import type { Board } from "~/app/[locale]/boards/_types";
2 |
3 | export interface RemoveItemInput {
4 | itemId: string;
5 | }
6 |
7 | export const removeItemCallback =
8 | ({ itemId }: RemoveItemInput) =>
9 | (board: Board): Board => ({
10 | ...board,
11 | items: board.items.filter((item) => item.id !== itemId),
12 | });
13 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/items/actions/test/mocks/category-section-mock.ts:
--------------------------------------------------------------------------------
1 | import { createId } from "@homarr/db";
2 |
3 | import type { CategorySection } from "~/app/[locale]/boards/_types";
4 |
5 | export class CategorySectionMockBuilder {
6 | private readonly section: CategorySection;
7 |
8 | constructor(section?: Partial) {
9 | this.section = {
10 | id: createId(),
11 | kind: "category",
12 | xOffset: 0,
13 | yOffset: 0,
14 | name: "Category",
15 | collapsed: false,
16 | ...section,
17 | } satisfies CategorySection;
18 | }
19 |
20 | public build(): CategorySection {
21 | return this.section;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/items/actions/test/mocks/empty-section-mock.ts:
--------------------------------------------------------------------------------
1 | import { createId } from "@homarr/db";
2 |
3 | import type { EmptySection } from "~/app/[locale]/boards/_types";
4 |
5 | export class EmptySectionMockBuilder {
6 | private readonly section: EmptySection;
7 |
8 | constructor(section?: Partial) {
9 | this.section = {
10 | id: createId(),
11 | kind: "empty",
12 | xOffset: 0,
13 | yOffset: 0,
14 | ...section,
15 | } satisfies EmptySection;
16 | }
17 |
18 | public build(): EmptySection {
19 | return this.section;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/items/actions/test/mocks/layout-mock.ts:
--------------------------------------------------------------------------------
1 | import { createId } from "@homarr/db";
2 |
3 | import type { Board } from "~/app/[locale]/boards/_types";
4 |
5 | export class LayoutMockBuilder {
6 | private readonly layout: Board["layouts"][number];
7 |
8 | constructor(layout?: Partial) {
9 | this.layout = {
10 | id: createId(),
11 | name: "Base",
12 | columnCount: 12,
13 | breakpoint: 0,
14 | ...layout,
15 | } satisfies Board["layouts"][0];
16 | }
17 |
18 | public build(): Board["layouts"][0] {
19 | return this.layout;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/items/item-content.module.css:
--------------------------------------------------------------------------------
1 | .badge {
2 | @mixin dark {
3 | --background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
4 | --border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
5 | }
6 | @mixin light {
7 | --background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
8 | --border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
9 | }
10 | background-color: var(--background-color) !important;
11 | border-color: var(--border-color) !important;
12 | }
13 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/permissions/client.ts:
--------------------------------------------------------------------------------
1 | import { useSession } from "@homarr/auth/client";
2 | import type { BoardPermissionsProps } from "@homarr/auth/shared";
3 | import { constructBoardPermissions } from "@homarr/auth/shared";
4 |
5 | export const useBoardPermissions = (board: BoardPermissionsProps) => {
6 | const { data: session } = useSession();
7 | return constructBoardPermissions(board, session);
8 | };
9 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/permissions/server.ts:
--------------------------------------------------------------------------------
1 | import { auth } from "@homarr/auth/next";
2 | import type { BoardPermissionsProps } from "@homarr/auth/shared";
3 | import { constructBoardPermissions } from "@homarr/auth/shared";
4 |
5 | export const getBoardPermissionsAsync = async (board: BoardPermissionsProps) => {
6 | const session = await auth();
7 | return constructBoardPermissions(board, session);
8 | };
9 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/board/sections/item.module.css:
--------------------------------------------------------------------------------
1 | .itemCard {
2 | @mixin dark {
3 | --background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
4 | --border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
5 | }
6 | @mixin light {
7 | --background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
8 | --border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
9 | }
10 | background-color: var(--background-color) !important;
11 | border-color: var(--border-color) !important;
12 | }
13 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/language/current-language-combobox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client";
4 |
5 | import { LanguageCombobox } from "./language-combobox";
6 |
7 | interface CurrentLanguageComboboxProps {
8 | width?: string;
9 | }
10 |
11 | export const CurrentLanguageCombobox = ({ width }: CurrentLanguageComboboxProps) => {
12 | const currentLocale = useCurrentLocale();
13 | const { changeLocale, isPending } = useChangeLocale();
14 |
15 | return ;
16 | };
17 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/language/language-combobox.module.css:
--------------------------------------------------------------------------------
1 | .flagIcon {
2 | border-radius: 4px;
3 | }
4 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/layout/analytics.tsx:
--------------------------------------------------------------------------------
1 | import Script from "next/script";
2 |
3 | import { UMAMI_WEBSITE_ID } from "@homarr/analytics";
4 | import { db } from "@homarr/db";
5 | import { getServerSettingByKeyAsync } from "@homarr/db/queries";
6 |
7 | export const Analytics = async () => {
8 | // For static pages it will not find any analytics data so we do not include the script on them
9 | const analytics = await getServerSettingByKeyAsync(db, "analytics").catch(() => null);
10 |
11 | if (analytics?.enableGeneral) {
12 | return ;
13 | }
14 |
15 | return <>>;
16 | };
17 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/layout/header/burger.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback } from "react";
4 | import { Burger } from "@mantine/core";
5 | import { atom, useAtom } from "jotai";
6 |
7 | export const navigationCollapsedAtom = atom(true);
8 |
9 | export const ClientBurger = () => {
10 | const [collapsed, setCollapsed] = useAtom(navigationCollapsedAtom);
11 |
12 | const toggle = useCallback(() => setCollapsed((collapsed) => !collapsed), [setCollapsed]);
13 |
14 | return ;
15 | };
16 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/layout/header/search.module.css:
--------------------------------------------------------------------------------
1 | .desktopSearch {
2 | @mixin smaller-than $mantine-breakpoint-sm {
3 | display: none;
4 | }
5 |
6 | > div {
7 | --_input-bd-override: var(--_input-bd);
8 |
9 | button:focus-within {
10 | border-color: var(--_input-bd-override);
11 | }
12 |
13 | button {
14 | cursor: pointer;
15 | color: var(--mantine-color-placeholder);
16 | }
17 | }
18 |
19 | cursor: pointer;
20 | }
21 |
22 | .mobileSearch {
23 | @mixin larger-than $mantine-breakpoint-sm {
24 | display: none;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/manage/manage-container.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 | import type { MantineSize } from "@mantine/core";
3 | import { Container } from "@mantine/core";
4 |
5 | export const ManageContainer = ({ children, size }: PropsWithChildren<{ size?: MantineSize }>) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/manage/mobile-affix-button.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from "react";
2 | import type { ButtonProps } from "@mantine/core";
3 | import { Affix, Button, createPolymorphicComponent } from "@mantine/core";
4 |
5 | type MobileAffixButtonProps = Omit;
6 |
7 | export const MobileAffixButton = createPolymorphicComponent<"button", MobileAffixButtonProps>(
8 | forwardRef((props, ref) => (
9 | <>
10 |
11 |
12 |
13 |
14 | >
15 | )),
16 | );
17 |
--------------------------------------------------------------------------------
/apps/nextjs/src/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import type { MantineSize } from "@mantine/core";
2 |
3 | import { auth } from "@homarr/auth/next";
4 | import { UserAvatar } from "@homarr/ui";
5 |
6 | interface UserAvatarProps {
7 | size: MantineSize;
8 | }
9 |
10 | export const CurrentUserAvatar = async ({ size }: UserAvatarProps) => {
11 | const currentSession = await auth();
12 |
13 | const user = {
14 | name: currentSession?.user.name ?? null,
15 | image: currentSession?.user.image ?? null,
16 | };
17 |
18 | return ;
19 | };
20 |
--------------------------------------------------------------------------------
/apps/nextjs/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const fullHeightWithoutHeaderAndFooter =
2 | "calc(100dvh - var(--app-shell-header-offset, 0px) - var(--app-shell-padding) - var(--app-shell-footer-offset, 0px) - var(--app-shell-padding))";
3 |
--------------------------------------------------------------------------------
/apps/nextjs/src/metadata.ts:
--------------------------------------------------------------------------------
1 | export const createMetaTitle = (name: string) => `${name} • Homarr`;
2 |
--------------------------------------------------------------------------------
/apps/nextjs/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png";
2 | declare module "*.svg";
3 | declare module "*.jpeg";
4 | declare module "*.jpg";
5 |
--------------------------------------------------------------------------------
/apps/nextjs/src/styles/scroll-area.scss:
--------------------------------------------------------------------------------
1 | .scroll-area-w100 .mantine-ScrollArea-viewport > div:nth-of-type(1) {
2 | width: 100%;
3 | display: inherit !important;
4 | }
5 |
--------------------------------------------------------------------------------
/apps/nextjs/src/theme/color-scheme.ts:
--------------------------------------------------------------------------------
1 | import { cache } from "react";
2 | import { cookies } from "next/headers";
3 |
4 | import { db } from "@homarr/db";
5 | import { getServerSettingByKeyAsync } from "@homarr/db/queries";
6 | import type { ColorScheme } from "@homarr/definitions";
7 | import { colorSchemeCookieKey } from "@homarr/definitions";
8 |
9 | export const getCurrentColorSchemeAsync = cache(async () => {
10 | const cookieValue = (await cookies()).get(colorSchemeCookieKey)?.value;
11 |
12 | if (cookieValue) {
13 | return cookieValue as ColorScheme;
14 | }
15 |
16 | const appearanceSettings = await getServerSettingByKeyAsync(db, "appearance");
17 | return appearanceSettings.defaultColorScheme;
18 | });
19 |
--------------------------------------------------------------------------------
/apps/nextjs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["./src/*"],
7 | "@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
8 | },
9 | "plugins": [
10 | {
11 | "name": "next"
12 | }
13 | ],
14 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
15 | },
16 | "include": [".", ".next/types/**/*.ts"],
17 | "exclude": ["node_modules", ".next"]
18 | }
19 |
--------------------------------------------------------------------------------
/apps/tasks/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: ["tasks.cjs"],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/apps/tasks/src/main.ts:
--------------------------------------------------------------------------------
1 | // This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
2 | import "./undici-log-agent-override";
3 |
4 | import { registerCronJobRunner } from "@homarr/cron-job-runner/register";
5 | import { jobGroup } from "@homarr/cron-jobs";
6 |
7 | void (async () => {
8 | registerCronJobRunner();
9 | await jobGroup.startAllAsync();
10 | })();
11 |
--------------------------------------------------------------------------------
/apps/tasks/src/undici-log-agent-override.ts:
--------------------------------------------------------------------------------
1 | import { setGlobalDispatcher } from "undici";
2 |
3 | import { LoggingAgent } from "@homarr/common/server";
4 |
5 | const agent = new LoggingAgent();
6 | setGlobalDispatcher(agent);
7 |
--------------------------------------------------------------------------------
/apps/tasks/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["src/*"],
7 | "@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
8 | },
9 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
10 | },
11 | "include": ["."],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/apps/websocket/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: ["wssServer.cjs"],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/apps/websocket/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "baseUrl": ".",
5 | "paths": {
6 | "~/*": ["src/*"],
7 | "@homarr/node-unifi": ["../../node_modules/@types/node-unifi"]
8 | },
9 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
10 | },
11 | "include": ["."],
12 | "exclude": ["node_modules"]
13 | }
14 |
--------------------------------------------------------------------------------
/crowdin.yml:
--------------------------------------------------------------------------------
1 | files:
2 | - source: /packages/translation/src/lang/en.json
3 | translation: /packages/translation/src/lang/%two_letters_code%.json
4 |
--------------------------------------------------------------------------------
/development/build.cmd:
--------------------------------------------------------------------------------
1 | docker build -t homarr .
--------------------------------------------------------------------------------
/development/docker-run.cmd:
--------------------------------------------------------------------------------
1 | :: Please do not run this command in production. It is only for local testing.
2 | docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000 homarr:latest
--------------------------------------------------------------------------------
/docs/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/docs/banner.png
--------------------------------------------------------------------------------
/docs/installation-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/docs/installation-button.png
--------------------------------------------------------------------------------
/docs/section-contribute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/docs/section-contribute.png
--------------------------------------------------------------------------------
/docs/section-features.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/docs/section-features.png
--------------------------------------------------------------------------------
/docs/section-installation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/docs/section-installation.png
--------------------------------------------------------------------------------
/docs/section-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/docs/section-preview.png
--------------------------------------------------------------------------------
/docs/section-widgets-and-integrations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/docs/section-widgets-and-integrations.png
--------------------------------------------------------------------------------
/e2e/home.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test } from "vitest";
2 |
3 | import { createHomarrContainer } from "./shared/create-homarr-container";
4 |
5 | describe("Home", () => {
6 | test("should open with status code 200", async () => {
7 | // Arrange
8 | const homarrContainer = await createHomarrContainer().start();
9 |
10 | // Act
11 | const homeResponse = await fetch(`http://localhost:${homarrContainer.getMappedPort(7575)}/`);
12 |
13 | // Assert
14 | expect(homeResponse.status).toBe(200);
15 | }, 20_000);
16 | });
17 |
--------------------------------------------------------------------------------
/packages/analytics/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/analytics/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/analytics/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const UMAMI_HOST_URL = "https://umami.homarr.dev";
2 | export const UMAMI_WEBSITE_ID = "ff7dc470-a84f-4779-b1ab-66a5bb16a94b";
3 |
--------------------------------------------------------------------------------
/packages/analytics/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./constants";
2 | export * from "./send-server-analytics";
3 |
--------------------------------------------------------------------------------
/packages/analytics/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/api/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/api/src/client.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createTRPCClient, httpLink } from "@trpc/client";
4 | import { createTRPCReact } from "@trpc/react-query";
5 | import SuperJSON from "superjson";
6 |
7 | import type { AppRouter } from ".";
8 | import { createHeadersCallbackForSource, getTrpcUrl } from "./shared";
9 |
10 | export const clientApi = createTRPCReact();
11 | export const fetchApi = createTRPCClient({
12 | links: [
13 | httpLink({
14 | url: getTrpcUrl(),
15 | transformer: SuperJSON,
16 | headers: createHeadersCallbackForSource("fetch"),
17 | }),
18 | ],
19 | });
20 |
--------------------------------------------------------------------------------
/packages/api/src/env.ts:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | import { shouldSkipEnvValidation } from "@homarr/common/env-validation";
5 |
6 | export const env = createEnv({
7 | server: {
8 | KUBERNETES_SERVICE_ACCOUNT_NAME: z.string().optional(),
9 | },
10 | runtimeEnv: {
11 | KUBERNETES_SERVICE_ACCOUNT_NAME: process.env.KUBERNETES_SERVICE_ACCOUNT_NAME,
12 | },
13 | skipValidation: shouldSkipEnvValidation(),
14 | emptyStringAsUndefined: true,
15 | });
16 |
--------------------------------------------------------------------------------
/packages/api/src/middlewares/docker.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from "@trpc/server";
2 |
3 | import { env } from "@homarr/docker/env";
4 |
5 | import { publicProcedure } from "../trpc";
6 |
7 | export const dockerMiddleware = () => {
8 | return publicProcedure.use(async ({ next }) => {
9 | if (env.ENABLE_DOCKER) {
10 | return await next();
11 | }
12 | throw new TRPCError({
13 | code: "NOT_FOUND",
14 | message: "Docker route is not available",
15 | });
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/packages/api/src/middlewares/kubernetes.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from "@trpc/server";
2 |
3 | import { env } from "@homarr/docker/env";
4 |
5 | import { publicProcedure } from "../trpc";
6 |
7 | export const kubernetesMiddleware = () => {
8 | return publicProcedure.use(async ({ next }) => {
9 | if (env.ENABLE_KUBERNETES) {
10 | return await next();
11 | }
12 | throw new TRPCError({
13 | code: "NOT_FOUND",
14 | message: "Kubernetes route is not available",
15 | });
16 | });
17 | };
18 |
--------------------------------------------------------------------------------
/packages/api/src/open-api.ts:
--------------------------------------------------------------------------------
1 | import { generateOpenApiDocument } from "trpc-to-openapi";
2 |
3 | import { appRouter } from "./root";
4 |
5 | export const openApiDocument = (base: string) =>
6 | generateOpenApiDocument(appRouter, {
7 | title: "Homarr API documentation",
8 | version: "1.0.0",
9 | baseUrl: base,
10 | docsUrl: "https://homarr.dev",
11 | securitySchemes: {
12 | apikey: {
13 | type: "apiKey",
14 | name: "ApiKey",
15 | description: "API key which can be obtained in the Homarr administration dashboard",
16 | in: "header",
17 | },
18 | },
19 | });
20 |
--------------------------------------------------------------------------------
/packages/api/src/router/invite/checks.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from "@trpc/server";
2 |
3 | import { env } from "@homarr/auth/env";
4 |
5 | export const throwIfCredentialsDisabled = () => {
6 | if (!env.AUTH_PROVIDERS.includes("credentials")) {
7 | throw new TRPCError({
8 | code: "FORBIDDEN",
9 | message: "Credentials provider is disabled",
10 | });
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/packages/api/src/router/kubernetes/resource-parser/resource-parser.ts:
--------------------------------------------------------------------------------
1 | export interface ResourceParser {
2 | parse(value: string): number;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/api/src/router/test/helper.ts:
--------------------------------------------------------------------------------
1 | import { expect } from "vitest";
2 |
3 | export const expectToBeDefined = (value: T) => {
4 | if (value === undefined) {
5 | expect(value).toBeDefined();
6 | }
7 | if (value === null) {
8 | expect(value).not.toBeNull();
9 | }
10 | return value as Exclude;
11 | };
12 |
--------------------------------------------------------------------------------
/packages/api/src/test/open-api.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test, vi } from "vitest";
2 |
3 | import { openApiDocument } from "../open-api";
4 |
5 | vi.mock("@homarr/auth", () => ({}));
6 |
7 | test("OpenAPI documentation should be generated", () => {
8 | // Arrange
9 | const base = "https://homarr.dev";
10 |
11 | // Act
12 | const act = () => openApiDocument(base);
13 |
14 | // Assert
15 | expect(act).not.toThrow();
16 | });
17 |
--------------------------------------------------------------------------------
/packages/api/src/websocket.ts:
--------------------------------------------------------------------------------
1 | export { appRouter } from "./root";
2 | export { createTRPCContext } from "./trpc";
3 |
--------------------------------------------------------------------------------
/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/auth/client.ts:
--------------------------------------------------------------------------------
1 | export { signIn, signOut, useSession, SessionProvider } from "next-auth/react";
2 | export * from "./permissions/integration-provider";
3 |
--------------------------------------------------------------------------------
/packages/auth/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/auth/next.ts:
--------------------------------------------------------------------------------
1 | import { cache } from "react";
2 |
3 | import { createConfiguration } from "./configuration";
4 |
5 | const { auth: defaultAuth } = createConfiguration("unknown", null, false);
6 |
7 | /**
8 | * This is the main way to get session data for your RSCs.
9 | * This will de-duplicate all calls to next-auth's default `auth()` function and only call it once per request
10 | */
11 | export const auth = cache(defaultAuth);
12 |
--------------------------------------------------------------------------------
/packages/auth/permissions/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./board-permissions";
2 | export * from "./integration-permissions";
3 |
--------------------------------------------------------------------------------
/packages/auth/providers/check-provider.ts:
--------------------------------------------------------------------------------
1 | import type { SupportedAuthProvider } from "@homarr/definitions";
2 |
3 | import { env } from "../env";
4 |
5 | export const isProviderEnabled = (provider: SupportedAuthProvider) => {
6 | // The question mark is placed there because isProviderEnabled is called during static build of about page
7 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
8 | return env.AUTH_PROVIDERS?.includes(provider);
9 | };
10 |
--------------------------------------------------------------------------------
/packages/auth/providers/empty/empty-provider.ts:
--------------------------------------------------------------------------------
1 | import type { OAuthConfig } from "next-auth/providers";
2 |
3 | export function EmptyNextAuthProvider(): OAuthConfig {
4 | return {
5 | id: "empty",
6 | name: "Empty",
7 | type: "oauth",
8 | profile: () => {
9 | throw new Error(
10 | "EmptyNextAuthProvider can not be used and is only a placeholder because credentials authentication can not be used as session authentication without additional providers.",
11 | );
12 | },
13 | issuer: "empty",
14 | authorization: new URL("https://example.empty"),
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/packages/auth/security.ts:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcrypt";
2 |
3 | export const createSaltAsync = async () => {
4 | return bcrypt.genSalt(10);
5 | };
6 |
7 | export const hashPasswordAsync = async (password: string, salt: string) => {
8 | return bcrypt.hash(password, salt);
9 | };
10 |
--------------------------------------------------------------------------------
/packages/auth/server.ts:
--------------------------------------------------------------------------------
1 | export { hasQueryAccessToIntegrationsAsync } from "./permissions/integration-query-permissions";
2 | export { getIntegrationsWithPermissionsAsync } from "./permissions/integrations-with-permissions";
3 | export { isProviderEnabled } from "./providers/check-provider";
4 | export { createSessionCallback, createSessionAsync } from "./callbacks";
5 |
--------------------------------------------------------------------------------
/packages/auth/shared.ts:
--------------------------------------------------------------------------------
1 | export * from "./permissions";
2 |
--------------------------------------------------------------------------------
/packages/auth/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/boards/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/boards/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/certificates/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/certificates/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cli/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/cli/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/cli/src/index.ts:
--------------------------------------------------------------------------------
1 | import { run } from "@drizzle-team/brocli";
2 |
3 | import { fixUsernames } from "./commands/fix-usernames";
4 | import { recreateAdmin } from "./commands/recreate-admin";
5 | import { resetPassword } from "./commands/reset-password";
6 |
7 | const commands = [resetPassword, fixUsernames, recreateAdmin];
8 |
9 | void run(commands, {
10 | name: "homarr-cli",
11 | version: "1.0.0",
12 | });
13 |
--------------------------------------------------------------------------------
/packages/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/common/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/common/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/common/src/array.ts:
--------------------------------------------------------------------------------
1 | export const splitToNChunks = (array: T[], chunks: number): T[][] => {
2 | const result: T[][] = [];
3 | for (let i = chunks; i > 0; i--) {
4 | result.push(array.splice(0, Math.ceil(array.length / i)));
5 | }
6 | return result;
7 | };
8 |
9 | export const splitToChunksWithNItems = (array: T[], itemCount: number): T[][] => {
10 | const result: T[][] = [];
11 | for (let i = 0; i < array.length; i += itemCount) {
12 | result.push(array.slice(i, i + itemCount));
13 | }
14 | return result;
15 | };
16 |
--------------------------------------------------------------------------------
/packages/common/src/client.ts:
--------------------------------------------------------------------------------
1 | export * from "./revalidate-path-action";
2 |
--------------------------------------------------------------------------------
/packages/common/src/cookie.ts:
--------------------------------------------------------------------------------
1 | import type { CookieSerializeOptions } from "cookie";
2 | import { parse, serialize } from "cookie";
3 |
4 | export function parseCookies(cookieString: string) {
5 | return parse(cookieString);
6 | }
7 |
8 | export function setClientCookie(name: string, value: string, options: CookieSerializeOptions = {}) {
9 | document.cookie = serialize(name, value, options);
10 | }
11 |
--------------------------------------------------------------------------------
/packages/common/src/error.ts:
--------------------------------------------------------------------------------
1 | export const extractErrorMessage = (error: unknown) => {
2 | if (error instanceof Error) {
3 | return error.message;
4 | }
5 |
6 | if (typeof error === "string") {
7 | return error;
8 | }
9 |
10 | return "Unknown error";
11 | };
12 |
13 | export abstract class FlattenError extends Error {
14 | constructor(
15 | message: string,
16 | private flattenResult: Record,
17 | ) {
18 | super(message);
19 | }
20 |
21 | public flatten(): Record {
22 | return this.flattenResult;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/common/src/errors/http/handlers/http-error-handler.ts:
--------------------------------------------------------------------------------
1 | import type { AnyRequestError } from "../request-error";
2 | import type { ResponseError } from "../response-error";
3 |
4 | export abstract class HttpErrorHandler {
5 | abstract handleRequestError(error: unknown): AnyRequestError | undefined;
6 | abstract handleResponseError(error: unknown): ResponseError | undefined;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/common/src/errors/http/handlers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./http-error-handler";
2 | export * from "./fetch-http-error-handler";
3 | export * from "./ofetch-http-error-handler";
4 | export * from "./axios-http-error-handler";
5 | export * from "./tsdav-http-error-handler";
6 |
--------------------------------------------------------------------------------
/packages/common/src/errors/http/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./handlers";
2 | export * from "./request-error";
3 | export * from "./response-error";
4 |
--------------------------------------------------------------------------------
/packages/common/src/errors/http/response-error.ts:
--------------------------------------------------------------------------------
1 | export class ResponseError extends Error {
2 | public readonly statusCode: number;
3 | public readonly url?: string;
4 |
5 | constructor(response: { status: number; url?: string }, options?: ErrorOptions) {
6 | super("Response did not indicate success", options);
7 | this.name = ResponseError.name;
8 |
9 | this.statusCode = response.status;
10 | this.url = response.url;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/common/src/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./parse";
2 | export * from "./http";
3 |
--------------------------------------------------------------------------------
/packages/common/src/errors/parse/handlers/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./parse-error-handler";
2 | export * from "./zod-parse-error-handler";
3 | export * from "./json-parse-error-handler";
4 |
--------------------------------------------------------------------------------
/packages/common/src/errors/parse/handlers/json-parse-error-handler.ts:
--------------------------------------------------------------------------------
1 | import { logger } from "@homarr/log";
2 |
3 | import { ParseError } from "../parse-error";
4 | import { ParseErrorHandler } from "./parse-error-handler";
5 |
6 | export class JsonParseErrorHandler extends ParseErrorHandler {
7 | handleParseError(error: unknown): ParseError | undefined {
8 | if (!(error instanceof SyntaxError)) return undefined;
9 |
10 | logger.debug("Received JSON parse error", {
11 | message: error.message,
12 | });
13 |
14 | return new ParseError("Failed to parse json", { cause: error });
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/common/src/errors/parse/handlers/parse-error-handler.ts:
--------------------------------------------------------------------------------
1 | import type { ParseError } from "../parse-error";
2 |
3 | export abstract class ParseErrorHandler {
4 | abstract handleParseError(error: unknown): ParseError | undefined;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/common/src/errors/parse/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./handlers";
2 | export * from "./parse-error";
3 |
--------------------------------------------------------------------------------
/packages/common/src/errors/parse/parse-error.ts:
--------------------------------------------------------------------------------
1 | export class ParseError extends Error {
2 | constructor(message: string, options?: { cause: Error }) {
3 | super(`Failed to parse data:\n${message}`, options);
4 | this.name = ParseError.name;
5 | }
6 |
7 | get cause(): Error {
8 | return super.cause as Error;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/common/src/fetch-with-timeout.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Same as fetch, but with a timeout of 10 seconds.
3 | * https://stackoverflow.com/questions/46946380/fetch-api-request-timeout
4 | * @param param0 fetch arguments
5 | * @returns fetch response
6 | */
7 | export const fetchWithTimeout = (...[url, requestInit]: Parameters) => {
8 | const controller = new AbortController();
9 |
10 | // 10 seconds timeout:
11 | const timeoutId = setTimeout(() => controller.abort(), 10000);
12 |
13 | return fetch(url, { signal: controller.signal, ...requestInit }).finally(() => {
14 | clearTimeout(timeoutId);
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/packages/common/src/function.ts:
--------------------------------------------------------------------------------
1 | export const isFunction = (value: unknown): value is (...args: unknown[]) => unknown => {
2 | return typeof value === "function";
3 | };
4 |
--------------------------------------------------------------------------------
/packages/common/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./object";
2 | export * from "./string";
3 | export * from "./cookie";
4 | export * from "./array";
5 | export * from "./date";
6 | export * from "./stopwatch";
7 | export * from "./hooks";
8 | export * from "./url";
9 | export * from "./number";
10 | export * from "./error";
11 | export * from "./fetch-with-timeout";
12 | export * from "./theme";
13 | export * from "./function";
14 |
--------------------------------------------------------------------------------
/packages/common/src/object.ts:
--------------------------------------------------------------------------------
1 | import { hashKey } from "@tanstack/query-core";
2 |
3 | export function objectKeys(obj: O): (keyof O)[] {
4 | return Object.keys(obj) as (keyof O)[];
5 | }
6 |
7 | type Entries = {
8 | [K in keyof T]: [K, T[K]];
9 | }[keyof T][];
10 |
11 | export const objectEntries = (obj: T) => Object.entries(obj) as Entries;
12 |
13 | export const hashObjectBase64 = (obj: object) => {
14 | return Buffer.from(hashKey([obj])).toString("base64");
15 | };
16 |
--------------------------------------------------------------------------------
/packages/common/src/revalidate-path-action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { revalidatePath } from "next/cache";
4 |
5 | export async function revalidatePathActionAsync(path: string) {
6 | return new Promise((resolve) => resolve(revalidatePath(path, "page")));
7 | }
8 |
--------------------------------------------------------------------------------
/packages/common/src/security.ts:
--------------------------------------------------------------------------------
1 | import { randomBytes } from "crypto";
2 |
3 | /**
4 | * Generates a random hex token twice the size of the given size
5 | * @param size amount of bytes to generate
6 | * @returns a random hex token twice the length of the given size
7 | */
8 | export const generateSecureRandomToken = (size: number) => {
9 | return randomBytes(size).toString("hex");
10 | };
11 |
--------------------------------------------------------------------------------
/packages/common/src/server.ts:
--------------------------------------------------------------------------------
1 | export * from "./security";
2 | export * from "./encryption";
3 | export * from "./user-agent";
4 | export * from "./fetch-agent";
5 | export * from "./errors";
6 |
--------------------------------------------------------------------------------
/packages/common/src/string.ts:
--------------------------------------------------------------------------------
1 | export const capitalize = (str: T) => {
2 | return (str.charAt(0).toUpperCase() + str.slice(1)) as Capitalize;
3 | };
4 |
5 | export const isNullOrWhitespace = (value: string | null): value is null => {
6 | return value == null || value.trim() === "";
7 | };
8 |
--------------------------------------------------------------------------------
/packages/common/src/test/string.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 |
3 | import { capitalize } from "../string";
4 |
5 | const capitalizeTestCases = [
6 | ["hello", "Hello"],
7 | ["World", "World"],
8 | ["123", "123"],
9 | ["a", "A"],
10 | ["two words", "Two words"],
11 | ] as const;
12 |
13 | describe("capitalize should capitalize the first letter of a string", () => {
14 | capitalizeTestCases.forEach(([input, expected]) => {
15 | it(`should capitalize ${input} to ${expected}`, () => {
16 | expect(capitalize(input)).toEqual(expected);
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/packages/common/src/theme.ts:
--------------------------------------------------------------------------------
1 | import type { DefaultMantineColor, MantineColorShade } from "@mantine/core";
2 | import { DEFAULT_THEME } from "@mantine/core";
3 |
4 | export const getMantineColor = (color: DefaultMantineColor, shade: MantineColorShade) =>
5 | DEFAULT_THEME.colors[color]?.[shade] ?? "#fff";
6 |
--------------------------------------------------------------------------------
/packages/common/src/user-agent.ts:
--------------------------------------------------------------------------------
1 | import { userAgent as userAgentNextServer } from "next/server";
2 |
3 | import type { Modify } from "./types";
4 |
5 | export const userAgent = (headers: Headers) => {
6 | return userAgentNextServer({ headers }) as Omit, "device"> & {
7 | device: Modify["device"], { type: DeviceType }>;
8 | };
9 | };
10 |
11 | export type DeviceType = "console" | "mobile" | "tablet" | "smarttv" | "wearable" | "embedded" | undefined;
12 |
--------------------------------------------------------------------------------
/packages/common/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cron-job-runner/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/cron-job-runner/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/cron-job-runner/src/register.ts:
--------------------------------------------------------------------------------
1 | import { jobGroup } from "@homarr/cron-jobs";
2 |
3 | import { cronJobRunnerChannel } from ".";
4 |
5 | /**
6 | * Registers the cron job runner to listen to the Redis PubSub channel.
7 | */
8 | export const registerCronJobRunner = () => {
9 | cronJobRunnerChannel.subscribe((jobName) => {
10 | void jobGroup.runManuallyAsync(jobName);
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/packages/cron-job-runner/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cron-job-status/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/cron-job-status/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/cron-job-status/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createSubPubChannel } from "../../redis/src/lib/channel";
2 |
3 | export interface TaskStatus {
4 | name: string;
5 | status: "running" | "idle";
6 | lastExecutionTimestamp: string;
7 | lastExecutionStatus: "success" | "error" | null;
8 | }
9 |
10 | export const createCronJobStatusChannel = (name: string) => createSubPubChannel(`cron-job-status:${name}`);
11 |
--------------------------------------------------------------------------------
/packages/cron-job-status/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cron-jobs-core/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/cron-jobs-core/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/cron-jobs-core/src/expressions.ts:
--------------------------------------------------------------------------------
1 | import { checkCron } from "./validation";
2 |
3 | export const EVERY_5_SECONDS = checkCron("*/5 * * * * *") satisfies string;
4 | export const EVERY_MINUTE = checkCron("* * * * *") satisfies string;
5 | export const EVERY_5_MINUTES = checkCron("*/5 * * * *") satisfies string;
6 | export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string;
7 | export const EVERY_HOUR = checkCron("0 * * * *") satisfies string;
8 | export const EVERY_DAY = checkCron("0 0 * * */1") satisfies string;
9 | export const EVERY_WEEK = checkCron("0 0 * * 1") satisfies string;
10 | export const NEVER = "never";
11 |
--------------------------------------------------------------------------------
/packages/cron-jobs-core/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { CreateCronJobCreatorOptions } from "./creator";
2 | import { createCronJobCreator } from "./creator";
3 | import { createJobGroupCreator } from "./group";
4 | import { ConsoleLogger } from "./logger";
5 |
6 | export const createCronJobFunctions = (
7 | options: CreateCronJobCreatorOptions = { logger: new ConsoleLogger() },
8 | ) => {
9 | return {
10 | createCronJob: createCronJobCreator(options),
11 | createCronJobGroup: createJobGroupCreator({
12 | logger: options.logger,
13 | }),
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/packages/cron-jobs-core/src/logger.ts:
--------------------------------------------------------------------------------
1 | export interface Logger {
2 | logDebug(message: string): void;
3 | logInfo(message: string): void;
4 | logError(error: unknown): void;
5 | logWarning(message: string): void;
6 | }
7 |
8 | export class ConsoleLogger implements Logger {
9 | public logDebug(message: string) {
10 | console.log(message);
11 | }
12 |
13 | public logInfo(message: string) {
14 | console.log(message);
15 | }
16 |
17 | public logError(error: unknown) {
18 | console.error(error);
19 | }
20 |
21 | public logWarning(message: string) {
22 | console.warn(message);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/cron-jobs-core/src/registry.ts:
--------------------------------------------------------------------------------
1 | import type { JobCallback } from "./creator";
2 |
3 | export const jobRegistry = new Map>>();
4 |
--------------------------------------------------------------------------------
/packages/cron-jobs-core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/cron-jobs/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/cron-jobs/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/cron-jobs/src/jobs/analytics.ts:
--------------------------------------------------------------------------------
1 | import { sendServerAnalyticsAsync } from "@homarr/analytics";
2 | import { EVERY_WEEK } from "@homarr/cron-jobs-core/expressions";
3 | import { db } from "@homarr/db";
4 | import { getServerSettingByKeyAsync } from "@homarr/db/queries";
5 |
6 | import { createCronJob } from "../lib";
7 |
8 | export const analyticsJob = createCronJob("analytics", EVERY_WEEK, {
9 | runOnStart: true,
10 | }).withCallback(async () => {
11 | const analyticSetting = await getServerSettingByKeyAsync(db, "analytics");
12 |
13 | if (!analyticSetting.enableGeneral) {
14 | return;
15 | }
16 |
17 | await sendServerAnalyticsAsync();
18 | });
19 |
--------------------------------------------------------------------------------
/packages/cron-jobs/src/jobs/integrations/dns-hole.ts:
--------------------------------------------------------------------------------
1 | import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
2 | import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
3 | import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
4 |
5 | import { createCronJob } from "../../lib";
6 |
7 | export const dnsHoleJob = createCronJob("dnsHole", EVERY_5_SECONDS).withCallback(
8 | createRequestIntegrationJobHandler(dnsHoleRequestHandler.handler, {
9 | widgetKinds: ["dnsHoleSummary", "dnsHoleControls"],
10 | getInput: {
11 | dnsHoleSummary: () => ({}),
12 | dnsHoleControls: () => ({}),
13 | },
14 | }),
15 | );
16 |
--------------------------------------------------------------------------------
/packages/cron-jobs/src/jobs/integrations/indexer-manager.ts:
--------------------------------------------------------------------------------
1 | import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
2 | import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager";
3 | import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
4 |
5 | import { createCronJob } from "../../lib";
6 |
7 | export const indexerManagerJob = createCronJob("indexerManager", EVERY_5_MINUTES).withCallback(
8 | createRequestIntegrationJobHandler(indexerManagerRequestHandler.handler, {
9 | widgetKinds: ["indexerManager"],
10 | getInput: {
11 | indexerManager: () => ({}),
12 | },
13 | }),
14 | );
15 |
--------------------------------------------------------------------------------
/packages/cron-jobs/src/jobs/integrations/media-server.ts:
--------------------------------------------------------------------------------
1 | import { EVERY_5_SECONDS } from "@homarr/cron-jobs-core/expressions";
2 | import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
3 | import { mediaServerRequestHandler } from "@homarr/request-handler/media-server";
4 |
5 | import { createCronJob } from "../../lib";
6 |
7 | export const mediaServerJob = createCronJob("mediaServer", EVERY_5_SECONDS).withCallback(
8 | createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, {
9 | widgetKinds: ["mediaServer"],
10 | getInput: {
11 | mediaServer: ({ showOnlyPlaying }) => ({ showOnlyPlaying }),
12 | },
13 | }),
14 | );
15 |
--------------------------------------------------------------------------------
/packages/cron-jobs/src/jobs/update-checker.ts:
--------------------------------------------------------------------------------
1 | import { EVERY_HOUR } from "@homarr/cron-jobs-core/expressions";
2 | import { updateCheckerRequestHandler } from "@homarr/request-handler/update-checker";
3 |
4 | import { createCronJob } from "../lib";
5 |
6 | export const updateCheckerJob = createCronJob("updateChecker", EVERY_HOUR, {
7 | runOnStart: true,
8 | }).withCallback(async () => {
9 | const handler = updateCheckerRequestHandler.handler({});
10 | await handler.getCachedOrUpdatedDataAsync({
11 | forceUpdate: true,
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/packages/cron-jobs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/db/client.ts:
--------------------------------------------------------------------------------
1 | export { createId } from "@paralleldrive/cuid2";
2 |
--------------------------------------------------------------------------------
/packages/db/configs/mysql.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "drizzle-kit";
2 |
3 | import { env } from "../env";
4 |
5 | export default {
6 | dialect: "mysql",
7 | schema: "./schema",
8 | casing: "snake_case",
9 | dbCredentials: env.DB_URL
10 | ? { url: env.DB_URL }
11 | : {
12 | host: env.DB_HOST,
13 | user: env.DB_USER,
14 | password: env.DB_PASSWORD,
15 | database: env.DB_NAME,
16 | port: env.DB_PORT,
17 | },
18 | out: "./migrations/mysql",
19 | } satisfies Config;
20 |
--------------------------------------------------------------------------------
/packages/db/configs/sqlite.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "drizzle-kit";
2 |
3 | import { env } from "../env";
4 |
5 | export default {
6 | dialect: "sqlite",
7 | schema: "./schema",
8 | casing: "snake_case",
9 | dbCredentials: { url: env.DB_URL },
10 | out: "./migrations/sqlite",
11 | } satisfies Config;
12 |
--------------------------------------------------------------------------------
/packages/db/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/db/index.ts:
--------------------------------------------------------------------------------
1 | import Database from "better-sqlite3";
2 |
3 | import { database } from "./driver";
4 |
5 | export * from "drizzle-orm";
6 |
7 | export const db = database;
8 |
9 | export type Database = typeof db;
10 | export type { HomarrDatabaseMysql } from "./driver";
11 |
12 | export { createId } from "@paralleldrive/cuid2";
13 | export { handleDiffrentDbDriverOperationsAsync as handleTransactionsAsync } from "./transactions";
14 |
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0001_wild_alex_wilder.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `homeBoardId` varchar(64);--> statement-breakpoint
2 | ALTER TABLE `user` ADD CONSTRAINT `user_homeBoardId_board_id_fk` FOREIGN KEY (`homeBoardId`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0002_flimsy_deathbird.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `item` ADD `advanced_options` text DEFAULT ('{"json": {}}') NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0003_freezing_black_panther.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `serverSetting` (
2 | `key` varchar(64) NOT NULL,
3 | `value` text NOT NULL DEFAULT ('{"json": {}}'),
4 | CONSTRAINT `serverSetting_key` PRIMARY KEY(`key`),
5 | CONSTRAINT `serverSetting_key_unique` UNIQUE(`key`)
6 | );
7 |
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0005_soft_microbe.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `provider` varchar(64) DEFAULT 'credentials' NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0006_young_micromax.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `section` RENAME COLUMN `position` TO `y_offset`;--> statement-breakpoint
2 | ALTER TABLE `section` ADD `x_offset` int NOT NULL;--> statement-breakpoint
3 | ALTER TABLE `section` ADD `width` int;--> statement-breakpoint
4 | ALTER TABLE `section` ADD `height` int;--> statement-breakpoint
5 | ALTER TABLE `section` ADD `parent_section_id` varchar(64);--> statement-breakpoint
6 | ALTER TABLE `section` ADD CONSTRAINT `section_parent_section_id_section_id_fk` FOREIGN KEY (`parent_section_id`) REFERENCES `section`(`id`) ON DELETE cascade ON UPDATE no action;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0007_boring_nocturne.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `colorScheme` varchar(5) DEFAULT 'auto' NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0008_far_lifeguard.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `search_engine` (
2 | `id` varchar(64) NOT NULL,
3 | `icon_url` text NOT NULL,
4 | `name` varchar(64) NOT NULL,
5 | `short` varchar(8) NOT NULL,
6 | `description` text,
7 | `url_template` text NOT NULL,
8 | CONSTRAINT `search_engine_id` PRIMARY KEY(`id`)
9 | );
10 |
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0009_wakeful_tenebrous.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `apiKey` (
2 | `id` varchar(64) NOT NULL,
3 | `apiKey` text NOT NULL,
4 | `salt` text NOT NULL,
5 | `userId` varchar(64) NOT NULL,
6 | CONSTRAINT `apiKey_id` PRIMARY KEY(`id`)
7 | );
8 | --> statement-breakpoint
9 | ALTER TABLE `apiKey` ADD CONSTRAINT `apiKey_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0010_melted_pestilence.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `firstDayOfWeek` tinyint DEFAULT 1 NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0011_freezing_banshee.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `pingIconsEnabled` boolean DEFAULT false NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0012_abnormal_wendell_vaughn.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` MODIFY COLUMN `colorScheme` varchar(5) NOT NULL DEFAULT 'dark';
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0013_youthful_vulture.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `group` ADD CONSTRAINT `group_name_unique` UNIQUE(`name`);
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0014_bizarre_red_shift.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `media` (
2 | `id` varchar(64) NOT NULL,
3 | `name` varchar(512) NOT NULL,
4 | `content` BLOB NOT NULL,
5 | `content_type` text NOT NULL,
6 | `size` int NOT NULL,
7 | `created_at` timestamp NOT NULL DEFAULT (now()),
8 | `creator_id` varchar(64),
9 | CONSTRAINT `media_id` PRIMARY KEY(`id`)
10 | );
11 | --> statement-breakpoint
12 | ALTER TABLE `media` ADD CONSTRAINT `media_creator_id_user_id_fk` FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0015_unknown_firedrake.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `search_engine` MODIFY COLUMN `url_template` text;--> statement-breakpoint
2 | ALTER TABLE `search_engine` ADD `type` varchar(64) DEFAULT 'generic' NOT NULL;--> statement-breakpoint
3 | ALTER TABLE `search_engine` ADD `integration_id` varchar(64);--> statement-breakpoint
4 | ALTER TABLE `search_engine` ADD CONSTRAINT `search_engine_integration_id_integration_id_fk` FOREIGN KEY (`integration_id`) REFERENCES `integration`(`id`) ON DELETE cascade ON UPDATE no action;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0017_tired_penance.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `onboarding` (
2 | `id` varchar(64) NOT NULL,
3 | `step` varchar(64) NOT NULL,
4 | `previous_step` varchar(64),
5 | CONSTRAINT `onboarding_id` PRIMARY KEY(`id`)
6 | );
7 |
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0018_mighty_shaman.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `search_engine` ADD CONSTRAINT `search_engine_short_unique` UNIQUE(`short`);
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0019_crazy_marvel_zombies.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `default_search_engine_id` varchar(64);--> statement-breakpoint
2 | ALTER TABLE `user` ADD CONSTRAINT `user_default_search_engine_id_search_engine_id_fk` FOREIGN KEY (`default_search_engine_id`) REFERENCES `search_engine`(`id`) ON DELETE set null ON UPDATE no action;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0020_salty_doorman.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `mobile_home_board_id` varchar(64);--> statement-breakpoint
2 | ALTER TABLE `user` ADD CONSTRAINT `user_mobile_home_board_id_board_id_fk` FOREIGN KEY (`mobile_home_board_id`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0021_fluffy_jocasta.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `open_search_in_new_tab` boolean DEFAULT false NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0023_fix_on_delete_actions.sql:
--------------------------------------------------------------------------------
1 | -- Custom SQL migration file, put your code below! --
2 | -- This file is empty as there was a bug in the migration script of sqlite, missing on delete actions. See https://github.com/homarr-labs/homarr/pull/2211 --
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0024_mean_vin_gonzales.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `board` ADD `disable_status` boolean DEFAULT false NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0026_add-border-radius.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `board` ADD `item_radius` text DEFAULT ('lg') NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0027_acoustic_karma.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `board` ADD `icon_color` text;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0028_add_app_ping_url.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `app` ADD `ping_url` text;
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0031_add_dynamic_section_options.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `section` ADD `options` text DEFAULT ('{"json": {}}');
--------------------------------------------------------------------------------
/packages/db/migrations/mysql/0032_add_trusted_certificate_hostnames.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `trusted_certificate_hostname` (
2 | `hostname` varchar(256) NOT NULL,
3 | `thumbprint` varchar(128) NOT NULL,
4 | `certificate` text NOT NULL,
5 | CONSTRAINT `trusted_certificate_hostname_hostname_thumbprint_pk` PRIMARY KEY(`hostname`,`thumbprint`)
6 | );
7 |
--------------------------------------------------------------------------------
/packages/db/migrations/run-seed.ts:
--------------------------------------------------------------------------------
1 | import { database } from "../driver";
2 | import { seedDataAsync } from "./seed";
3 |
4 | seedDataAsync(database)
5 | .then(() => {
6 | console.log("Seed complete");
7 | process.exit(0);
8 | })
9 | .catch((err) => {
10 | console.log("Seed failed\n\t", err);
11 | process.exit(1);
12 | });
13 |
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0002_cooing_sumo.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `item` ADD `advanced_options` text DEFAULT '{"json": {}}' NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0003_adorable_raider.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `serverSetting` (
2 | `key` text PRIMARY KEY NOT NULL,
3 | `value` text DEFAULT '{"json": {}}' NOT NULL
4 | );
5 | --> statement-breakpoint
6 | CREATE UNIQUE INDEX `serverSetting_key_unique` ON `serverSetting` (`key`);
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0005_lean_random.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `provider` text DEFAULT 'credentials' NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0007_known_ultragirl.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `colorScheme` text DEFAULT 'auto' NOT NULL;
2 |
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0008_third_thor.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `search_engine` (
2 | `id` text PRIMARY KEY NOT NULL,
3 | `icon_url` text NOT NULL,
4 | `name` text NOT NULL,
5 | `short` text NOT NULL,
6 | `description` text,
7 | `url_template` text NOT NULL
8 | );
9 |
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0009_stale_roulette.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `apiKey` (
2 | `id` text PRIMARY KEY NOT NULL,
3 | `apiKey` text NOT NULL,
4 | `salt` text NOT NULL,
5 | `userId` text NOT NULL,
6 | FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
7 | );
8 |
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0010_gorgeous_stingray.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `firstDayOfWeek` integer DEFAULT 1 NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0011_classy_angel.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `pingIconsEnabled` integer DEFAULT false NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0013_faithful_hex.sql:
--------------------------------------------------------------------------------
1 | CREATE UNIQUE INDEX `group_name_unique` ON `group` (`name`);
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0014_colorful_cargill.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `media` (
2 | `id` text PRIMARY KEY NOT NULL,
3 | `name` text NOT NULL,
4 | `content` blob NOT NULL,
5 | `content_type` text NOT NULL,
6 | `size` integer NOT NULL,
7 | `created_at` integer DEFAULT (unixepoch()) NOT NULL,
8 | `creator_id` text,
9 | FOREIGN KEY (`creator_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null
10 | );
11 |
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0017_small_rumiko_fujikawa.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `onboarding` (
2 | `id` text PRIMARY KEY NOT NULL,
3 | `step` text NOT NULL,
4 | `previous_step` text
5 | );
6 |
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0018_cheerful_tattoo.sql:
--------------------------------------------------------------------------------
1 | CREATE UNIQUE INDEX `search_engine_short_unique` ON `search_engine` (`short`);
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0019_steady_darkhawk.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `default_search_engine_id` text REFERENCES search_engine(id);
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0020_empty_hellfire_club.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `mobile_home_board_id` text REFERENCES board(id);
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0021_famous_bruce_banner.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `user` ADD `open_search_in_new_tab` integer DEFAULT true NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0022_modern_sunfire.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `section_collapse_state` (
2 | `user_id` text NOT NULL,
3 | `section_id` text NOT NULL,
4 | `collapsed` integer DEFAULT false NOT NULL,
5 | PRIMARY KEY(`user_id`, `section_id`),
6 | FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,
7 | FOREIGN KEY (`section_id`) REFERENCES `section`(`id`) ON UPDATE no action ON DELETE cascade
8 | );
9 |
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0024_bitter_scrambler.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `board` ADD `disable_status` integer DEFAULT false NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0026_add-border-radius.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `board` ADD `item_radius` text DEFAULT 'lg' NOT NULL;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0027_wooden_blizzard.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `board` ADD `icon_color` text;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0028_add_app_ping_url.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `app` ADD `ping_url` text;
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0031_add_dynamic_section_options.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE `section` ADD `options` text DEFAULT '{"json": {}}';
--------------------------------------------------------------------------------
/packages/db/migrations/sqlite/0032_add_trusted_certificate_hostnames.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE `trusted_certificate_hostname` (
2 | `hostname` text NOT NULL,
3 | `thumbprint` text NOT NULL,
4 | `certificate` text NOT NULL,
5 | PRIMARY KEY(`hostname`, `thumbprint`)
6 | );
7 |
--------------------------------------------------------------------------------
/packages/db/queries/group.ts:
--------------------------------------------------------------------------------
1 | import { max } from "drizzle-orm";
2 |
3 | import type { HomarrDatabase } from "../driver";
4 | import { groups } from "../schema";
5 |
6 | export const getMaxGroupPositionAsync = async (db: HomarrDatabase) => {
7 | return await db
8 | .select({ value: max(groups.position) })
9 | .from(groups)
10 | .then((result) => result[0]?.value ?? 1);
11 | };
12 |
--------------------------------------------------------------------------------
/packages/db/queries/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./item";
2 | export * from "./server-setting";
3 | export * from "./group";
4 |
--------------------------------------------------------------------------------
/packages/db/test/db-mock.ts:
--------------------------------------------------------------------------------
1 | import Database from "better-sqlite3";
2 | import { drizzle } from "drizzle-orm/better-sqlite3";
3 | import { migrate } from "drizzle-orm/better-sqlite3/migrator";
4 |
5 | import * as sqliteSchema from "../schema/sqlite";
6 |
7 | export const createDb = (debug?: boolean) => {
8 | const sqlite = new Database(":memory:");
9 | const db = drizzle(sqlite, { schema: sqliteSchema, logger: debug, casing: "snake_case" });
10 | migrate(db, {
11 | migrationsFolder: "./packages/db/migrations/sqlite",
12 | });
13 |
14 | if (debug) {
15 | console.log("Database created");
16 | }
17 |
18 | return db;
19 | };
20 |
--------------------------------------------------------------------------------
/packages/db/test/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./db-mock";
2 |
--------------------------------------------------------------------------------
/packages/db/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/db/validationSchemas.ts:
--------------------------------------------------------------------------------
1 | import { createSelectSchema } from "drizzle-zod";
2 |
3 | import { apps, boards, groups, invites, searchEngines, serverSettings, users } from "./schema";
4 |
5 | export const selectAppSchema = createSelectSchema(apps);
6 | export const selectBoardSchema = createSelectSchema(boards);
7 | export const selectGroupSchema = createSelectSchema(groups);
8 | export const selectInviteSchema = createSelectSchema(invites);
9 | export const selectSearchEnginesSchema = createSelectSchema(searchEngines);
10 | export const selectSeverSettingsSchema = createSelectSchema(serverSettings);
11 | export const selectUserSchema = createSelectSchema(users);
12 |
--------------------------------------------------------------------------------
/packages/definitions/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/definitions/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/definitions/src/_definition.ts:
--------------------------------------------------------------------------------
1 | export const createDefinition = (
2 | values: TKeys,
3 | options: TOptions,
4 | ) => ({
5 | values,
6 | defaultValue: options?.defaultValue as TOptions extends {
7 | defaultValue: infer T;
8 | }
9 | ? T
10 | : undefined,
11 | });
12 |
13 | export type inferDefinitionType = TDefinition extends {
14 | values: readonly (infer T)[];
15 | }
16 | ? T
17 | : never;
18 |
--------------------------------------------------------------------------------
/packages/definitions/src/auth.ts:
--------------------------------------------------------------------------------
1 | export const supportedAuthProviders = ["credentials", "oidc", "ldap"] as const;
2 | export type SupportedAuthProvider = (typeof supportedAuthProviders)[number];
3 |
--------------------------------------------------------------------------------
/packages/definitions/src/cookie.ts:
--------------------------------------------------------------------------------
1 | export const colorSchemeCookieKey = "homarr.color-scheme";
2 | export const localeCookieKey = "homarr.locale";
3 |
--------------------------------------------------------------------------------
/packages/definitions/src/docker.ts:
--------------------------------------------------------------------------------
1 | export const dockerContainerStates = [
2 | "created",
3 | "running",
4 | "paused",
5 | "restarting",
6 | "exited",
7 | "removing",
8 | "dead",
9 | ] as const;
10 |
11 | export type DockerContainerState = (typeof dockerContainerStates)[number];
12 |
--------------------------------------------------------------------------------
/packages/definitions/src/docs/index.ts:
--------------------------------------------------------------------------------
1 | import type { HomarrDocumentationPath } from "./homarr-docs-sitemap";
2 |
3 | const documentationBaseUrl = "https://homarr.dev";
4 |
5 | // Please use the method so the path can be checked!
6 | export const createDocumentationLink = (
7 | path: HomarrDocumentationPath,
8 | hashTag?: `#${string}`,
9 | queryParams?: Record,
10 | ) => {
11 | const url = `${documentationBaseUrl}${path}`;
12 | const params = queryParams ? `?${new URLSearchParams(queryParams)}` : "";
13 | return `${url}${params}${hashTag ?? ""}`;
14 | };
15 |
--------------------------------------------------------------------------------
/packages/definitions/src/emptysuperjson.ts:
--------------------------------------------------------------------------------
1 | export const emptySuperJSON = '{"json": {}}';
2 |
--------------------------------------------------------------------------------
/packages/definitions/src/group.ts:
--------------------------------------------------------------------------------
1 | export const everyoneGroup = "everyone";
2 | export const credentialsAdminGroup = "credentials-admin";
3 |
--------------------------------------------------------------------------------
/packages/definitions/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./board";
2 | export * from "./integration";
3 | export * from "./section";
4 | export * from "./widget";
5 | export * from "./permissions";
6 | export * from "./docker";
7 | export * from "./kubernetes";
8 | export * from "./auth";
9 | export * from "./user";
10 | export * from "./group";
11 | export * from "./docs";
12 | export * from "./cookie";
13 | export * from "./search-engine";
14 | export * from "./onboarding";
15 | export * from "./emptysuperjson";
16 |
--------------------------------------------------------------------------------
/packages/definitions/src/onboarding.ts:
--------------------------------------------------------------------------------
1 | export const onboardingSteps = ["start", "import", "user", "group", "settings", "finish"] as const;
2 | export type OnboardingStep = (typeof onboardingSteps)[number];
3 |
--------------------------------------------------------------------------------
/packages/definitions/src/search-engine.ts:
--------------------------------------------------------------------------------
1 | export const searchEngineTypes = ["generic", "fromIntegration"] as const;
2 | export type SearchEngineType = (typeof searchEngineTypes)[number];
3 |
--------------------------------------------------------------------------------
/packages/definitions/src/section.ts:
--------------------------------------------------------------------------------
1 | export const sectionKinds = ["category", "empty", "dynamic"] as const;
2 | export type SectionKind = (typeof sectionKinds)[number];
3 |
--------------------------------------------------------------------------------
/packages/definitions/src/test/integration.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 |
3 | import { objectEntries } from "@homarr/common";
4 |
5 | import { integrationDefs } from "../integration";
6 |
7 | describe("Icon url's of integrations should be valid and return 200", () => {
8 | objectEntries(integrationDefs).forEach(([integration, { iconUrl }]) => {
9 | it.concurrent(`should return 200 for ${integration}`, async () => {
10 | const res = await fetch(iconUrl);
11 | expect(res.status).toBe(200);
12 | });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/packages/definitions/src/user.ts:
--------------------------------------------------------------------------------
1 | export const colorSchemes = ["light", "dark"] as const;
2 | export type ColorScheme = (typeof colorSchemes)[number];
3 |
--------------------------------------------------------------------------------
/packages/definitions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/docker/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/docker/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/docker/src/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | import { createEnv } from "@homarr/env";
4 | import { createBooleanSchema } from "@homarr/env/schemas";
5 |
6 | export const env = createEnv({
7 | server: {
8 | // Comma separated list of docker hostnames that can be used to connect to query the docker endpoints (localhost:2375,host.docker.internal:2375, ...)
9 | DOCKER_HOSTNAMES: z.string().optional(),
10 | DOCKER_PORTS: z.string().optional(),
11 | ENABLE_DOCKER: createBooleanSchema(true),
12 | ENABLE_KUBERNETES: createBooleanSchema(false),
13 | },
14 | experimental__runtimeEnv: process.env,
15 | });
16 |
--------------------------------------------------------------------------------
/packages/docker/src/index.ts:
--------------------------------------------------------------------------------
1 | import type Docker from "dockerode";
2 |
3 | export type { DockerInstance } from "./singleton";
4 | export { DockerSingleton } from "./singleton";
5 | export type { ContainerInfo, Container, Port } from "dockerode";
6 | export type { Docker };
7 |
8 | export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const;
9 |
10 | export type ContainerState = (typeof containerStates)[number];
11 |
--------------------------------------------------------------------------------
/packages/docker/src/shared.ts:
--------------------------------------------------------------------------------
1 | import type { MantineColor } from "@mantine/core";
2 |
3 | import type { ContainerState } from ".";
4 |
5 | export const containerStateColorMap = {
6 | created: "cyan",
7 | running: "green",
8 | paused: "yellow",
9 | restarting: "orange",
10 | exited: "red",
11 | removing: "pink",
12 | dead: "dark",
13 | } satisfies Record;
14 |
--------------------------------------------------------------------------------
/packages/docker/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/env/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/env/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/env/src/index.ts:
--------------------------------------------------------------------------------
1 | import { createEnv as createEnvT3 } from "@t3-oss/env-nextjs";
2 |
3 | export const defaultEnvOptions = {
4 | emptyStringAsUndefined: true,
5 | skipValidation:
6 | Boolean(process.env.CI) || Boolean(process.env.SKIP_ENV_VALIDATION) || process.env.npm_lifecycle_event === "lint",
7 | } satisfies Partial[0]>;
8 |
9 | export const createEnv: typeof createEnvT3 = (options) => createEnvT3({ ...defaultEnvOptions, ...options });
10 |
--------------------------------------------------------------------------------
/packages/env/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/form/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/form/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 | export * from "@mantine/form";
3 |
--------------------------------------------------------------------------------
/packages/form/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/forms-collection/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/forms-collection/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/forms-collection/src/icon-picker/icon-picker.module.css:
--------------------------------------------------------------------------------
1 | [data-combobox-selected="true"] .iconCard {
2 | border-color: var(--mantine-primary-color-6);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/forms-collection/src/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./new-app/_app-new-form";
2 | export * from "./new-app/_form";
3 |
4 | export * from "./icon-picker/icon-picker";
5 | export * from "./new-app/icon-matcher";
6 |
7 | export * from "./upload-media/upload-media";
8 |
--------------------------------------------------------------------------------
/packages/forms-collection/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src", "index.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/icons/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src/icons-fetcher";
2 | export * from "./src/types";
3 | export * from "./src/auto-icon-searcher";
4 |
--------------------------------------------------------------------------------
/packages/icons/src/auto-icon-searcher.ts:
--------------------------------------------------------------------------------
1 | import type { Database } from "@homarr/db";
2 | import { like } from "@homarr/db";
3 | import { icons } from "@homarr/db/schema";
4 |
5 | export const getIconForName = (db: Database, name: string) => {
6 | return db.query.icons.findFirst({
7 | where: like(icons.name, `%${name}%`),
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/packages/icons/src/local.ts:
--------------------------------------------------------------------------------
1 | export { createLocalImageUrl, mapMediaToIcon, LOCAL_ICON_REPOSITORY_SLUG } from "./repositories/local.icon-repository";
2 |
--------------------------------------------------------------------------------
/packages/icons/src/types/icon-repository-license.ts:
--------------------------------------------------------------------------------
1 | export type IconRepositoryLicense = "MIT" | "GPL-3.0" | "CC0-1.0" | undefined;
2 |
--------------------------------------------------------------------------------
/packages/icons/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./icon-repository-license";
2 | export * from "./repository-icon-group";
3 | export * from "./repository-icon";
4 |
--------------------------------------------------------------------------------
/packages/icons/src/types/repository-icon-group.ts:
--------------------------------------------------------------------------------
1 | import type { RepositoryIcon } from "./repository-icon";
2 |
3 | export interface RepositoryIconGroup {
4 | icons: RepositoryIcon[];
5 | success: boolean;
6 | slug: string;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/icons/src/types/repository-icon.ts:
--------------------------------------------------------------------------------
1 | export interface RepositoryIcon {
2 | fileNameWithExtension: string;
3 | sizeInBytes?: number;
4 | imageUrl: string;
5 | local: boolean;
6 | checksum: string;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/icons/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/integrations/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/integrations/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/errors/handler.ts:
--------------------------------------------------------------------------------
1 | import type { IntegrationError, IntegrationErrorData } from "./integration-error";
2 |
3 | export interface IIntegrationErrorHandler {
4 | handleError(error: unknown, integration: IntegrationErrorData): IntegrationError | undefined;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/errors/http/integration-response-error.ts:
--------------------------------------------------------------------------------
1 | import type { ResponseError } from "@homarr/common/server";
2 |
3 | import type { IntegrationErrorData } from "../integration-error";
4 | import { IntegrationError } from "../integration-error";
5 |
6 | export class IntegrationResponseError extends IntegrationError {
7 | constructor(integration: IntegrationErrorData, { cause }: { cause: ResponseError }) {
8 | super(integration, "Response from integration did not indicate success", { cause });
9 | this.name = IntegrationResponseError.name;
10 | }
11 |
12 | get cause(): ResponseError {
13 | return super.cause as ResponseError;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/errors/integration-error.ts:
--------------------------------------------------------------------------------
1 | export interface IntegrationErrorData {
2 | id: string;
3 | name: string;
4 | url: string;
5 | }
6 |
7 | export abstract class IntegrationError extends Error {
8 | public readonly integrationId: string;
9 | public readonly integrationName: string;
10 | public readonly integrationUrl: string;
11 |
12 | constructor(integration: IntegrationErrorData, message: string, { cause }: ErrorOptions) {
13 | super(message, { cause });
14 | this.integrationId = integration.id;
15 | this.integrationName = integration.name;
16 | this.integrationUrl = integration.url;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/errors/integration-unknown-error.ts:
--------------------------------------------------------------------------------
1 | import type { IntegrationErrorData } from "./integration-error";
2 | import { IntegrationError } from "./integration-error";
3 |
4 | export class IntegrationUnknownError extends IntegrationError {
5 | constructor(integration: IntegrationErrorData, { cause }: ErrorOptions) {
6 | super(integration, "An unknown error occured while executing Integration method", { cause });
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/errors/parse/index.ts:
--------------------------------------------------------------------------------
1 | import { JsonParseErrorHandler, ZodParseErrorHandler } from "@homarr/common/server";
2 |
3 | import { IntegrationParseErrorHandler } from "./integration-parse-error-handler";
4 |
5 | export const integrationZodParseErrorHandler = new IntegrationParseErrorHandler(new ZodParseErrorHandler());
6 | export const integrationJsonParseErrorHandler = new IntegrationParseErrorHandler(new JsonParseErrorHandler());
7 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/errors/parse/integration-parse-error.ts:
--------------------------------------------------------------------------------
1 | import type { ParseError } from "@homarr/common/server";
2 |
3 | import type { IntegrationErrorData } from "../integration-error";
4 | import { IntegrationError } from "../integration-error";
5 |
6 | export class IntegrationParseError extends IntegrationError {
7 | constructor(integration: IntegrationErrorData, { cause }: { cause: ParseError }) {
8 | super(integration, "Failed to parse integration data", { cause });
9 | this.name = IntegrationParseError.name;
10 | }
11 |
12 | get cause(): ParseError {
13 | return super.cause as ParseError;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/searchable-integration.ts:
--------------------------------------------------------------------------------
1 | export interface ISearchableIntegration {
2 | searchAsync(query: string): Promise;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/test-connection/index.ts:
--------------------------------------------------------------------------------
1 | export type {
2 | TestConnectionError,
3 | AnyTestConnectionError,
4 | TestConnectionErrorDataOfType,
5 | TestConnectionErrorType,
6 | } from "./test-connection-error";
7 |
--------------------------------------------------------------------------------
/packages/integrations/src/base/types.ts:
--------------------------------------------------------------------------------
1 | import type { IntegrationSecretKind } from "@homarr/definitions";
2 |
3 | export interface IntegrationSecret {
4 | kind: IntegrationSecretKind;
5 | value: string;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/integrations/src/homeassistant/homeassistant-types.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const entityStateSchema = z.object({
4 | attributes: z.record(
5 | z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.union([z.string(), z.number()]))]),
6 | ),
7 | entity_id: z.string(),
8 | last_changed: z.string().pipe(z.coerce.date()),
9 | last_updated: z.string().pipe(z.coerce.date()),
10 | state: z.string(),
11 | });
12 |
13 | export type EntityState = z.infer;
14 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts:
--------------------------------------------------------------------------------
1 | import type { DnsHoleSummary } from "./dns-hole-summary-types";
2 |
3 | export interface DnsHoleSummaryIntegration {
4 | getSummaryAsync(): Promise;
5 | enableAsync(): Promise;
6 | disableAsync(duration?: number): Promise;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-types.ts:
--------------------------------------------------------------------------------
1 | export interface DnsHoleSummary {
2 | status?: "enabled" | "disabled";
3 | domainsBeingBlocked: number;
4 | adsBlockedToday: number;
5 | adsBlockedTodayPercentage: number;
6 | dnsQueriesToday: number;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/downloads/download-client-data.ts:
--------------------------------------------------------------------------------
1 | import type { DownloadClientItem } from "./download-client-items";
2 | import type { DownloadClientStatus } from "./download-client-status";
3 |
4 | export interface DownloadClientJobsAndStatus {
5 | status: DownloadClientStatus;
6 | items: DownloadClientItem[];
7 | }
8 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/indexer-manager/indexer.ts:
--------------------------------------------------------------------------------
1 | export interface Indexer {
2 | id: number;
3 | name: string;
4 | url: string;
5 | /**
6 | * Enabled: when the user enable / disable the indexer.
7 | * Status: when there is an error with the indexer site.
8 | * If one of the options are false the indexer is off.
9 | */
10 | enabled: boolean;
11 | status: boolean;
12 | }
13 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/media-server/session.ts:
--------------------------------------------------------------------------------
1 | export interface StreamSession {
2 | sessionId: string;
3 | sessionName: string;
4 | user: {
5 | userId: string;
6 | username: string;
7 | profilePictureUrl: string | null;
8 | };
9 | currentlyPlaying: {
10 | type: "audio" | "video" | "tv" | "movie";
11 | name: string;
12 | seasonName: string | undefined;
13 | episodeName?: string | null;
14 | albumName?: string | null;
15 | episodeCount?: number | null;
16 | } | null;
17 | }
18 |
19 | export interface CurrentSessionsInput {
20 | showOnlyPlaying: boolean;
21 | }
22 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/media-transcoding/queue.ts:
--------------------------------------------------------------------------------
1 | export interface TdarrQueue {
2 | array: {
3 | id: string;
4 | healthCheck: string;
5 | transcode: string;
6 | filePath: string;
7 | fileSize: number;
8 | container: string;
9 | codec: string;
10 | resolution: string;
11 | type: "transcode" | "health-check";
12 | }[];
13 | totalCount: number;
14 | startIndex: number;
15 | endIndex: number;
16 | }
17 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/media-transcoding/workers.ts:
--------------------------------------------------------------------------------
1 | export interface TdarrWorker {
2 | id: string;
3 | filePath: string;
4 | fps: number;
5 | percentage: number;
6 | ETA: string;
7 | jobType: string;
8 | status: string;
9 | step: string;
10 | originalSize: number;
11 | estimatedSize: number | null;
12 | outputSize: number | null;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/network-controller-summary/network-controller-summary-integration.ts:
--------------------------------------------------------------------------------
1 | import type { NetworkControllerSummary } from "./network-controller-summary-types";
2 |
3 | export interface NetworkControllerSummaryIntegration {
4 | getNetworkSummaryAsync(): Promise;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/integrations/src/interfaces/network-controller-summary/network-controller-summary-types.ts:
--------------------------------------------------------------------------------
1 | export interface NetworkControllerSummary {
2 | wanStatus: "enabled" | "disabled";
3 |
4 | www: {
5 | status: "enabled" | "disabled";
6 | latency: number;
7 | ping: number;
8 | uptime: number;
9 | };
10 |
11 | wifi: {
12 | status: "enabled" | "disabled";
13 | users: number;
14 | guests: number;
15 | };
16 |
17 | lan: {
18 | status: "enabled" | "disabled";
19 | users: number;
20 | guests: number;
21 | };
22 |
23 | vpn: {
24 | status: "enabled" | "disabled";
25 | users: number;
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/packages/integrations/src/jellyseerr/jellyseerr-integration.ts:
--------------------------------------------------------------------------------
1 | import { OverseerrIntegration } from "../overseerr/overseerr-integration";
2 |
3 | export class JellyseerrIntegration extends OverseerrIntegration {}
4 |
--------------------------------------------------------------------------------
/packages/integrations/src/pi-hole/v5/pi-hole-schemas-v5.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const summaryResponseSchema = z.object({
4 | status: z.enum(["enabled", "disabled"]),
5 | domains_being_blocked: z.number(),
6 | ads_blocked_today: z.number(),
7 | dns_queries_today: z.number(),
8 | ads_percentage_today: z.number(),
9 | });
10 |
--------------------------------------------------------------------------------
/packages/integrations/src/prowlarr/prowlarr-types.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const indexerResponseSchema = z.object({
4 | id: z.number(),
5 | indexerUrls: z.array(z.string()),
6 | name: z.string(),
7 | enable: z.boolean(),
8 | });
9 |
10 | export const statusResponseSchema = z.array(
11 | z.object({
12 | indexerId: z.number(),
13 | }),
14 | );
15 |
--------------------------------------------------------------------------------
/packages/integrations/src/types.ts:
--------------------------------------------------------------------------------
1 | export * from "./calendar-types";
2 | export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
3 | export * from "./interfaces/network-controller-summary/network-controller-summary-types";
4 | export * from "./interfaces/health-monitoring/healt-monitoring";
5 | export * from "./interfaces/indexer-manager/indexer";
6 | export * from "./interfaces/media-requests/media-request";
7 | export * from "./base/searchable-integration";
8 | export * from "./homeassistant/homeassistant-types";
9 | export * from "./proxmox/proxmox-types";
10 | export * from "./unifi-controller/unifi-controller-types";
11 |
--------------------------------------------------------------------------------
/packages/integrations/src/unifi-controller/unifi-controller-types.ts:
--------------------------------------------------------------------------------
1 | import type { SiteStats } from "node-unifi";
2 |
3 | export type HealthSubsystem = SiteStats["health"][number]["subsystem"];
4 |
--------------------------------------------------------------------------------
/packages/integrations/test/volumes/home-assistant-config.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/homarr-labs/homarr/ccd8a85e0fcd2cf40dbfb6058787d24a02680b93/packages/integrations/test/volumes/home-assistant-config.zip
--------------------------------------------------------------------------------
/packages/integrations/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src", "test"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/log/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/log/src/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | import { createEnv } from "@homarr/env";
4 |
5 | export const env = createEnv({
6 | server: {
7 | LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
8 | },
9 | experimental__runtimeEnv: process.env,
10 | });
11 |
--------------------------------------------------------------------------------
/packages/log/src/metadata.ts:
--------------------------------------------------------------------------------
1 | export const formatMetadata = (metadata: Record | Error, ignoreKeys?: string[]) => {
2 | const filteredMetadata = Object.keys(metadata)
3 | .filter((key) => !ignoreKeys?.includes(key))
4 | .map((key) => ({ key, value: metadata[key as keyof typeof metadata] }))
5 | .filter(({ value }) => typeof value !== "object" && typeof value !== "function");
6 |
7 | return filteredMetadata.map(({ key, value }) => `${key}="${value as string}"`).join(" ");
8 | };
9 |
--------------------------------------------------------------------------------
/packages/log/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/modals-collection/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/modals-collection/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/apps/index.ts:
--------------------------------------------------------------------------------
1 | export { AppSelectModal } from "./app-select-modal";
2 | export { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal";
3 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/boards/index.ts:
--------------------------------------------------------------------------------
1 | export { AddBoardModal } from "./add-board-modal";
2 | export { ImportBoardModal } from "./import-board-modal";
3 | export { DuplicateBoardModal } from "./duplicate-board-modal";
4 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/certificates/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./add-certificate-modal";
2 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/docker/index.ts:
--------------------------------------------------------------------------------
1 | export { AddDockerAppToHomarrModal as AddDockerAppToHomarr } from "./add-docker-app-to-homarr";
2 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/groups/index.ts:
--------------------------------------------------------------------------------
1 | export { AddGroupModal } from "./add-group-modal";
2 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./boards";
2 | export * from "./invites";
3 | export * from "./groups";
4 | export * from "./search-engines";
5 | export * from "./docker";
6 | export * from "./apps";
7 | export * from "./certificates";
8 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/invites/index.ts:
--------------------------------------------------------------------------------
1 | export { InviteCopyModal } from "./invite-copy-modal";
2 | export { InviteCreateModal } from "./invite-create-modal";
3 |
--------------------------------------------------------------------------------
/packages/modals-collection/src/search-engines/index.ts:
--------------------------------------------------------------------------------
1 | export { RequestMediaModal } from "./request-media-modal";
2 |
--------------------------------------------------------------------------------
/packages/modals-collection/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/modals/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 | import reactConfig from "@homarr/eslint-config/react";
3 |
4 | /** @type {import('typescript-eslint').Config} */
5 | export default [...baseConfig, ...reactConfig];
6 |
--------------------------------------------------------------------------------
/packages/modals/index.ts:
--------------------------------------------------------------------------------
1 | export { ModalProvider, useModalAction, useConfirmModal } from "./src";
2 | export { createModal } from "./src/creator";
3 |
--------------------------------------------------------------------------------
/packages/modals/src/creator.ts:
--------------------------------------------------------------------------------
1 | import type { CreateModalOptions, ModalComponent } from "./type";
2 |
3 | export const createModal = (component: ModalComponent) => {
4 | return {
5 | withOptions: (options: Partial) => {
6 | return {
7 | component,
8 | options,
9 | };
10 | },
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/packages/modals/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "*.tsx", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/notifications/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/notifications/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 | export { Notifications } from "@mantine/notifications";
3 |
--------------------------------------------------------------------------------
/packages/notifications/src/index.tsx:
--------------------------------------------------------------------------------
1 | import type { NotificationData } from "@mantine/notifications";
2 | import { notifications } from "@mantine/notifications";
3 | import { IconCheck, IconX } from "@tabler/icons-react";
4 |
5 | export const showSuccessNotification = (props: NotificationData) =>
6 | notifications.show({
7 | ...props,
8 | color: "teal",
9 | icon: ,
10 | });
11 |
12 | export const showErrorNotification = (props: NotificationData) =>
13 | notifications.show({
14 | ...props,
15 | color: "red",
16 | icon: ,
17 | });
18 |
--------------------------------------------------------------------------------
/packages/notifications/src/styles.css:
--------------------------------------------------------------------------------
1 | @import "@mantine/notifications/styles.css";
2 |
--------------------------------------------------------------------------------
/packages/notifications/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/old-import/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/old-import/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/old-import/src/analyse/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./input";
2 | export { analyseOldmarrImportForRouterAsync } from "./analyse-oldmarr-import";
3 |
--------------------------------------------------------------------------------
/packages/old-import/src/analyse/input.ts:
--------------------------------------------------------------------------------
1 | import { zfd } from "zod-form-data";
2 |
3 | export const analyseOldmarrImportInputSchema = zfd.formData({
4 | file: zfd.file(),
5 | });
6 |
--------------------------------------------------------------------------------
/packages/old-import/src/analyse/types.ts:
--------------------------------------------------------------------------------
1 | import type { Modify } from "@homarr/common/types";
2 | import type { OldmarrConfig } from "@homarr/old-schema";
3 |
4 | import type { AnalyseResult } from "./analyse-oldmarr-import";
5 |
6 | export type AnalyseConfig = AnalyseResult["configs"][number];
7 | export type ValidAnalyseConfig = Modify;
8 |
--------------------------------------------------------------------------------
/packages/old-import/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { InitialOldmarrImport } from "./initial-oldmarr-import";
2 | export { SidebarBehaviourSelect } from "./shared/sidebar-behaviour-select";
3 | export { OldmarrImportAppsSettings } from "./shared/apps-section";
4 |
--------------------------------------------------------------------------------
/packages/old-import/src/import-error.ts:
--------------------------------------------------------------------------------
1 | import type { OldmarrConfig } from "@homarr/old-schema";
2 |
3 | export class OldHomarrImportError extends Error {
4 | constructor(oldConfig: OldmarrConfig, cause: unknown) {
5 | super(`Failed to import old homarr configuration name=${oldConfig.configProperties.name}`, {
6 | cause,
7 | });
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/old-import/src/import/index.ts:
--------------------------------------------------------------------------------
1 | export { importInitialOldmarrAsync } from "./import-initial-oldmarr";
2 | export * from "./input";
3 | export { ensureValidTokenOrThrow } from "./validate-token";
4 |
--------------------------------------------------------------------------------
/packages/old-import/src/import/input.ts:
--------------------------------------------------------------------------------
1 | import SuperJSON from "superjson";
2 | import { z } from "zod";
3 | import { zfd } from "zod-form-data";
4 |
5 | import { initialOldmarrImportSettings } from "../settings";
6 |
7 | const boardSelectionMapSchema = z.map(z.string(), z.boolean());
8 |
9 | export const importInitialOldmarrInputSchema = zfd.formData({
10 | file: zfd.file(),
11 | settings: zfd.json(initialOldmarrImportSettings),
12 | boardSelections: zfd.text().transform((value) => {
13 | const map = boardSelectionMapSchema.parse(SuperJSON.parse(value));
14 | return map;
15 | }),
16 | token: zfd.text().nullable().optional(),
17 | });
18 |
--------------------------------------------------------------------------------
/packages/old-import/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { Database } from "@homarr/db";
2 | import type { OldmarrConfig } from "@homarr/old-schema";
3 |
4 | import { importSingleOldmarrConfigAsync } from "./import/import-single-oldmarr";
5 | import type { OldmarrImportConfiguration } from "./settings";
6 |
7 | export const importOldmarrAsync = async (
8 | db: Database,
9 | old: OldmarrConfig,
10 | configuration: OldmarrImportConfiguration,
11 | ) => {
12 | await importSingleOldmarrConfigAsync(db, old, configuration);
13 | };
14 |
--------------------------------------------------------------------------------
/packages/old-import/src/mappers/map-breakpoint.ts:
--------------------------------------------------------------------------------
1 | import type { BoardSize } from "@homarr/old-schema";
2 |
3 | /**
4 | * Copied from https://github.com/ajnart/homarr/blob/274eaa92084a8be4d04a69a87f9920860a229128/src/components/Dashboard/Wrappers/gridstack/store.tsx#L21-L30
5 | * @param screenSize board size
6 | * @returns layout breakpoint for the board
7 | */
8 | export const mapBreakpoint = (screenSize: BoardSize) => {
9 | switch (screenSize) {
10 | case "lg":
11 | return 1400;
12 | case "md":
13 | return 800;
14 | case "sm":
15 | default:
16 | return 0;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/packages/old-import/src/mappers/map-column-count.ts:
--------------------------------------------------------------------------------
1 | import type { BoardSize, OldmarrConfig } from "@homarr/old-schema";
2 |
3 | export const mapColumnCount = (
4 | gridstackSettings: OldmarrConfig["settings"]["customization"]["gridstack"],
5 | screenSize: BoardSize,
6 | ) => {
7 | switch (screenSize) {
8 | case "lg":
9 | return gridstackSettings.columnCountLarge;
10 | case "md":
11 | return gridstackSettings.columnCountMedium;
12 | case "sm":
13 | return gridstackSettings.columnCountSmall;
14 | default:
15 | return 10;
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/packages/old-import/src/prepare/prepare-boards.ts:
--------------------------------------------------------------------------------
1 | import type { ValidAnalyseConfig } from "../analyse/types";
2 | import type { BoardSelectionMap } from "../components/initial/board-selection-card";
3 |
4 | export const prepareBoards = (analyseConfigs: ValidAnalyseConfig[], selections: BoardSelectionMap) => {
5 | return analyseConfigs.filter(({ name }) => selections.get(name));
6 | };
7 |
--------------------------------------------------------------------------------
/packages/old-import/src/prepare/prepare-integrations.ts:
--------------------------------------------------------------------------------
1 | import type { ValidAnalyseConfig } from "../analyse/types";
2 |
3 | export type PreparedIntegration = ReturnType[number];
4 |
5 | export const prepareIntegrations = (analyseConfigs: ValidAnalyseConfig[]) => {
6 | return analyseConfigs.flatMap(({ config }) => {
7 | return config.apps
8 | .map((app) =>
9 | app.integration?.type
10 | ? {
11 | ...app.integration,
12 | name: app.name,
13 | url: app.url,
14 | }
15 | : null,
16 | )
17 | .filter((integration) => integration !== null);
18 | });
19 | };
20 |
--------------------------------------------------------------------------------
/packages/old-import/src/prepare/prepare-sections.ts:
--------------------------------------------------------------------------------
1 | import type { OldmarrConfig } from "@homarr/old-schema";
2 |
3 | import { mapCategorySection, mapEmptySection } from "../mappers/map-section";
4 |
5 | export const prepareSections = (
6 | boardId: string,
7 | { categories, wrappers }: Pick,
8 | ) =>
9 | new Map(
10 | categories
11 | .map((category) => [category.id, mapCategorySection(boardId, category)] as const)
12 | .concat(wrappers.map((wrapper) => [wrapper.id, mapEmptySection(boardId, wrapper)] as const)),
13 | );
14 |
--------------------------------------------------------------------------------
/packages/old-import/src/prepare/prepare-single.ts:
--------------------------------------------------------------------------------
1 | import type { OldmarrConfig } from "@homarr/old-schema";
2 |
3 | import type { OldmarrImportConfiguration } from "../settings";
4 | import { prepareApps } from "./prepare-apps";
5 |
6 | export const prepareSingleImport = (config: OldmarrConfig, settings: OldmarrImportConfiguration) => {
7 | const validAnalyseConfigs = [{ name: settings.name, config, isError: false }];
8 |
9 | return {
10 | preparedApps: prepareApps(validAnalyseConfigs),
11 | preparedBoards: settings.onlyImportApps ? [] : validAnalyseConfigs,
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/packages/old-import/src/shared.ts:
--------------------------------------------------------------------------------
1 | export { importJsonFileSchema, superRefineJsonImportFile, oldmarrImportConfigurationSchema } from "./settings";
2 | export type { OldmarrImportConfiguration } from "./settings";
3 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/bookmark.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrBookmarkDefinition = CommonOldmarrWidgetDefinition<
4 | "bookmark",
5 | {
6 | name: string;
7 | items: {
8 | id: string;
9 | name: string;
10 | href: string;
11 | iconUrl: string;
12 | openNewTab: boolean;
13 | hideHostname: boolean;
14 | hideIcon: boolean;
15 | }[];
16 | layout: "autoGrid" | "horizontal" | "vertical";
17 | }
18 | >;
19 |
20 | export type BookmarkApp = OldmarrBookmarkDefinition["options"]["items"][number];
21 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/calendar.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrCalendarDefinition = CommonOldmarrWidgetDefinition<
4 | "calendar",
5 | {
6 | hideWeekDays: boolean;
7 | showUnmonitored: boolean;
8 | radarrReleaseType: "inCinemas" | "physicalRelease" | "digitalRelease";
9 | fontSize: "xs" | "sm" | "md" | "lg" | "xl";
10 | }
11 | >;
12 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/common.ts:
--------------------------------------------------------------------------------
1 | import type { OldmarrWidgetKind } from "@homarr/old-schema";
2 |
3 | export interface CommonOldmarrWidgetDefinition<
4 | TId extends OldmarrWidgetKind,
5 | TOptions extends Record,
6 | > {
7 | id: TId;
8 | options: TOptions;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/date.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrDateDefinition = CommonOldmarrWidgetDefinition<
4 | "date",
5 | {
6 | timezone: string;
7 | customTitle: string;
8 | display24HourFormat: boolean;
9 | dateFormat:
10 | | "hide"
11 | | "dddd, MMMM D"
12 | | "dddd, D MMMM"
13 | | "MMM D"
14 | | "D MMM"
15 | | "DD/MM/YYYY"
16 | | "MM/DD/YYYY"
17 | | "DD/MM"
18 | | "MM/DD";
19 | titleState: "none" | "city" | "both";
20 | }
21 | >;
22 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/dlspeed.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
4 | export type OldmarrDlspeedDefinition = CommonOldmarrWidgetDefinition<"dlspeed", {}>;
5 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/dns-hole-controls.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrDnsHoleControlsDefinition = CommonOldmarrWidgetDefinition<
4 | "dns-hole-controls",
5 | {
6 | showToggleAllButtons: boolean;
7 | }
8 | >;
9 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/dns-hole-summary.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrDnsHoleSummaryDefinition = CommonOldmarrWidgetDefinition<
4 | "dns-hole-summary",
5 | { usePiHoleColors: boolean; layout: "column" | "row" | "grid" }
6 | >;
7 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/health-monitoring.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrHealthMonitoringDefinition = CommonOldmarrWidgetDefinition<
4 | "health-monitoring",
5 | {
6 | fahrenheit: boolean;
7 | cpu: boolean;
8 | memory: boolean;
9 | fileSystem: boolean;
10 | defaultTabState: "system" | "cluster";
11 | node: string;
12 | defaultViewState: "storage" | "none" | "node" | "vm" | "lxc";
13 | summary: boolean;
14 | showNode: boolean;
15 | showVM: boolean;
16 | showLXCs: boolean;
17 | showStorage: boolean;
18 | sectionIndicatorColor: "all" | "any";
19 | ignoreCert: boolean;
20 | }
21 | >;
22 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/iframe.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrIframeDefinition = CommonOldmarrWidgetDefinition<
4 | "iframe",
5 | {
6 | embedUrl: string;
7 | allowFullScreen: boolean;
8 | allowScrolling: boolean;
9 | allowTransparency: boolean;
10 | allowPayment: boolean;
11 | allowAutoPlay: boolean;
12 | allowMicrophone: boolean;
13 | allowCamera: boolean;
14 | allowGeolocation: boolean;
15 | }
16 | >;
17 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/indexer-manager.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrIndexerManagerDefinition = CommonOldmarrWidgetDefinition<
4 | "indexer-manager",
5 | {
6 | openIndexerSiteInNewTab: boolean;
7 | }
8 | >;
9 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/media-requests-list.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrMediaRequestListDefinition = CommonOldmarrWidgetDefinition<
4 | "media-requests-list",
5 | {
6 | replaceLinksWithExternalHost: boolean;
7 | openInNewTab: boolean;
8 | }
9 | >;
10 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/media-requests-stats.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrMediaRequestStatsDefinition = CommonOldmarrWidgetDefinition<
4 | "media-requests-stats",
5 | {
6 | replaceLinksWithExternalHost: boolean;
7 | openInNewTab: boolean;
8 | }
9 | >;
10 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/media-server.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
4 | export type OldmarrMediaServerDefinition = CommonOldmarrWidgetDefinition<"media-server", {}>;
5 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/media-transcoding.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrMediaTranscodingDefinition = CommonOldmarrWidgetDefinition<
4 | "media-transcoding",
5 | {
6 | defaultView: "workers" | "queue" | "statistics";
7 | showHealthCheck: boolean;
8 | showHealthChecksInQueue: boolean;
9 | queuePageSize: number;
10 | showAppIcon: boolean;
11 | }
12 | >;
13 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/notebook.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrNotebookDefinition = CommonOldmarrWidgetDefinition<
4 | "notebook",
5 | {
6 | showToolbar: boolean;
7 | allowReadOnlyCheck: boolean;
8 | content: string;
9 | }
10 | >;
11 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/rss.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrRssDefinition = CommonOldmarrWidgetDefinition<
4 | "rss",
5 | {
6 | rssFeedUrl: string[];
7 | enableRtl: boolean;
8 | refreshInterval: number;
9 | dangerousAllowSanitizedItemContent: boolean;
10 | textLinesClamp: number;
11 | sortByPublishDateAscending: boolean;
12 | sortPostsWithoutPublishDateToTheTop: boolean;
13 | maximumAmountOfPosts: number;
14 | }
15 | >;
16 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/smart-home-entity-state.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrSmartHomeEntityStateDefinition = CommonOldmarrWidgetDefinition<
4 | "smart-home/entity-state",
5 | {
6 | entityId: string;
7 | appendUnit: boolean;
8 | genericToggle: boolean;
9 | automationId: string;
10 | displayName: string;
11 | displayFriendlyName: boolean;
12 | }
13 | >;
14 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/smart-home-trigger-automation.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrSmartHomeTriggerAutomationDefinition = CommonOldmarrWidgetDefinition<
4 | "smart-home/trigger-automation",
5 | {
6 | automationId: string;
7 | displayName: string;
8 | }
9 | >;
10 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/torrent-status.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrTorrentStatusDefinition = CommonOldmarrWidgetDefinition<
4 | "torrents-status",
5 | {
6 | displayCompletedTorrents: boolean;
7 | displayActiveTorrents: boolean;
8 | speedLimitOfActiveTorrents: number;
9 | displayStaleTorrents: boolean;
10 | labelFilterIsWhitelist: boolean;
11 | labelFilter: string[];
12 | displayRatioWithFilter: boolean;
13 | columnOrdering: boolean;
14 | rowSorting: boolean;
15 | columns: ("up" | "down" | "eta" | "progress")[];
16 | nameColumnSize: number;
17 | }
18 | >;
19 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/usenet.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type
4 | export type OldmarrUsenetDefinition = CommonOldmarrWidgetDefinition<"usenet", {}>;
5 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/video-stream.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrVideoStreamDefinition = CommonOldmarrWidgetDefinition<
4 | "video-stream",
5 | {
6 | FeedUrl: string;
7 | autoPlay: boolean;
8 | muted: boolean;
9 | controls: boolean;
10 | }
11 | >;
12 |
--------------------------------------------------------------------------------
/packages/old-import/src/widgets/definitions/weather.ts:
--------------------------------------------------------------------------------
1 | import type { CommonOldmarrWidgetDefinition } from "./common";
2 |
3 | export type OldmarrWeatherDefinition = CommonOldmarrWidgetDefinition<
4 | "weather",
5 | {
6 | displayInFahrenheit: boolean;
7 | displayCityName: boolean;
8 | displayWeekly: boolean;
9 | forecastDays: number;
10 | location: {
11 | name: string;
12 | latitude: number;
13 | longitude: number;
14 | };
15 | dateFormat:
16 | | "hide"
17 | | "dddd, MMMM D"
18 | | "dddd, D MMMM"
19 | | "MMM D"
20 | | "D MMM"
21 | | "DD/MM/YYYY"
22 | | "MM/DD/YYYY"
23 | | "DD/MM"
24 | | "MM/DD";
25 | }
26 | >;
27 |
--------------------------------------------------------------------------------
/packages/old-import/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/old-schema/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/old-schema/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/old-schema/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { OldmarrConfig, OldmarrCategorySection, OldmarrEmptySection } from "./config";
2 | export { oldmarrConfigSchema } from "./config";
3 | export type { OldmarrApp, OldmarrIntegrationType } from "./app";
4 | export type { OldmarrWidget, OldmarrWidgetKind } from "./widget";
5 | export { oldmarrWidgetKinds } from "./widget";
6 | export { boardSizes, getBoardSizeName } from "./tile";
7 | export type { BoardSize, SizedShape } from "./tile";
8 |
--------------------------------------------------------------------------------
/packages/old-schema/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src", "../old-import/src/widgets/options.ts"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ping/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/ping/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/ping/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "types": ["node"],
5 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
6 | },
7 | "include": ["*.ts", "src"],
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/redis/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/redis/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/redis/redis.conf:
--------------------------------------------------------------------------------
1 | # Directory to store dump.rdb
2 | dir /appdata/redis
3 |
4 | # Save the data to disk every 60 seconds if at least 1 key changed
5 | save 60 1
--------------------------------------------------------------------------------
/packages/redis/src/lib/connection.ts:
--------------------------------------------------------------------------------
1 | import { Redis } from "ioredis";
2 |
3 | /**
4 | * Creates a new Redis connection
5 | * @returns redis client
6 | */
7 | export const createRedisConnection = () => {
8 | if (Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS)) {
9 | // Return null if we are in CI as we don't want to connect to Redis
10 | return null as unknown as Redis;
11 | }
12 |
13 | return new Redis();
14 | };
15 |
--------------------------------------------------------------------------------
/packages/redis/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/request-handler/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/request-handler/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/server-settings/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/server-settings/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/server-settings/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/settings/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/packages/settings/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src/context";
2 |
--------------------------------------------------------------------------------
/packages/settings/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/spotlight/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 | import reactConfig from "@homarr/eslint-config/react";
3 |
4 | /** @type {import('typescript-eslint').Config} */
5 | export default [...baseConfig, ...reactConfig];
6 |
--------------------------------------------------------------------------------
/packages/spotlight/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/spotlight/src/components/actions/items/action-item.module.css:
--------------------------------------------------------------------------------
1 | .spotlightAction:hover {
2 | background-color: alpha(var(--mantine-primary-color-filled), 0.1);
3 | }
4 |
5 | .spotlightAction[data-selected="true"] {
6 | background-color: alpha(var(--mantine-primary-color-filled), 0.3);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/spotlight/src/components/no-results.tsx:
--------------------------------------------------------------------------------
1 | import { Spotlight } from "@mantine/spotlight";
2 |
3 | import { useI18n } from "@homarr/translation/client";
4 |
5 | export const SpotlightNoResults = () => {
6 | const t = useI18n();
7 |
8 | return {t("search.nothingFound")};
9 | };
10 |
--------------------------------------------------------------------------------
/packages/spotlight/src/index.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { spotlightActions } from "./spotlight-store";
4 |
5 | export { Spotlight } from "./components/spotlight";
6 | export { openSpotlight };
7 | export {
8 | SpotlightProvider,
9 | useRegisterSpotlightContextResults,
10 | useRegisterSpotlightContextActions,
11 | } from "./modes/home/context";
12 |
13 | const openSpotlight = spotlightActions.open;
14 |
--------------------------------------------------------------------------------
/packages/spotlight/src/lib/mode.ts:
--------------------------------------------------------------------------------
1 | import type { TranslationObject } from "@homarr/translation";
2 |
3 | import type { SearchGroup } from "./group";
4 |
5 | export type SearchMode = {
6 | modeKey: keyof TranslationObject["search"]["mode"];
7 | character: string | undefined;
8 | } & (
9 | | {
10 | groups: SearchGroup[];
11 | }
12 | | {
13 | useGroups: () => SearchGroup[];
14 | }
15 | );
16 |
--------------------------------------------------------------------------------
/packages/spotlight/src/modes/command/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SearchMode } from "../../lib/mode";
2 | import { contextSpecificActionsSearchGroups } from "./context-specific-group";
3 | import { globalCommandGroup } from "./global-group";
4 |
5 | export const commandMode = {
6 | modeKey: "command",
7 | character: ">",
8 | groups: [contextSpecificActionsSearchGroups, globalCommandGroup],
9 | } satisfies SearchMode;
10 |
--------------------------------------------------------------------------------
/packages/spotlight/src/modes/external/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SearchMode } from "../../lib/mode";
2 | import { searchEnginesSearchGroups } from "./search-engines-search-group";
3 |
4 | export const externalMode = {
5 | modeKey: "external",
6 | character: "!",
7 | groups: [searchEnginesSearchGroups],
8 | } satisfies SearchMode;
9 |
--------------------------------------------------------------------------------
/packages/spotlight/src/modes/home/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SearchMode } from "../../lib/mode";
2 | import { contextSpecificSearchGroups } from "./context-specific-group";
3 | import { homeSearchEngineGroup } from "./home-search-engine-group";
4 |
5 | export const homeMode = {
6 | character: undefined,
7 | modeKey: "home",
8 | groups: [homeSearchEngineGroup, contextSpecificSearchGroups],
9 | } satisfies SearchMode;
10 |
--------------------------------------------------------------------------------
/packages/spotlight/src/modes/page/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SearchMode } from "../../lib/mode";
2 | import { pagesSearchGroup } from "./pages-search-group";
3 |
4 | export const pageMode = {
5 | modeKey: "page",
6 | character: "/",
7 | groups: [pagesSearchGroup],
8 | } satisfies SearchMode;
9 |
--------------------------------------------------------------------------------
/packages/spotlight/src/modes/user-group/index.tsx:
--------------------------------------------------------------------------------
1 | import type { SearchMode } from "../../lib/mode";
2 | import { groupsSearchGroup } from "./groups-search-group";
3 | import { usersSearchGroup } from "./users-search-group";
4 |
5 | export const userGroupMode = {
6 | modeKey: "userGroup",
7 | character: "@",
8 | groups: [usersSearchGroup, groupsSearchGroup],
9 | } satisfies SearchMode;
10 |
--------------------------------------------------------------------------------
/packages/spotlight/src/styles.css:
--------------------------------------------------------------------------------
1 | @import "@mantine/spotlight/styles.css";
2 |
--------------------------------------------------------------------------------
/packages/spotlight/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/translation/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/translation/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/translation/src/client/use-current-locale.ts:
--------------------------------------------------------------------------------
1 | import { useLocale } from "next-intl";
2 |
3 | export const useCurrentLocale = useLocale;
4 |
--------------------------------------------------------------------------------
/packages/translation/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import createMiddleware from "next-intl/middleware";
2 |
3 | import type { SupportedLanguage } from ".";
4 | import { supportedLanguages } from "./config";
5 | import { createRouting } from "./routing";
6 |
7 | export const createI18nMiddleware = (defaultLocale: SupportedLanguage) =>
8 | createMiddleware(createRouting(defaultLocale));
9 |
10 | export const config = {
11 | // Match only internationalized pathnames
12 | matcher: ["/", `/(${supportedLanguages.join("|")})/:path*`],
13 | };
14 |
--------------------------------------------------------------------------------
/packages/translation/src/routing.ts:
--------------------------------------------------------------------------------
1 | import { defineRouting } from "next-intl/routing";
2 |
3 | import { localeCookieKey } from "@homarr/definitions";
4 |
5 | import type { SupportedLanguage } from "./config";
6 | import { supportedLanguages } from "./config";
7 |
8 | export const createRouting = (defaultLocale: SupportedLanguage) =>
9 | defineRouting({
10 | locales: supportedLanguages,
11 | defaultLocale,
12 | localeCookie: {
13 | name: localeCookieKey,
14 | // 1 year
15 | maxAge: 60 * 60 * 24 * 365,
16 | },
17 | localePrefix: {
18 | mode: "never", // Rewrite the URL with locale parameter but without shown in url
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/packages/translation/src/server.ts:
--------------------------------------------------------------------------------
1 | import { getTranslations } from "next-intl/server";
2 |
3 | import type { SupportedLanguage } from "./config";
4 | import type englishTranslation from "./lang/en.json";
5 |
6 | declare module "next-intl" {
7 | interface AppConfig {
8 | Messages: typeof englishTranslation;
9 | Locale: SupportedLanguage;
10 | }
11 | }
12 |
13 | export const { getI18n, getScopedI18n } = {
14 | getI18n: getTranslations,
15 | getScopedI18n: getTranslations,
16 | };
17 |
18 | export { getMessages as getI18nMessages } from "next-intl/server";
19 |
--------------------------------------------------------------------------------
/packages/translation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 | import reactConfig from "@homarr/eslint-config/react";
3 |
4 | /** @type {import('typescript-eslint').Config} */
5 | export default [...baseConfig, ...reactConfig];
6 |
--------------------------------------------------------------------------------
/packages/ui/index.ts:
--------------------------------------------------------------------------------
1 | import type { Icon123, IconProps } from "@tabler/icons-react";
2 |
3 | export * from "./src";
4 |
5 | export type TablerIcon = typeof Icon123;
6 | export type TablerIconProps = IconProps;
7 |
--------------------------------------------------------------------------------
/packages/ui/src/components/beta-badge.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { BadgeProps } from "@mantine/core";
4 | import { Badge } from "@mantine/core";
5 |
6 | import { useTranslations } from "@homarr/translation/client";
7 |
8 | interface BetaBadgeProps {
9 | size: BadgeProps["size"];
10 | }
11 |
12 | export const BetaBadge = ({ size }: BetaBadgeProps) => {
13 | const t = useTranslations();
14 | return (
15 |
16 | {t("common.beta")}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/packages/ui/src/components/count-badge.module.css:
--------------------------------------------------------------------------------
1 | .badge {
2 | @mixin light {
3 | --badge-bg: rgba(30, 34, 39, 0.08);
4 | --badge-color: var(--mantine-color-black);
5 | }
6 |
7 | @mixin dark {
8 | --badge-bg: #363c44;
9 | --badge-color: var(--mantine-color-white);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/ui/src/components/count-badge.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@mantine/core";
2 |
3 | import classes from "./count-badge.module.css";
4 |
5 | interface CountBadgeProps {
6 | count: number;
7 | }
8 |
9 | export const CountBadge = ({ count }: CountBadgeProps) => {
10 | return {count};
11 | };
12 |
--------------------------------------------------------------------------------
/packages/ui/src/components/masked-image.module.css:
--------------------------------------------------------------------------------
1 | .maskedImage {
2 | background-color: var(--image-color);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/ui/src/components/password-input/password-requirement.tsx:
--------------------------------------------------------------------------------
1 | import { rem, Text } from "@mantine/core";
2 | import { IconCheck, IconX } from "@tabler/icons-react";
3 |
4 | export function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
5 | return (
6 |
7 | {meets ? (
8 |
9 | ) : (
10 |
11 | )}
12 |
13 | {label}
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/packages/ui/src/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import type { AvatarProps } from "@mantine/core";
2 | import { Avatar } from "@mantine/core";
3 |
4 | export interface UserProps {
5 | name: string | null;
6 | image: string | null;
7 | }
8 |
9 | interface UserAvatarProps {
10 | user: UserProps | null;
11 | size: AvatarProps["size"];
12 | }
13 |
14 | export const UserAvatar = ({ user, size }: UserAvatarProps) => {
15 | if (!user?.name) return ;
16 | if (user.image) {
17 | return ;
18 | }
19 |
20 | return ;
21 | };
22 |
--------------------------------------------------------------------------------
/packages/ui/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./use-translated-mantine-react-table";
2 |
--------------------------------------------------------------------------------
/packages/ui/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { MantineProviderProps } from "@mantine/core";
2 |
3 | import { theme } from "./theme";
4 |
5 | export * from "./components";
6 |
7 | export const uiConfiguration = {
8 | theme,
9 | } satisfies MantineProviderProps;
10 |
--------------------------------------------------------------------------------
/packages/ui/src/styles.css:
--------------------------------------------------------------------------------
1 | @import "@mantine/core/styles.css";
2 | @import "@mantine/dates/styles.css";
3 | @import "mantine-react-table/styles.css";
4 |
--------------------------------------------------------------------------------
/packages/ui/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { createTheme } from "@mantine/core";
2 |
3 | import { primaryColor } from "./theme/colors/primary";
4 | import { secondaryColor } from "./theme/colors/secondary";
5 |
6 | export const theme = createTheme({
7 | colors: {
8 | primaryColor,
9 | secondaryColor,
10 | },
11 | primaryColor: "primaryColor",
12 | });
13 |
--------------------------------------------------------------------------------
/packages/ui/src/theme/colors/primary.ts:
--------------------------------------------------------------------------------
1 | import type { MantineColorsTuple } from "@mantine/core";
2 |
3 | export const primaryColor: MantineColorsTuple = [
4 | "#eafbf0",
5 | "#ddefe3",
6 | "#bedcc7",
7 | "#9bc8aa",
8 | "#7eb892",
9 | "#6bad81",
10 | "#60a878",
11 | "#509265",
12 | "#438359",
13 | "#35724a",
14 | ];
15 |
--------------------------------------------------------------------------------
/packages/ui/src/theme/colors/secondary.ts:
--------------------------------------------------------------------------------
1 | import type { MantineColorsTuple } from "@mantine/core";
2 |
3 | export const secondaryColor: MantineColorsTuple = [
4 | "#e6f7ff",
5 | "#d9e8f6",
6 | "#b6cde6",
7 | "#90b2d4",
8 | "#6f9ac5",
9 | "#5a8bbd",
10 | "#4d84ba",
11 | "#3d71a4",
12 | "#326595",
13 | "#205885",
14 | ];
15 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/validation/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/packages/validation/src/common.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const paginatedSchema = z.object({
4 | search: z.string().optional(),
5 | pageSize: z.number().int().positive().default(10),
6 | page: z.number().int().positive().default(1),
7 | });
8 |
9 | export const byIdSchema = z.object({
10 | id: z.string(),
11 | });
12 |
13 | export const searchSchema = z.object({
14 | query: z.string(),
15 | limit: z.number().int().positive().default(10),
16 | });
17 |
--------------------------------------------------------------------------------
/packages/validation/src/enums.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | type CouldBeReadonlyArray = T[] | readonly T[];
4 |
5 | export const zodEnumFromArray = (array: CouldBeReadonlyArray) =>
6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
7 | z.enum([array[0]!, ...array.slice(1)]);
8 |
9 | export const zodUnionFromArray = (array: CouldBeReadonlyArray) =>
10 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
11 | z.union([array[0]!, array[1]!, ...array.slice(2)]);
12 |
--------------------------------------------------------------------------------
/packages/validation/src/icons.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const iconsFindSchema = z.object({
4 | searchText: z.string().optional(),
5 | limitPerGroup: z.number().min(1).max(500).default(12),
6 | });
7 |
--------------------------------------------------------------------------------
/packages/validation/src/permissions.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const createSavePermissionsSchema = >(
4 | permissionSchema: TPermissionSchema,
5 | ) => {
6 | return z.object({
7 | entityId: z.string(),
8 | permissions: z.array(
9 | z.object({
10 | principalId: z.string(),
11 | permission: permissionSchema,
12 | }),
13 | ),
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/packages/validation/src/settings.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const settingsInitSchema = z.object({
4 | analytics: z.object({
5 | enableGeneral: z.boolean(),
6 | enableWidgetData: z.boolean(),
7 | enableIntegrationData: z.boolean(),
8 | enableUserData: z.boolean(),
9 | }),
10 | crawlingAndIndexing: z.object({
11 | noIndex: z.boolean(),
12 | noFollow: z.boolean(),
13 | noTranslate: z.boolean(),
14 | noSiteLinksSearchBox: z.boolean(),
15 | }),
16 | });
17 |
--------------------------------------------------------------------------------
/packages/validation/src/widgets/media-request.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const mediaRequestOptionsSchema = z.object({
4 | mediaId: z.number(),
5 | mediaType: z.enum(["tv", "movie"]),
6 | });
7 |
8 | export const mediaRequestRequestSchema = z.object({
9 | mediaType: z.enum(["tv", "movie"]),
10 | mediaId: z.number(),
11 | seasons: z.array(z.number().min(0)).optional(),
12 | });
13 |
--------------------------------------------------------------------------------
/packages/validation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/widgets/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 | import reactConfig from "@homarr/eslint-config/react";
3 |
4 | /** @type {import('typescript-eslint').Config} */
5 | export default [...baseConfig, ...reactConfig];
6 |
--------------------------------------------------------------------------------
/packages/widgets/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./src";
2 |
--------------------------------------------------------------------------------
/packages/widgets/src/_inputs/form.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { createFormContext } from "@homarr/form";
4 |
5 | import type { WidgetEditModalState } from "../modals/widget-edit-modal";
6 |
7 | export const [FormProvider, useFormContext, useForm] =
8 | createFormContext>();
9 |
--------------------------------------------------------------------------------
/packages/widgets/src/_inputs/widget-text-input.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { TextInput } from "@mantine/core";
4 |
5 | import type { CommonWidgetInputProps } from "./common";
6 | import { useWidgetInputTranslation } from "./common";
7 | import { useFormContext } from "./form";
8 |
9 | export const WidgetTextInput = ({ property, kind, options }: CommonWidgetInputProps<"text">) => {
10 | const t = useWidgetInputTranslation(kind, property);
11 | const form = useFormContext();
12 |
13 | return (
14 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/packages/widgets/src/app/app.module.css:
--------------------------------------------------------------------------------
1 | .appIcon {
2 | max-height: 100%;
3 | max-width: 100%;
4 | overflow: auto;
5 | flex: 1 !important;
6 | object-fit: contain;
7 | scale: 0.8;
8 | transition: scale 0.2s ease-in-out;
9 | }
10 |
11 | .appWithUrl:hover > .appIcon {
12 | scale: 0.9;
13 | }
14 |
15 | .appWithUrl:hover > div.appIcon {
16 | background-color: var(--mantine-color-iconColor-filled-hover);
17 | }
18 |
--------------------------------------------------------------------------------
/packages/widgets/src/bookmarks/bookmark.module.css:
--------------------------------------------------------------------------------
1 | .card:hover {
2 | background-color: var(--mantine-color-primaryColor-light-hover);
3 | }
4 |
5 | [data-mantine-color-scheme="light"] .card-grid {
6 | background-color: var(--mantine-color-gray-1);
7 | }
8 |
9 | [data-mantine-color-scheme="dark"] .card-grid {
10 | background-color: var(--mantine-color-dark-7);
11 | }
12 |
13 | .card:hover > div > div.bookmarkIcon {
14 | background-color: var(--mantine-color-iconColor-filled-hover);
15 | }
16 |
--------------------------------------------------------------------------------
/packages/widgets/src/calendar/calendar-event-list.module.css:
--------------------------------------------------------------------------------
1 | .badge {
2 | transform: translateX(-50%);
3 | }
4 |
--------------------------------------------------------------------------------
/packages/widgets/src/calendar/component.module.css:
--------------------------------------------------------------------------------
1 | .calendar div[data-month-level] {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
--------------------------------------------------------------------------------
/packages/widgets/src/dns-hole/controls/component.module.css:
--------------------------------------------------------------------------------
1 | [data-mantine-color-scheme="light"] .card {
2 | background-color: var(--mantine-color-gray-1);
3 | }
4 |
5 | [data-mantine-color-scheme="dark"] .card {
6 | background-color: var(--mantine-color-dark-7);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/widgets/src/docker/index.ts:
--------------------------------------------------------------------------------
1 | import { IconBrandDocker, IconServerOff } from "@tabler/icons-react";
2 |
3 | import { createWidgetDefinition } from "../definition";
4 | import { optionsBuilder } from "../options";
5 |
6 | export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
7 | icon: IconBrandDocker,
8 | createOptions() {
9 | return optionsBuilder.from(() => ({}));
10 | },
11 | errors: {
12 | INTERNAL_SERVER_ERROR: {
13 | icon: IconServerOff,
14 | message: (t) => t("widget.dockerContainers.error.internalServerError"),
15 | },
16 | },
17 | }).withDynamicImport(() => import("./component"));
18 |
--------------------------------------------------------------------------------
/packages/widgets/src/errors/base.ts:
--------------------------------------------------------------------------------
1 | import type { TablerIcon } from "@tabler/icons-react";
2 |
3 | import type { stringOrTranslation } from "@homarr/translation";
4 |
5 | export abstract class ErrorBoundaryError extends Error {
6 | public abstract getErrorBoundaryData(): {
7 | icon: TablerIcon;
8 | message: stringOrTranslation;
9 | showLogsLink: boolean;
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/packages/widgets/src/errors/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./no-integration-selected";
2 | export * from "./base";
3 |
--------------------------------------------------------------------------------
/packages/widgets/src/errors/no-data-integration.tsx:
--------------------------------------------------------------------------------
1 | import { IconDatabaseOff } from "@tabler/icons-react";
2 |
3 | import type { TranslationFunction } from "@homarr/translation";
4 |
5 | import { ErrorBoundaryError } from "./base";
6 |
7 | export class NoIntegrationDataError extends ErrorBoundaryError {
8 | constructor() {
9 | super("No integration data available");
10 | }
11 |
12 | public getErrorBoundaryData() {
13 | return {
14 | icon: IconDatabaseOff,
15 | message: (t: TranslationFunction) => t("widget.common.error.noData"),
16 | showLogsLink: false,
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/widgets/src/errors/no-integration-selected.tsx:
--------------------------------------------------------------------------------
1 | import { IconPlugX } from "@tabler/icons-react";
2 |
3 | import type { TranslationFunction } from "@homarr/translation";
4 |
5 | import { ErrorBoundaryError } from "./base";
6 |
7 | export class NoIntegrationSelectedError extends ErrorBoundaryError {
8 | constructor() {
9 | super("No integration selected");
10 | }
11 |
12 | public getErrorBoundaryData() {
13 | return {
14 | icon: IconPlugX,
15 | message: (t: TranslationFunction) => t("widget.common.error.noIntegration"),
16 | showLogsLink: false,
17 | };
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/widgets/src/health-monitoring/system-health.module.css:
--------------------------------------------------------------------------------
1 | [data-mantine-color-scheme="light"] .card {
2 | background-color: var(--mantine-color-gray-1);
3 | }
4 |
5 | [data-mantine-color-scheme="dark"] .card {
6 | background-color: var(--mantine-color-dark-7);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/widgets/src/iframe/component.module.css:
--------------------------------------------------------------------------------
1 | .iframe {
2 | border-radius: var(--mantine-radius-sm);
3 | width: 100%;
4 | height: 100%;
5 | border: none;
6 | background: none;
7 | background-color: transparent;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/widgets/src/import.ts:
--------------------------------------------------------------------------------
1 | import type { WidgetKind } from "@homarr/definitions";
2 |
3 | export type WidgetImportRecord = Record;
4 |
--------------------------------------------------------------------------------
/packages/widgets/src/indexer-manager/component.module.css:
--------------------------------------------------------------------------------
1 | [data-mantine-color-scheme="light"] .card {
2 | background-color: var(--mantine-color-gray-1);
3 | }
4 |
5 | [data-mantine-color-scheme="dark"] .card {
6 | background-color: var(--mantine-color-dark-7);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/widgets/src/media-requests/stats/component.module.css:
--------------------------------------------------------------------------------
1 | [data-mantine-color-scheme="light"] .card {
2 | background-color: var(--mantine-color-gray-1);
3 | }
4 |
5 | [data-mantine-color-scheme="dark"] .card {
6 | background-color: var(--mantine-color-dark-7);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/widgets/src/media-requests/stats/index.ts:
--------------------------------------------------------------------------------
1 | import { IconChartBar } from "@tabler/icons-react";
2 |
3 | import { getIntegrationKindsByCategory } from "@homarr/definitions";
4 |
5 | import { createWidgetDefinition } from "../../definition";
6 |
7 | export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", {
8 | icon: IconChartBar,
9 | createOptions() {
10 | return {};
11 | },
12 | supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"),
13 | }).withDynamicImport(() => import("./component"));
14 |
--------------------------------------------------------------------------------
/packages/widgets/src/media-server/index.ts:
--------------------------------------------------------------------------------
1 | import { IconVideo } from "@tabler/icons-react";
2 |
3 | import { createWidgetDefinition } from "../definition";
4 | import { optionsBuilder } from "../options";
5 |
6 | export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
7 | icon: IconVideo,
8 | createOptions() {
9 | return optionsBuilder.from((factory) => ({
10 | showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
11 | }));
12 | },
13 | supportedIntegrations: ["jellyfin", "plex", "emby"],
14 | }).withDynamicImport(() => import("./component"));
15 |
--------------------------------------------------------------------------------
/packages/widgets/src/modals/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./widget-edit-modal";
2 |
--------------------------------------------------------------------------------
/packages/widgets/src/network-controller/network-status/variants/stat-row.tsx:
--------------------------------------------------------------------------------
1 | import { Stack, Text } from "@mantine/core";
2 |
3 | export const StatRow = ({ label, value }: { label: string; value: string | number }) => {
4 | return (
5 |
6 |
7 | {value}
8 |
9 |
10 | {label}
11 |
12 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/packages/widgets/src/notebook/component.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import dynamic from "next/dynamic";
4 |
5 | import type { WidgetComponentProps } from "../definition";
6 |
7 | const Notebook = dynamic(() => import("./notebook").then((module) => module.Notebook), {
8 | ssr: false,
9 | });
10 |
11 | export default function NotebookWidget(props: WidgetComponentProps<"notebook">) {
12 | return ;
13 | }
14 |
--------------------------------------------------------------------------------
/packages/widgets/src/rssFeed/component.module.scss:
--------------------------------------------------------------------------------
1 | .backgroundImage {
2 | position: absolute;
3 | width: 100%;
4 | height: 100%;
5 | filter: blur(5px);
6 | transform: scaleX(-1.05) scaleZ(-1.05);
7 | opacity: 0.25;
8 | top: 0;
9 | left: 0;
10 | object-fit: cover;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/widgets/src/video/component.module.css:
--------------------------------------------------------------------------------
1 | .video {
2 | height: 100%;
3 | width: 100%;
4 | border-radius: var(--mantine-radius-md);
5 | overflow: hidden;
6 | }
7 |
--------------------------------------------------------------------------------
/packages/widgets/src/widget-integration-select.module.css:
--------------------------------------------------------------------------------
1 | .pill {
2 | cursor: default;
3 | background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
4 | border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-7));
5 | padding-left: var(--mantine-spacing-xs);
6 | border-radius: var(--mantine-radius-xl);
7 | }
8 |
--------------------------------------------------------------------------------
/packages/widgets/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["*.ts", "src", "*.tsx"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - apps/*
3 | - packages/*
4 | - tooling/*
5 |
--------------------------------------------------------------------------------
/tooling/eslint/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [...baseConfig];
5 |
--------------------------------------------------------------------------------
/tooling/eslint/nextjs.js:
--------------------------------------------------------------------------------
1 | import nextPlugin from "@next/eslint-plugin-next";
2 |
3 | /** @type {Awaited} */
4 | export default [
5 | {
6 | files: ["**/*.ts", "**/*.tsx"],
7 | plugins: {
8 | "@next/next": nextPlugin,
9 | },
10 | rules: {
11 | ...nextPlugin.configs.recommended.rules,
12 | ...nextPlugin.configs["core-web-vitals"].rules,
13 | // TypeError: context.getAncestors is not a function
14 | "@next/next/no-duplicate-head": "off",
15 | },
16 | },
17 | ];
18 |
--------------------------------------------------------------------------------
/tooling/eslint/react.js:
--------------------------------------------------------------------------------
1 | import reactPlugin from "eslint-plugin-react";
2 | import hooksPlugin from "eslint-plugin-react-hooks";
3 |
4 | /** @type {Awaited} */
5 | export default [
6 | {
7 | files: ["**/*.ts", "**/*.tsx"],
8 | plugins: {
9 | react: reactPlugin,
10 | "react-hooks": hooksPlugin,
11 | },
12 | rules: {
13 | ...reactPlugin.configs["jsx-runtime"].rules,
14 | ...hooksPlugin.configs.recommended.rules,
15 | },
16 | languageOptions: {
17 | globals: {
18 | React: "writable",
19 | },
20 | },
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/tooling/eslint/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/tooling/github/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@homarr/github"
3 | }
4 |
--------------------------------------------------------------------------------
/tooling/github/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: "Setup and install"
2 | description: "Common setup steps for Actions"
3 |
4 | runs:
5 | using: composite
6 | steps:
7 | - uses: pnpm/action-setup@v4
8 | - uses: actions/setup-node@v4
9 | with:
10 | node-version: 22
11 | cache: "pnpm"
12 |
13 | - shell: bash
14 | run: pnpm add -g turbo
15 |
16 | - shell: bash
17 | run: pnpm install
18 |
--------------------------------------------------------------------------------
/tooling/prettier/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@homarr/prettier-config",
3 | "version": "0.1.0",
4 | "private": true,
5 | "main": "index.mjs",
6 | "scripts": {
7 | "clean": "rm -rf .turbo node_modules",
8 | "format": "prettier --check . --ignore-path ../../.gitignore",
9 | "typecheck": "tsc --noEmit"
10 | },
11 | "prettier": "@homarr/prettier-config",
12 | "dependencies": {
13 | "@ianvs/prettier-plugin-sort-imports": "^4.4.2",
14 | "prettier": "^3.5.3"
15 | },
16 | "devDependencies": {
17 | "@homarr/tsconfig": "workspace:^0.1.0",
18 | "prettier-plugin-packagejson": "^2.5.15",
19 | "typescript": "^5.8.3"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tooling/prettier/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["."],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/tooling/typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@homarr/tsconfig",
3 | "private": true,
4 | "version": "0.1.0",
5 | "files": [
6 | "base.json"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": ["e2e"],
7 | "exclude": ["node_modules"]
8 | }
9 |
--------------------------------------------------------------------------------
/turbo/generators/templates/eslint.config.js.hbs:
--------------------------------------------------------------------------------
1 | import baseConfig from "@homarr/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [],
7 | },
8 | ...baseConfig,
9 | ];
--------------------------------------------------------------------------------
/turbo/generators/templates/tsconfig.json.hbs:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@homarr/tsconfig/base.json",
3 | "compilerOptions": {
4 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
5 | },
6 | "include": [
7 | "*.ts",
8 | "src"
9 | ],
10 | "exclude": [
11 | "node_modules"
12 | ]
13 | }
--------------------------------------------------------------------------------
/vitest.setup.ts:
--------------------------------------------------------------------------------
1 | import { vi } from "vitest";
2 |
3 | vi.mock("server-only", () => ({ default: undefined }));
4 |
--------------------------------------------------------------------------------