Showing preview only (4,265K chars total). Download the full file or copy to clipboard to get everything.
Repository: getumbrel/umbrel
Branch: master
Commit: 3e4ad453ad63
Files: 901
Total size: 3.9 MB
Directory structure:
gitextract_9b_v3mll/
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── update-translations-in-pr.yml
├── .gitignore
├── .prettierrc.js
├── .umbrel
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── containers/
│ ├── app-auth/
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── www
│ │ ├── middleware/
│ │ │ ├── handle_error.js
│ │ │ └── validate_token.js
│ │ ├── package.json
│ │ ├── routes/
│ │ │ └── auth.js
│ │ ├── test/
│ │ │ ├── docker-compose.yml
│ │ │ ├── fixtures/
│ │ │ │ └── app-data/
│ │ │ │ └── mempool/
│ │ │ │ └── umbrel-app.yml
│ │ │ ├── global.js
│ │ │ ├── test.sh
│ │ │ └── utils/
│ │ │ └── hmac.js
│ │ ├── utils/
│ │ │ ├── app.js
│ │ │ ├── const.js
│ │ │ ├── dashboard.js
│ │ │ ├── express.js
│ │ │ ├── hmac.js
│ │ │ ├── host_resolution.js
│ │ │ ├── manager.js
│ │ │ ├── safe_handler.js
│ │ │ └── token.js
│ │ └── views/
│ │ └── pages/
│ │ └── redirect.ejs
│ ├── app-proxy/
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── Dockerfile.dev
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── www
│ │ ├── middleware/
│ │ │ └── handle_error.js
│ │ ├── package.json
│ │ ├── routes/
│ │ │ └── umbrel.js
│ │ ├── test/
│ │ │ ├── .gitignore
│ │ │ ├── docker-compose.app1.yml
│ │ │ ├── docker-compose.app2.yml
│ │ │ ├── docker-compose.bleskomat.yml
│ │ │ ├── docker-compose.error.yml
│ │ │ ├── docker-compose.mempool.yml
│ │ │ ├── docker-compose.nextcloud.yml
│ │ │ ├── docker-compose.proxy.yml
│ │ │ ├── docker-compose.proxyhttps.yaml
│ │ │ ├── docker-compose.sse.yml
│ │ │ ├── docker-compose.suredbits.yml
│ │ │ ├── docker-compose.ws.yml
│ │ │ ├── docker-compose.yml
│ │ │ ├── fixtures/
│ │ │ │ └── mempool-umbrel-app.yml
│ │ │ ├── global.js
│ │ │ ├── sse-test-server/
│ │ │ │ ├── .dockerignore
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── bin/
│ │ │ │ │ └── www
│ │ │ │ └── package.json
│ │ │ ├── test/
│ │ │ │ └── Caddyfile-https
│ │ │ ├── test.sh
│ │ │ └── utils/
│ │ │ ├── express.js
│ │ │ └── tor.js
│ │ ├── utils/
│ │ │ ├── const.js
│ │ │ ├── express.js
│ │ │ ├── hmac.js
│ │ │ ├── manager.js
│ │ │ ├── proxy.js
│ │ │ ├── safe_handler.js
│ │ │ ├── token.js
│ │ │ └── tor.js
│ │ └── views/
│ │ └── pages/
│ │ └── error.ejs
│ └── tor/
│ ├── Dockerfile
│ ├── README.md
│ └── test/
│ ├── .gitignore
│ ├── docker-compose.entrypoint.yml
│ ├── docker-compose.yml
│ ├── entrypoint.sh
│ ├── test-entrypoint.sh
│ ├── test.sh
│ └── torrc
├── info.json
├── package.json
├── packages/
│ ├── os/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build-steps/
│ │ │ ├── initialize.sh
│ │ │ ├── setup-raspberrypi/
│ │ │ │ ├── cmdline.txt
│ │ │ │ ├── config.txt
│ │ │ │ ├── raspberrypi.gpg.key
│ │ │ │ └── raspberrypi.list
│ │ │ └── setup-raspberrypi.sh
│ │ ├── build.sh
│ │ ├── builder.Dockerfile
│ │ ├── mender.cfg
│ │ ├── overlay-amd64/
│ │ │ └── umbrelOS
│ │ ├── overlay-arm64/
│ │ │ ├── etc/
│ │ │ │ └── systemd/
│ │ │ │ └── system/
│ │ │ │ └── umbrel-external-storage.service
│ │ │ └── opt/
│ │ │ └── umbrel-external-storage/
│ │ │ └── umbrel-external-storage
│ │ ├── overlay-common/
│ │ │ ├── etc/
│ │ │ │ ├── NetworkManager/
│ │ │ │ │ ├── NetworkManager.conf
│ │ │ │ │ └── conf.d/
│ │ │ │ │ └── 10-cloudflaredns.conf
│ │ │ │ ├── acpi/
│ │ │ │ │ ├── events/
│ │ │ │ │ │ └── power-button
│ │ │ │ │ └── power-button.sh
│ │ │ │ ├── fstab
│ │ │ │ ├── hostname
│ │ │ │ ├── hosts
│ │ │ │ ├── issue
│ │ │ │ ├── locale.conf
│ │ │ │ ├── motd
│ │ │ │ ├── sudoers.d/
│ │ │ │ │ └── umbrel
│ │ │ │ ├── sudoers.lecture
│ │ │ │ ├── sysctl.d/
│ │ │ │ │ └── 99-vm-zram-parameters.conf
│ │ │ │ └── systemd/
│ │ │ │ ├── logind.conf.d/
│ │ │ │ │ ├── lid-switch.conf
│ │ │ │ │ └── power-button.conf
│ │ │ │ ├── system/
│ │ │ │ │ ├── umbrel-dns-sync.service
│ │ │ │ │ ├── umbrel-ssh-host-key-hydration.service
│ │ │ │ │ ├── umbrel-tty-message.service
│ │ │ │ │ └── umbrel.service
│ │ │ │ ├── timesyncd.conf.d/
│ │ │ │ │ └── cloudflare.conf
│ │ │ │ └── zram-generator.conf
│ │ │ ├── opt/
│ │ │ │ ├── umbrel-data/
│ │ │ │ │ └── umbrel-data-mount
│ │ │ │ ├── umbrel-dns-sync/
│ │ │ │ │ └── umbrel-dns-sync
│ │ │ │ ├── umbrel-ssh-host-key-hydration/
│ │ │ │ │ └── umbrel-ssh-host-key-hydration
│ │ │ │ └── umbrel-tty-message/
│ │ │ │ └── umbrel-tty-message
│ │ │ └── umbrelOS
│ │ ├── package.json
│ │ ├── rugix/
│ │ │ ├── .gitignore
│ │ │ ├── fix-umbrelos-pi-mbr.sh
│ │ │ ├── layers/
│ │ │ │ ├── umbrelos-amd64.toml
│ │ │ │ ├── umbrelos-mender-amd64.toml
│ │ │ │ ├── umbrelos-pi.toml
│ │ │ │ ├── umbrelos-pi4.toml
│ │ │ │ ├── umbrelos-root-amd64.toml
│ │ │ │ └── umbrelos-root-arm64.toml
│ │ │ ├── recipes/
│ │ │ │ ├── fix-overlay/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── .gitignore
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── setup-rugix/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── bootstrapping-amd64.toml
│ │ │ │ │ │ ├── bootstrapping-arm64.toml
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ └── state-reset/
│ │ │ │ │ │ │ └── prepare.sh
│ │ │ │ │ │ ├── state-data.toml
│ │ │ │ │ │ └── system.toml
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── setup-rugix-mender/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── init
│ │ │ │ │ │ ├── migrate-state.sh
│ │ │ │ │ │ └── system.toml
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── umbrelos-boot/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ └── grub.cfg
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── umbrelos-cleanup/
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ └── umbrelos-prepare/
│ │ │ │ ├── recipe.toml
│ │ │ │ └── steps/
│ │ │ │ └── 00-install.sh
│ │ │ ├── rugix-bakery.toml
│ │ │ └── run-bakery
│ │ ├── rugpi-image
│ │ ├── trigger-change
│ │ ├── umbrelos.Dockerfile
│ │ ├── usb-installer/
│ │ │ ├── .gitignore
│ │ │ ├── build.sh
│ │ │ ├── builder.Dockerfile
│ │ │ ├── overlay/
│ │ │ │ ├── etc/
│ │ │ │ │ └── systemd/
│ │ │ │ │ └── system/
│ │ │ │ │ └── custom-tty.service
│ │ │ │ └── opt/
│ │ │ │ └── custom-tty
│ │ │ ├── run.sh
│ │ │ └── usb-installer.Dockerfile
│ │ └── vm.sh
│ ├── ui/
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── .prettierignore
│ │ ├── .prettierrc.js
│ │ ├── Dockerfile
│ │ ├── app-auth/
│ │ │ ├── README.md
│ │ │ ├── index.html
│ │ │ ├── src/
│ │ │ │ ├── login-with-umbrel.tsx
│ │ │ │ └── main.tsx
│ │ │ └── vite.config.ts
│ │ ├── components.json
│ │ ├── eslint.config.js
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── assets/
│ │ │ │ ├── onboarding/
│ │ │ │ │ └── onboarding-bg.webm
│ │ │ │ └── whats-new/
│ │ │ │ ├── backups.webm
│ │ │ │ ├── external-storage.webm
│ │ │ │ ├── network-devices.webm
│ │ │ │ ├── restore.webm
│ │ │ │ └── rewind.webm
│ │ │ ├── locales/
│ │ │ │ ├── de.json
│ │ │ │ ├── en.json
│ │ │ │ ├── es.json
│ │ │ │ ├── fr.json
│ │ │ │ ├── hu.json
│ │ │ │ ├── it.json
│ │ │ │ ├── ja.json
│ │ │ │ ├── ko.json
│ │ │ │ ├── nl.json
│ │ │ │ ├── pt.json
│ │ │ │ ├── tr.json
│ │ │ │ └── uk.json
│ │ │ └── site.webmanifest
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── app-icon.tsx
│ │ │ │ ├── caret-right.tsx
│ │ │ │ ├── chevron-down.tsx
│ │ │ │ ├── cmdk-providers.tsx
│ │ │ │ ├── cmdk.tsx
│ │ │ │ ├── darken-layer.tsx
│ │ │ │ ├── fade-scroller.tsx
│ │ │ │ ├── iframe-checker.tsx
│ │ │ │ ├── install-button-connected.tsx
│ │ │ │ ├── install-button.tsx
│ │ │ │ ├── markdown.tsx
│ │ │ │ ├── onboarding-background.tsx
│ │ │ │ ├── progress-button.tsx
│ │ │ │ ├── ui/
│ │ │ │ │ ├── alert-dialog.tsx
│ │ │ │ │ ├── alert.tsx
│ │ │ │ │ ├── animated-number.tsx
│ │ │ │ │ ├── arc.tsx
│ │ │ │ │ ├── badge.tsx
│ │ │ │ │ ├── button-link.tsx
│ │ │ │ │ ├── button.tsx
│ │ │ │ │ ├── card.tsx
│ │ │ │ │ ├── carousel.tsx
│ │ │ │ │ ├── checkbox.tsx
│ │ │ │ │ ├── command.tsx
│ │ │ │ │ ├── context-menu.tsx
│ │ │ │ │ ├── copy-button.tsx
│ │ │ │ │ ├── copyable-field.tsx
│ │ │ │ │ ├── cover-message.tsx
│ │ │ │ │ ├── debug-only.tsx
│ │ │ │ │ ├── dialog-close-button.tsx
│ │ │ │ │ ├── dialog.tsx
│ │ │ │ │ ├── drawer.tsx
│ │ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ │ ├── error-boundary-card-fallback.tsx
│ │ │ │ │ ├── error-boundary-page-fallback.tsx
│ │ │ │ │ ├── fade-in-img.tsx
│ │ │ │ │ ├── form.tsx
│ │ │ │ │ ├── generic-error-text.tsx
│ │ │ │ │ ├── icon-button-link.tsx
│ │ │ │ │ ├── icon-button.tsx
│ │ │ │ │ ├── icon.tsx
│ │ │ │ │ ├── immersive-dialog.tsx
│ │ │ │ │ ├── input.tsx
│ │ │ │ │ ├── label.tsx
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── loading.tsx
│ │ │ │ │ ├── notification-badge.tsx
│ │ │ │ │ ├── numbered-list.tsx
│ │ │ │ │ ├── pagination.tsx
│ │ │ │ │ ├── pin-input.tsx
│ │ │ │ │ ├── popover.tsx
│ │ │ │ │ ├── progress.tsx
│ │ │ │ │ ├── radio-group.tsx
│ │ │ │ │ ├── root-error-fallback.tsx
│ │ │ │ │ ├── scroll-area.tsx
│ │ │ │ │ ├── segmented-control.tsx
│ │ │ │ │ ├── separator.tsx
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── dialog.ts
│ │ │ │ │ │ └── menu.ts
│ │ │ │ │ ├── sheet-scroll-area.tsx
│ │ │ │ │ ├── sheet.tsx
│ │ │ │ │ ├── switch.tsx
│ │ │ │ │ ├── table.tsx
│ │ │ │ │ ├── tabs.tsx
│ │ │ │ │ ├── toast.tsx
│ │ │ │ │ └── tooltip.tsx
│ │ │ │ ├── umbrel-logo.tsx
│ │ │ │ └── widget-check-icon.tsx
│ │ │ ├── constants/
│ │ │ │ ├── index.ts
│ │ │ │ └── links.ts
│ │ │ ├── features/
│ │ │ │ ├── backups/
│ │ │ │ │ ├── cmdk-search-provider.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── backup-device-icon.tsx
│ │ │ │ │ │ ├── backup-location-dropdown.tsx
│ │ │ │ │ │ ├── backups-exclusions.tsx
│ │ │ │ │ │ ├── configure-wizard.tsx
│ │ │ │ │ │ ├── floating-island/
│ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ ├── modals/
│ │ │ │ │ │ │ ├── already-configured-modal.tsx
│ │ │ │ │ │ │ └── connect-existing-modal.tsx
│ │ │ │ │ │ ├── restore-location-dropdown.tsx
│ │ │ │ │ │ ├── restore-wizard.tsx
│ │ │ │ │ │ ├── review-card.tsx
│ │ │ │ │ │ ├── setup-wizard.tsx
│ │ │ │ │ │ ├── tab-switcher.tsx
│ │ │ │ │ │ └── tiles.tsx
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── use-apps-auto-excluded-paths.ts
│ │ │ │ │ │ ├── use-apps-backup-ignore.ts
│ │ │ │ │ │ ├── use-backup-ignored-paths.ts
│ │ │ │ │ │ ├── use-backups.ts
│ │ │ │ │ │ └── use-existing-backup-detection.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── backup-location-helpers.ts
│ │ │ │ │ ├── error-messages.ts
│ │ │ │ │ ├── filepath-helpers.ts
│ │ │ │ │ └── sort.ts
│ │ │ │ ├── files/
│ │ │ │ │ ├── assets/
│ │ │ │ │ │ ├── add-folder-icon.tsx
│ │ │ │ │ │ ├── apps-icon.tsx
│ │ │ │ │ │ ├── caret-right.tsx
│ │ │ │ │ │ ├── chevron-left.tsx
│ │ │ │ │ │ ├── chevron-right.tsx
│ │ │ │ │ │ ├── copy-icon.tsx
│ │ │ │ │ │ ├── cursor-text-icon.tsx
│ │ │ │ │ │ ├── empty-folder-icon.tsx
│ │ │ │ │ │ ├── file-items-thumbnails/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── flame-icon.tsx
│ │ │ │ │ │ ├── grid-layout-icon.tsx
│ │ │ │ │ │ ├── home-icon.tsx
│ │ │ │ │ │ ├── list-layout-icon.tsx
│ │ │ │ │ │ ├── recents-icon.tsx
│ │ │ │ │ │ ├── rewind-icon.tsx
│ │ │ │ │ │ ├── search-icon.tsx
│ │ │ │ │ │ ├── shared-folder-badge.tsx
│ │ │ │ │ │ └── trash-icon.tsx
│ │ │ │ │ ├── cmdk-search-provider.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── cards/
│ │ │ │ │ │ │ └── server-cards.tsx
│ │ │ │ │ │ ├── dialogs/
│ │ │ │ │ │ │ ├── add-network-share-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── external-storage-unsupported-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── format-drive-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── permanently-delete-confirmation-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── share-info-dialog/
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── platform-instructions/
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ ├── inline-copyable-field.tsx
│ │ │ │ │ │ │ │ ├── instruction.tsx
│ │ │ │ │ │ │ │ ├── ios-instructions.tsx
│ │ │ │ │ │ │ │ ├── macos-instructions.tsx
│ │ │ │ │ │ │ │ ├── umbrelos-instructions.tsx
│ │ │ │ │ │ │ │ └── windows-instructions.tsx
│ │ │ │ │ │ │ ├── platform-selector.tsx
│ │ │ │ │ │ │ └── share-toggle.tsx
│ │ │ │ │ │ ├── embedded/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── file-viewer/
│ │ │ │ │ │ │ ├── audio-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── downloader/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── image-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── pdf-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── video-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── viewer-wrapper.tsx
│ │ │ │ │ │ ├── files-dnd-wrapper/
│ │ │ │ │ │ │ ├── files-dnd-overlay.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── floating-islands/
│ │ │ │ │ │ │ ├── audio-island/
│ │ │ │ │ │ │ │ ├── equalizer.tsx
│ │ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ │ ├── formatting-island/
│ │ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ │ ├── operations-island/
│ │ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ │ └── uploading-island/
│ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ ├── listing/
│ │ │ │ │ │ │ ├── actions-bar/
│ │ │ │ │ │ │ │ ├── actions-bar-context.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ ├── mobile-actions.tsx
│ │ │ │ │ │ │ │ ├── navigation-controls.tsx
│ │ │ │ │ │ │ │ ├── path-bar/
│ │ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ │ ├── path-bar-desktop.tsx
│ │ │ │ │ │ │ │ │ ├── path-bar-mobile.tsx
│ │ │ │ │ │ │ │ │ └── path-input.tsx
│ │ │ │ │ │ │ │ ├── search-input.tsx
│ │ │ │ │ │ │ │ ├── sort-dropdown.tsx
│ │ │ │ │ │ │ │ └── view-toggle.tsx
│ │ │ │ │ │ │ ├── apps-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── directory-listing/
│ │ │ │ │ │ │ │ ├── empty-state.tsx
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── file-item/
│ │ │ │ │ │ │ │ ├── circular-progress.tsx
│ │ │ │ │ │ │ │ ├── editable-name.tsx
│ │ │ │ │ │ │ │ ├── icons-view-file-item.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ ├── list-view-file-item.css
│ │ │ │ │ │ │ │ ├── list-view-file-item.tsx
│ │ │ │ │ │ │ │ └── truncated-filename.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── listing-and-file-item-context-menu.tsx
│ │ │ │ │ │ │ ├── listing-body.tsx
│ │ │ │ │ │ │ ├── marquee-selection.tsx
│ │ │ │ │ │ │ ├── recents-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── search-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── trash-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── virtualized-list.tsx
│ │ │ │ │ │ ├── mini-browser/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── rewind/
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── overlay-context.tsx
│ │ │ │ │ │ │ ├── prerewind-dialog.tsx
│ │ │ │ │ │ │ ├── restore-grouping.ts
│ │ │ │ │ │ │ ├── restore-progress-dialog.tsx
│ │ │ │ │ │ │ ├── snapshot-carousel.tsx
│ │ │ │ │ │ │ ├── snapshot-date-label.ts
│ │ │ │ │ │ │ ├── timeline-bar.tsx
│ │ │ │ │ │ │ └── tooltip.tsx
│ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ ├── circular-progress.tsx
│ │ │ │ │ │ │ ├── drag-and-drop.tsx
│ │ │ │ │ │ │ ├── file-item-icon/
│ │ │ │ │ │ │ │ ├── animated-folder-icon.tsx
│ │ │ │ │ │ │ │ ├── embedded-overlay-icons.tsx
│ │ │ │ │ │ │ │ ├── folder-icon.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── unknown-file-thumbnail.tsx
│ │ │ │ │ │ │ ├── file-upload-drop-zone.tsx
│ │ │ │ │ │ │ └── upload-input.tsx
│ │ │ │ │ │ └── sidebar/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── mobile-sidebar-wrapper.tsx
│ │ │ │ │ │ ├── sidebar-apps.tsx
│ │ │ │ │ │ ├── sidebar-external-storage-item.tsx
│ │ │ │ │ │ ├── sidebar-external-storage.tsx
│ │ │ │ │ │ ├── sidebar-favorites.tsx
│ │ │ │ │ │ ├── sidebar-home.tsx
│ │ │ │ │ │ ├── sidebar-item.tsx
│ │ │ │ │ │ ├── sidebar-network-share-item.tsx
│ │ │ │ │ │ ├── sidebar-network-storage.tsx
│ │ │ │ │ │ ├── sidebar-recents.tsx
│ │ │ │ │ │ ├── sidebar-shares.tsx
│ │ │ │ │ │ └── sidebar-trash.tsx
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── use-drag-and-drop.ts
│ │ │ │ │ │ ├── use-external-storage.ts
│ │ │ │ │ │ ├── use-favorites.ts
│ │ │ │ │ │ ├── use-files-keyboard-shortcuts.ts
│ │ │ │ │ │ ├── use-files-operations.ts
│ │ │ │ │ │ ├── use-home-directory-name.ts
│ │ │ │ │ │ ├── use-is-touch-device.ts
│ │ │ │ │ │ ├── use-item-click.ts
│ │ │ │ │ │ ├── use-list-directory.ts
│ │ │ │ │ │ ├── use-list-recents.ts
│ │ │ │ │ │ ├── use-navigate.ts
│ │ │ │ │ │ ├── use-network-device-type.ts
│ │ │ │ │ │ ├── use-network-storage.ts
│ │ │ │ │ │ ├── use-new-folder.ts
│ │ │ │ │ │ ├── use-preferences.ts
│ │ │ │ │ │ ├── use-rewind-action.ts
│ │ │ │ │ │ ├── use-rewind.ts
│ │ │ │ │ │ ├── use-search-files.ts
│ │ │ │ │ │ └── use-shares.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── providers/
│ │ │ │ │ │ └── files-capabilities-context.tsx
│ │ │ │ │ ├── routes.tsx
│ │ │ │ │ ├── store/
│ │ │ │ │ │ ├── slices/
│ │ │ │ │ │ │ ├── clipboard-slice.ts
│ │ │ │ │ │ │ ├── drag-and-drop-slice.ts
│ │ │ │ │ │ │ ├── file-viewer-slice.ts
│ │ │ │ │ │ │ ├── interaction-slice.ts
│ │ │ │ │ │ │ ├── new-folder-slice.ts
│ │ │ │ │ │ │ ├── rename-slice.ts
│ │ │ │ │ │ │ └── selection-slice.ts
│ │ │ │ │ │ └── use-files-store.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ ├── error-messages.ts
│ │ │ │ │ │ ├── format-filesystem-date.ts
│ │ │ │ │ │ ├── format-filesystem-name.ts
│ │ │ │ │ │ ├── format-filesystem-size.ts
│ │ │ │ │ │ ├── get-grid-column-count.ts
│ │ │ │ │ │ ├── get-item-key.ts
│ │ │ │ │ │ ├── is-directory-a-network-device-or-share.ts
│ │ │ │ │ │ ├── is-directory-an-external-drive-partition.ts
│ │ │ │ │ │ ├── is-directory-an-umbrel-backup.ts
│ │ │ │ │ │ ├── path-alias.ts
│ │ │ │ │ │ └── sort-filesystem-items.ts
│ │ │ │ │ └── widgets.tsx
│ │ │ │ └── storage/
│ │ │ │ ├── components/
│ │ │ │ │ ├── dialogs/
│ │ │ │ │ │ ├── add-to-raid-dialog.tsx
│ │ │ │ │ │ ├── install-ssd-dialog.tsx
│ │ │ │ │ │ ├── install-tips-collapsible.tsx
│ │ │ │ │ │ ├── operation-in-progress-banner.tsx
│ │ │ │ │ │ ├── replace-failed-drive-dialog.tsx
│ │ │ │ │ │ ├── shutdown-confirmation-dialog.tsx
│ │ │ │ │ │ ├── ssd-health-dialog.tsx
│ │ │ │ │ │ └── swap-dialog.tsx
│ │ │ │ │ ├── floating-island/
│ │ │ │ │ │ ├── data-stream-icon.tsx
│ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ ├── ssd-shape.tsx
│ │ │ │ │ ├── storage-donut-chart.tsx
│ │ │ │ │ └── storage-mode-display.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-active-raid-operation.ts
│ │ │ │ │ ├── use-raid-progress.ts
│ │ │ │ │ └── use-storage.ts
│ │ │ │ ├── index.tsx
│ │ │ │ ├── providers/
│ │ │ │ │ └── pending-operation-context.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── hooks/
│ │ │ │ ├── use-2fa.ts
│ │ │ │ ├── use-app-install.ts
│ │ │ │ ├── use-apps-with-updates.ts
│ │ │ │ ├── use-auto-height-animation.tsx
│ │ │ │ ├── use-color-thief.ts
│ │ │ │ ├── use-cpu-temperature.ts
│ │ │ │ ├── use-cpu.ts
│ │ │ │ ├── use-debug-install-random-apps.ts
│ │ │ │ ├── use-device-info.ts
│ │ │ │ ├── use-disk.ts
│ │ │ │ ├── use-is-externaldns.ts
│ │ │ │ ├── use-is-home-or-pro.ts
│ │ │ │ ├── use-is-mobile.ts
│ │ │ │ ├── use-is-umbrel-home.tsx
│ │ │ │ ├── use-is-umbrel-pro.ts
│ │ │ │ ├── use-language.ts
│ │ │ │ ├── use-launch-app.ts
│ │ │ │ ├── use-memory.ts
│ │ │ │ ├── use-notifications.ts
│ │ │ │ ├── use-password.ts
│ │ │ │ ├── use-prefixed-local-storage.ts
│ │ │ │ ├── use-query-params.ts
│ │ │ │ ├── use-scroll-restoration.ts
│ │ │ │ ├── use-settings-notification-count.ts
│ │ │ │ ├── use-software-update.ts
│ │ │ │ ├── use-temperature-unit.ts
│ │ │ │ ├── use-tor-enabled.ts
│ │ │ │ ├── use-update-all-apps.ts
│ │ │ │ ├── use-user-name.ts
│ │ │ │ ├── use-version.ts
│ │ │ │ └── use-widgets.ts
│ │ │ ├── index.css
│ │ │ ├── init.tsx
│ │ │ ├── layouts/
│ │ │ │ ├── README.md
│ │ │ │ ├── app-store.tsx
│ │ │ │ ├── bare/
│ │ │ │ │ ├── bare-page.tsx
│ │ │ │ │ ├── bare.tsx
│ │ │ │ │ ├── onboarding-page.tsx
│ │ │ │ │ ├── onboarding.tsx
│ │ │ │ │ └── shared.tsx
│ │ │ │ ├── desktop.tsx
│ │ │ │ └── sheet.tsx
│ │ │ ├── lib/
│ │ │ │ └── utils.ts
│ │ │ ├── main.tsx
│ │ │ ├── modules/
│ │ │ │ ├── app-store/
│ │ │ │ │ ├── app-page/
│ │ │ │ │ │ ├── about-section.tsx
│ │ │ │ │ │ ├── app-content.tsx
│ │ │ │ │ │ ├── app-settings-dialog.tsx
│ │ │ │ │ │ ├── default-credentials-dialog.tsx
│ │ │ │ │ │ ├── dependencies.tsx
│ │ │ │ │ │ ├── get-recommendations.ts
│ │ │ │ │ │ ├── info-section.tsx
│ │ │ │ │ │ ├── recommendations-section.tsx
│ │ │ │ │ │ ├── release-notes-section.tsx
│ │ │ │ │ │ ├── settings-section.tsx
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ └── top-header.tsx
│ │ │ │ │ ├── app-store-nav.tsx
│ │ │ │ │ ├── community-app-store-dialog.tsx
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── discover/
│ │ │ │ │ │ ├── apps-grid-section.tsx
│ │ │ │ │ │ ├── apps-row-section.tsx
│ │ │ │ │ │ └── apps-three-column-section.tsx
│ │ │ │ │ ├── gallery-section.tsx
│ │ │ │ │ ├── os-update-required.tsx
│ │ │ │ │ ├── select-dependencies-dialog.tsx
│ │ │ │ │ ├── shared.tsx
│ │ │ │ │ ├── updates-button.tsx
│ │ │ │ │ ├── updates-dialog.tsx
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── auth/
│ │ │ │ │ ├── ensure-backend-available.tsx
│ │ │ │ │ ├── ensure-logged-in.tsx
│ │ │ │ │ ├── ensure-no-raid-mount-failure.tsx
│ │ │ │ │ ├── ensure-pro-device.tsx
│ │ │ │ │ ├── ensure-user-exists.tsx
│ │ │ │ │ ├── redirects.tsx
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ └── use-auth.tsx
│ │ │ │ ├── bare/
│ │ │ │ │ ├── alert.tsx
│ │ │ │ │ ├── failed-layout.tsx
│ │ │ │ │ ├── progress-layout.tsx
│ │ │ │ │ ├── progress.tsx
│ │ │ │ │ ├── shared.tsx
│ │ │ │ │ └── success-layout.tsx
│ │ │ │ ├── community-app-store/
│ │ │ │ │ └── community-badge.tsx
│ │ │ │ ├── desktop/
│ │ │ │ │ ├── app-grid/
│ │ │ │ │ │ ├── app-grid.tsx
│ │ │ │ │ │ ├── app-pagination-utils.tsx
│ │ │ │ │ │ └── paginator.tsx
│ │ │ │ │ ├── app-icon.tsx
│ │ │ │ │ ├── blur-below-dock.tsx
│ │ │ │ │ ├── desktop-content.tsx
│ │ │ │ │ ├── desktop-context-menu.tsx
│ │ │ │ │ ├── desktop-misc.tsx
│ │ │ │ │ ├── desktop-preview.tsx
│ │ │ │ │ ├── dock-item.tsx
│ │ │ │ │ ├── dock.tsx
│ │ │ │ │ ├── greeting-message.ts
│ │ │ │ │ ├── header.tsx
│ │ │ │ │ ├── install-first-app.tsx
│ │ │ │ │ ├── logout-dialog.tsx
│ │ │ │ │ ├── uninstall-confirmation-dialog.tsx
│ │ │ │ │ └── uninstall-these-first-dialog.tsx
│ │ │ │ ├── floating-island/
│ │ │ │ │ ├── bare-island.tsx
│ │ │ │ │ └── container.tsx
│ │ │ │ ├── immersive-picker/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── migrate/
│ │ │ │ │ ├── migrate-image.tsx
│ │ │ │ │ └── migrate-inner.tsx
│ │ │ │ ├── sheet-top-fixed.tsx
│ │ │ │ ├── widgets/
│ │ │ │ │ ├── four-stats-widget.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── list-emoji-widget.tsx
│ │ │ │ │ ├── list-widget.tsx
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── backdrop-blur-context.tsx
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ ├── stat-text.tsx
│ │ │ │ │ │ ├── tabler-icon.tsx
│ │ │ │ │ │ └── widget-wrapper.tsx
│ │ │ │ │ ├── text-with-buttons-widget.tsx
│ │ │ │ │ ├── text-with-progress-widget.tsx
│ │ │ │ │ ├── three-stats-widget.tsx
│ │ │ │ │ └── two-stats-with-guage-widget.tsx
│ │ │ │ └── wifi/
│ │ │ │ ├── desktop-wifi-button-connected.tsx
│ │ │ │ ├── icon.tsx
│ │ │ │ ├── wifi-drawer-or-dialog.tsx
│ │ │ │ ├── wifi-item-content.tsx
│ │ │ │ └── wifi-list-row-connected-description.tsx
│ │ │ ├── providers/
│ │ │ │ ├── apps.tsx
│ │ │ │ ├── auth-bootstrap.tsx
│ │ │ │ ├── available-apps.tsx
│ │ │ │ ├── confirmation/
│ │ │ │ │ ├── confirmation-context.tsx
│ │ │ │ │ ├── confirmation-provider.tsx
│ │ │ │ │ ├── generic-confirmation-dialog.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── use-confirmation.ts
│ │ │ │ ├── global-files.tsx
│ │ │ │ ├── global-system-state/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── migrate.tsx
│ │ │ │ │ ├── reset.tsx
│ │ │ │ │ ├── restart.tsx
│ │ │ │ │ ├── restore.tsx
│ │ │ │ │ ├── shutdown.tsx
│ │ │ │ │ └── update.tsx
│ │ │ │ ├── immersive-dialog.tsx
│ │ │ │ ├── language.tsx
│ │ │ │ ├── prefetch.tsx
│ │ │ │ ├── sheet-sticky-header.tsx
│ │ │ │ └── wallpaper.tsx
│ │ │ ├── router.tsx
│ │ │ ├── routes/
│ │ │ │ ├── README.md
│ │ │ │ ├── app-store/
│ │ │ │ │ ├── app-page/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── category-page.tsx
│ │ │ │ │ ├── discover.tsx
│ │ │ │ │ └── use-discover-query.tsx
│ │ │ │ ├── community-app-store/
│ │ │ │ │ ├── app-page/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── edit-widgets/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── widget-selector.tsx
│ │ │ │ ├── factory-reset/
│ │ │ │ │ ├── _components/
│ │ │ │ │ │ ├── confirm-with-password.tsx
│ │ │ │ │ │ ├── misc.tsx
│ │ │ │ │ │ └── review-data.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── live-usage.tsx
│ │ │ │ ├── login.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ ├── notifications.tsx
│ │ │ │ ├── onboarding/
│ │ │ │ │ ├── account-created.tsx
│ │ │ │ │ ├── create-account.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── onboarding-footer.tsx
│ │ │ │ │ ├── raid/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── raid-error.tsx
│ │ │ │ │ │ ├── setup.tsx
│ │ │ │ │ │ ├── ssd-health-dialog.tsx
│ │ │ │ │ │ ├── ssd-tray.tsx
│ │ │ │ │ │ └── use-raid-setup.ts
│ │ │ │ │ ├── restore.tsx
│ │ │ │ │ └── use-onboarding-device.ts
│ │ │ │ ├── raid-error/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── settings/
│ │ │ │ │ ├── 2fa-disable.tsx
│ │ │ │ │ ├── 2fa-enable.tsx
│ │ │ │ │ ├── 2fa.tsx
│ │ │ │ │ ├── _components/
│ │ │ │ │ │ ├── app-store-preferences-content.tsx
│ │ │ │ │ │ ├── cpu-card-content.tsx
│ │ │ │ │ │ ├── cpu-temperature-card-content.tsx
│ │ │ │ │ │ ├── device-info-content.tsx
│ │ │ │ │ │ ├── device-info-umbrel-home.tsx
│ │ │ │ │ │ ├── device-info-umbrel-pro.tsx
│ │ │ │ │ │ ├── language-dropdown.tsx
│ │ │ │ │ │ ├── laser-engraving.tsx
│ │ │ │ │ │ ├── list-row.tsx
│ │ │ │ │ │ ├── memory-card-content.tsx
│ │ │ │ │ │ ├── no-forgot-password-message.tsx
│ │ │ │ │ │ ├── progress-card-content.tsx
│ │ │ │ │ │ ├── settings-content-mobile.tsx
│ │ │ │ │ │ ├── settings-content.tsx
│ │ │ │ │ │ ├── settings-summary.tsx
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ ├── software-update-list-row.tsx
│ │ │ │ │ │ ├── storage-card-content.tsx
│ │ │ │ │ │ └── wallpaper-picker.tsx
│ │ │ │ │ ├── advanced.tsx
│ │ │ │ │ ├── app-store-preferences.tsx
│ │ │ │ │ ├── change-name.tsx
│ │ │ │ │ ├── change-password.tsx
│ │ │ │ │ ├── device-info.tsx
│ │ │ │ │ ├── file-sharing.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── migration-assistant.tsx
│ │ │ │ │ ├── mobile/
│ │ │ │ │ │ ├── account.tsx
│ │ │ │ │ │ ├── app-store-preferences.tsx
│ │ │ │ │ │ ├── backups-mobile-drawer.tsx
│ │ │ │ │ │ ├── device-info.tsx
│ │ │ │ │ │ ├── language.tsx
│ │ │ │ │ │ ├── software-update.tsx
│ │ │ │ │ │ ├── start-migration-drawer-or-dialog.tsx
│ │ │ │ │ │ ├── tor.tsx
│ │ │ │ │ │ └── wallpaper.tsx
│ │ │ │ │ ├── restart.tsx
│ │ │ │ │ ├── shutdown.tsx
│ │ │ │ │ ├── software-update-confirm.tsx
│ │ │ │ │ ├── terminal/
│ │ │ │ │ │ ├── _shared.tsx
│ │ │ │ │ │ ├── app.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── umbrelos.tsx
│ │ │ │ │ ├── troubleshoot/
│ │ │ │ │ │ ├── _shared.tsx
│ │ │ │ │ │ ├── app.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── umbrelos.tsx
│ │ │ │ │ ├── wifi-unsupported.tsx
│ │ │ │ │ └── wifi.tsx
│ │ │ │ └── whats-new-modal.tsx
│ │ │ ├── trpc/
│ │ │ │ ├── loading-indicator.tsx
│ │ │ │ ├── trpc-provider.tsx
│ │ │ │ └── trpc.ts
│ │ │ ├── types.d.ts
│ │ │ └── utils/
│ │ │ ├── call-every-interval.ts
│ │ │ ├── date-time.ts
│ │ │ ├── dialog.ts
│ │ │ ├── element-classes.ts
│ │ │ ├── i18n.ts
│ │ │ ├── language.ts
│ │ │ ├── logs.ts
│ │ │ ├── misc.ts
│ │ │ ├── number.ts
│ │ │ ├── pretty-bytes.ts
│ │ │ ├── search.ts
│ │ │ ├── seconds-to-eta.ts
│ │ │ ├── system.ts
│ │ │ ├── temperature.ts
│ │ │ ├── tw.ts
│ │ │ └── wifi.ts
│ │ ├── tailwind.config.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ ├── update-translations.js
│ │ └── vite.config.ts
│ └── umbreld/
│ ├── .gitignore
│ ├── .prettierignore
│ ├── package.json
│ ├── scripts/
│ │ └── validate-manifests.ts
│ ├── source/
│ │ ├── cli.ts
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── apps/
│ │ │ ├── app-repository.integration.test.ts
│ │ │ ├── app-repository.ts
│ │ │ ├── app-store.integration.test.ts
│ │ │ ├── app-store.ts
│ │ │ ├── app.ts
│ │ │ ├── apps.integration.test.ts
│ │ │ ├── apps.ts
│ │ │ ├── legacy-compat/
│ │ │ │ ├── app-environment.ts
│ │ │ │ ├── app-script
│ │ │ │ ├── app-script.ts
│ │ │ │ ├── bin/
│ │ │ │ │ ├── bitcoin-cli
│ │ │ │ │ └── lncli
│ │ │ │ ├── docker-compose.app_proxy.yml
│ │ │ │ ├── docker-compose.common.yml
│ │ │ │ ├── docker-compose.tor.yml
│ │ │ │ ├── docker-compose.yml
│ │ │ │ ├── tor-entrypoint.sh
│ │ │ │ ├── tor-proxy-torrc
│ │ │ │ └── tor-server-torrc
│ │ │ ├── routes.ts
│ │ │ └── schema.ts
│ │ ├── backups/
│ │ │ ├── backups.backupProgress.test.ts
│ │ │ ├── backups.integration.test.ts
│ │ │ ├── backups.ts
│ │ │ └── routes.ts
│ │ ├── blacklist-uas/
│ │ │ └── blacklist-uas.ts
│ │ ├── cli-client.ts
│ │ ├── dbus/
│ │ │ └── dbus.ts
│ │ ├── development.ts
│ │ ├── event-bus/
│ │ │ ├── event-bus.ts
│ │ │ └── routes.ts
│ │ ├── files/
│ │ │ ├── api.download.integration.test.ts
│ │ │ ├── api.thumbnail.integration.test.ts
│ │ │ ├── api.ts
│ │ │ ├── api.upload.integration.test.ts
│ │ │ ├── api.view.integration.test.ts
│ │ │ ├── archive.integration.test.ts
│ │ │ ├── archive.ts
│ │ │ ├── external-storage.integration.test.ts
│ │ │ ├── external-storage.ts
│ │ │ ├── favorites.integration.test.ts
│ │ │ ├── favorites.ts
│ │ │ ├── files-reflink-copy.vm.test.ts
│ │ │ ├── files.copy.integration.test.ts
│ │ │ ├── files.createDirectory.integration.test.ts
│ │ │ ├── files.delete.test.ts
│ │ │ ├── files.emptyTrash.test.ts
│ │ │ ├── files.list.integration.test.ts
│ │ │ ├── files.move.integration.test.ts
│ │ │ ├── files.operationProgress.test.ts
│ │ │ ├── files.preferences.integration.test.ts
│ │ │ ├── files.rename.integration.test.ts
│ │ │ ├── files.restore.test.ts
│ │ │ ├── files.trash.test.ts
│ │ │ ├── files.ts
│ │ │ ├── fixtures/
│ │ │ │ └── thumbnails/
│ │ │ │ └── master-lossless-video.mkv
│ │ │ ├── network-storage.integration.test.ts
│ │ │ ├── network-storage.ts
│ │ │ ├── recents.test.ts
│ │ │ ├── recents.ts
│ │ │ ├── routes.ts
│ │ │ ├── samba.integration.test.ts
│ │ │ ├── samba.ts
│ │ │ ├── search.integration.test.ts
│ │ │ ├── search.ts
│ │ │ ├── thumbnails.integration.test.ts
│ │ │ ├── thumbnails.ts
│ │ │ ├── watcher.ts
│ │ │ └── widgets.ts
│ │ ├── hardware/
│ │ │ ├── hardware.ts
│ │ │ ├── internal-storage-rounding.vm.test.ts
│ │ │ ├── internal-storage-slot-detection.vm.test.ts
│ │ │ ├── internal-storage.ts
│ │ │ ├── raid-failsafe-space-reporting.vm.test.ts
│ │ │ ├── raid-failsafe.vm.test.ts
│ │ │ ├── raid-foreign-pool.vm.test.ts
│ │ │ ├── raid-operations-different-sizes.vm.test.ts
│ │ │ ├── raid-preused-drives.vm.test.ts
│ │ │ ├── raid-recovery-mode.vm.test.ts
│ │ │ ├── raid-replace-larger-capacity.vm.test.ts
│ │ │ ├── raid-replace.vm.test.ts
│ │ │ ├── raid-size-rounding.vm.test.ts
│ │ │ ├── raid-slot-swap.vm.test.ts
│ │ │ ├── raid-storage.vm.test.ts
│ │ │ ├── raid-transition-different-size.vm.test.ts
│ │ │ ├── raid-transition-full-storage.vm.test.ts
│ │ │ ├── raid-transition.vm.test.ts
│ │ │ ├── raid.getRoundedDeviceSize.unit.test.ts
│ │ │ ├── raid.ts
│ │ │ ├── routes.ts
│ │ │ └── umbrel-pro.ts
│ │ ├── is-umbrel-home.ts
│ │ ├── jwt.ts
│ │ ├── migration/
│ │ │ ├── migration.ts
│ │ │ └── routes.ts
│ │ ├── notifications/
│ │ │ ├── notifications.integration.test.ts
│ │ │ ├── notifications.ts
│ │ │ └── routes.ts
│ │ ├── server/
│ │ │ ├── index.ts
│ │ │ ├── terminal-socket.ts
│ │ │ └── trpc/
│ │ │ ├── common.ts
│ │ │ ├── context.ts
│ │ │ ├── index.ts
│ │ │ ├── is-authenticated.ts
│ │ │ ├── trpc.ts
│ │ │ └── websocket-logger.ts
│ │ ├── startup-migrations/
│ │ │ ├── index.ts
│ │ │ └── startup-migrations.integration.test.ts
│ │ ├── system/
│ │ │ ├── factory-reset.ts
│ │ │ ├── routes.ts
│ │ │ ├── system-widgets.ts
│ │ │ ├── system.integration.test.ts
│ │ │ ├── system.ts
│ │ │ ├── system.unit.test.ts
│ │ │ ├── update.ts
│ │ │ └── wifi-routes.ts
│ │ ├── test-utilities/
│ │ │ ├── create-test-umbreld.ts
│ │ │ ├── fixtures/
│ │ │ │ ├── another-community-repo/
│ │ │ │ │ ├── another-sparkles-hello-world/
│ │ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ │ └── umbrel-app.yml
│ │ │ │ │ └── umbrel-app-store.yml
│ │ │ │ └── community-repo/
│ │ │ │ ├── app-with-invalid-id/
│ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ └── umbrel-app.yml
│ │ │ │ ├── app-with-invalid-manifest/
│ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ └── umbrel-app.yml
│ │ │ │ ├── sparkles-hello-world/
│ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ └── umbrel-app.yml
│ │ │ │ └── umbrel-app-store.yml
│ │ │ └── run-git-server.ts
│ │ ├── user/
│ │ │ ├── routes.ts
│ │ │ ├── user.integration.test.ts
│ │ │ └── user.ts
│ │ ├── utilities/
│ │ │ ├── copy-with-progress.ts
│ │ │ ├── dependencies.ts
│ │ │ ├── docker-pull.ts
│ │ │ ├── file-store.integration.test.ts
│ │ │ ├── file-store.ts
│ │ │ ├── get-directory-size.ts
│ │ │ ├── get-or-create-file.ts
│ │ │ ├── logger.ts
│ │ │ ├── package-directory.ts
│ │ │ ├── random-token.ts
│ │ │ ├── regexp.ts
│ │ │ ├── run-every.ts
│ │ │ ├── temporary-directory.ts
│ │ │ └── totp.ts
│ │ └── widgets/
│ │ ├── routes.ts
│ │ └── widget.integration.test.ts
│ ├── trigger-change
│ ├── tsconfig.json
│ └── umbreld
└── scripts/
├── data-export
├── install
├── remote-builder
├── umbrel-dev
└── update-script
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# On Windows, Git defaults to checkout Windows-style and commit Unix-style.
# As such, line endings of bash scripts are converted from LF to CRLF, with
# the effect that when mounting the checkout into a Linux container, the
# bash scripts can't execute because bash does not handle CR. To mitigate,
# conservatively force ALL line endings to be retained.
* -text
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: push
jobs:
# Detect if only UI files changed to skip heavy tests
detect-changes:
name: Detect changes
runs-on: ubuntu-24.04
outputs:
ui-only: ${{ steps.check.outputs.ui-only }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- id: check
run: |
DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
# On default branch, always run all tests
if [[ "${{ github.ref }}" == "refs/heads/${DEFAULT_BRANCH}" ]]; then
echo "ui-only=false" >> $GITHUB_OUTPUT
exit 0
fi
# Get changed files compared to the previous push event
CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} 2>/dev/null || git diff --name-only HEAD~1 HEAD)
# Check if all changed files are in packages/ui/
UI_ONLY="true"
while IFS= read -r file; do
if [[ -n "$file" && ! "$file" =~ ^packages/ui/ ]]; then
UI_ONLY="false"
break
fi
done <<< "$CHANGED_FILES"
echo "Changed files:"
echo "$CHANGED_FILES"
echo "UI only: $UI_ONLY"
echo "ui-only=$UI_ONLY" >> $GITHUB_OUTPUT
# These jobs simply serve as architecture native cache steps to load up the Docker cache for umbrelOS.
# We must cache to unique scopes per architecture otherwise only one of the architectures gets cached.
umbrelos-amd64-cache:
name: Build umbrelOS amd64
needs: detect-changes
if: needs.detect-changes.outputs.ui-only != 'true'
runs-on: ubicloud-standard-4-ubuntu-2404
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v3
- uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3
- name: Build amd64 Docker image
working-directory: packages/os
run: docker buildx build --platform linux/amd64 --file umbrelos.Dockerfile --cache-from type=gha,scope=umbrelos-amd64 --cache-to type=gha,mode=max,scope=umbrelos-amd64 ../../
umbrelos-arm64-cache:
name: Build umbrelOS arm64
needs: detect-changes
if: needs.detect-changes.outputs.ui-only != 'true'
runs-on: ubicloud-standard-4-ubuntu-2404
steps:
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v3
- uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3
- name: Build arm64 Docker image
working-directory: packages/os
run: docker buildx build --platform linux/arm64 --file umbrelos.Dockerfile --cache-from type=gha,scope=umbrelos-arm64 --cache-to type=gha,mode=max,scope=umbrelos-arm64 ../../
# Build the image for the VM tests
# We run this in a seperate step so we only build it once and reuse for all VM runners to reduce billed minutes.
umbrelos-amd64-vm-image-build:
name: Build umbrelOS amd64 VM image
needs: [detect-changes, umbrelos-amd64-cache]
if: needs.detect-changes.outputs.ui-only != 'true'
runs-on: ubicloud-standard-4-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Expose GitHub Actions runtime for Docker cache
uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3
- name: Build OS image
working-directory: packages/os
run: npm run build:amd64:rugix
# Hack to pass the image to the next job. We used to use upload-artifact but network performance
# is terrible between GitHub <> Ubicloud. Just abuse the cache for now which is local and fast.
- name: Cache OS image
uses: actions/cache/save@v4
with:
path: packages/os/build/umbrelos-amd64.img
key: umbrelos-amd64-image-${{ github.run_id }}
# Discover all MV test files so we can run the result as a build matrix
discover-vm-tests:
name: Discover VM tests
needs: detect-changes
if: needs.detect-changes.outputs.ui-only != 'true'
runs-on: ubuntu-24.04
outputs:
tests: ${{ steps.find.outputs.tests }}
steps:
- uses: actions/checkout@v3
- id: find
run: |
tests=$(find packages/umbreld/source -name "*.vm.test.ts" | sed 's|packages/umbreld/||' | jq -R -s -c 'split("\n") | map(select(length > 0)) | map({path: ., name: (. | split("/") | last | sub("\\.vm\\.test\\.ts$"; ""))})')
echo "tests=$tests" >> $GITHUB_OUTPUT
# Run each VM test in a different runner
umbrelos-amd64-vm:
name: VM test (${{ matrix.test.name }})
needs: [detect-changes, umbrelos-amd64-vm-image-build, discover-vm-tests]
if: needs.detect-changes.outputs.ui-only != 'true'
runs-on: ubicloud-standard-4-ubuntu-2404
strategy:
fail-fast: false
matrix:
test: ${{ fromJSON(needs.discover-vm-tests.outputs.tests) }}
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
submodules: recursive
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Restore OS image from cache
uses: actions/cache/restore@v4
with:
path: packages/os/build/umbrelos-amd64.img
key: umbrelos-amd64-image-${{ github.run_id }}
fail-on-cache-miss: true
- name: Install QEMU
run: sudo apt-get update && sudo apt-get install -y qemu-system-x86
- name: Install umbreld dependencies
working-directory: packages/umbreld
run: npm clean-install
- name: Run VM test
working-directory: packages/umbreld
run: npm run test -- ${{ matrix.test.path }}
# Discover all integration test files so we can run the result as a build matrix
discover-integration-tests:
name: Discover integration tests
needs: detect-changes
if: needs.detect-changes.outputs.ui-only != 'true'
runs-on: ubuntu-24.04
outputs:
tests: ${{ steps.find.outputs.tests }}
steps:
- uses: actions/checkout@v3
- id: find
run: |
tests=$(find packages/umbreld/source -name "*.integration.test.ts" | sed 's|packages/umbreld/||' | jq -R -s -c 'split("\n") | map(select(length > 0)) | map({path: ., name: (. | split("/") | last | sub("\\.integration\\.test\\.ts$"; ""))})')
echo "tests=$tests" >> $GITHUB_OUTPUT
# Run each integration test in a different runner
umbreld-integration:
name: Integration test (${{ matrix.test.name }})
needs: [detect-changes, discover-integration-tests, umbrelos-amd64-cache]
if: needs.detect-changes.outputs.ui-only != 'true'
runs-on: ubicloud-standard-4-ubuntu-2404
strategy:
fail-fast: false
matrix:
test: ${{ fromJSON(needs.discover-integration-tests.outputs.tests) }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Expose GitHub Actions runtime for Docker cache
uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3
- run: npm run dev rebuild
- run: npm run dev start
- run: npm run --prefix packages/umbreld test:umbrel-dev:prepare
- run: npm run --prefix packages/umbreld test:umbrel-dev ${{ matrix.test.path }}
# Run umbreld checks
umbreld:
name: umbreld checks
runs-on: ubuntu-24.04
defaults:
run:
working-directory: packages/umbreld
strategy:
fail-fast: false
matrix:
task:
- format:check
- typecheck
- test:unit -- --coverage
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: sudo npm clean-install
- run: sudo npm run ${{ matrix.task }}
# Run ui checks
ui:
name: ui checks
runs-on: ubuntu-24.04
defaults:
run:
working-directory: packages/ui
strategy:
fail-fast: false
matrix:
task:
- lint
- format:check
- typecheck
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 22
- name: Install Umbreld dependencies
if: ${{ matrix.task == 'typecheck' }}
run: npm --prefix ../umbreld clean-install
- name: Install dependencies
run: npm clean-install
- run: npm run ${{ matrix.task }}
# If the current build is tagged, build all release assets, push to Cloudflare R2 and create a GitHub Release
create-release:
name: Create release on tag
if: startsWith(github.ref, 'refs/tags/')
needs: [umbrelos-amd64-cache, umbrelos-arm64-cache]
runs-on: ubicloud-standard-16-ubuntu-2404
defaults:
run:
working-directory: packages/os
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Expose GitHub Actions runtime for Docker cache
uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3
- run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# We need this to namespace Docker images on forks
- run: echo "VERSION_IS_SEMVER=$(if [[ '${{ env.VERSION }}' =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then echo 'true'; else echo 'false'; fi)" >> $GITHUB_ENV
- run: echo "PREFIX=$(if [ '${{ env.VERSION_IS_SEMVER }}' = 'true' ]; then echo ''; else echo $(basename ${{ github.repository }})-; fi)" >> $GITHUB_ENV
- run: echo "TAG=${{ github.repository_owner }}/${{ env.PREFIX }}umbrelos:${{ env.VERSION }}" >> $GITHUB_ENV
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.repository_owner }}" --password-stdin
- name: Build and push Docker image
run: docker buildx build --platform linux/amd64,linux/arm64 --file umbrelos.Dockerfile --tag ghcr.io/${{env.TAG }} --push --cache-from type=gha,scope=umbrelos-amd64 --cache-from type=gha,scope=umbrelos-arm64 ../../
- run: mkdir -p build && docker buildx imagetools inspect ghcr.io/${{ env.TAG }} > build/docker-umbrelos-${{ env.VERSION }}
# Build OS images
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Build OS images
# Awkward hack to run in parallel but correctly handle errors
run: |
npm run build:amd64 "${{ env.VERSION }}" &
pid1=$!
npm run build:arm64 "${{ env.VERSION }}" &
pid2=$!
wait $pid1 || exit 1
wait $pid2 || exit 1
# TODO: Use .img.xz for all release assets once https://github.com/balena-io/etcher/issues/4064 is fixed
- name: Compress release assets
# Awkward hack to run in parallel but correctly handle errors
run: |
cd build
zip umbrelos-pi4.img.zip umbrelos-pi4.img &
pid1=$!
zip umbrelos-pi5.img.zip umbrelos-pi5.img &
pid2=$!
sudo xz --keep --threads=0 umbrelos-amd64.img &
pid3=$!
wait $pid1 || exit 1
wait $pid2 || exit 1
wait $pid3 || exit 1
- name: Create USB installer
run: npm run build:amd64:usb-installer
- name: Create release directory
run: |
mkdir -p release
mv build/docker-umbrelos-* release/
mv build/*.update release/
mv build/*.img.zip release/
mv build/*.img.xz release/
mv build/*.img release/
mv usb-installer/build/*.iso release/
- name: Create SHASUM
run: cd release && shasum -a 256 * | tee SHA256SUMS
- name: OpenTimestamps
run: npm ci && npx ots-cli.js stamp release/SHA256SUMS
- name: Nuke uncompressed images (we just wanted them covered by the SHASUMs)
run: rm -rf release/*.img
- name: Upload to R2
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.R2_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
r2-bucket: ${{ secrets.R2_BUCKET }}
source-dir: packages/os/release
destination-dir: ./${{ env.VERSION }}
- name: Create GitHub Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
with:
draft: true
name: umbrelOS ${{ github.ref_name }}
files: |
packages/os/release/SHA256SUMS*
================================================
FILE: .github/workflows/update-translations-in-pr.yml
================================================
name: Update Translations in PR
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'packages/ui/public/locales/en.json'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
update-translations:
timeout-minutes: 10
permissions:
contents: write
runs-on: ubuntu-22.04
steps:
- name: Check if enabled
run: |
if [[ "${{ secrets.TRANSLATIONS_ACTION_IS_ENABLED }}" != "true" ]]; then
echo "Translation generation is disabled."
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for git comparisons
ref: ${{ github.head_ref }} # Check out the PR's head branch
- name: Fetch base branch
run: |
git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: |
npm install
working-directory: packages/ui
- name: Update translations
env:
CI: 'true'
GITHUB_BASE_REF: ${{ github.base_ref }}
TRANSLATIONS_OPENAI_API_KEY: ${{ secrets.TRANSLATIONS_OPENAI_API_KEY }}
TRANSLATIONS_OPENAI_MODEL: ${{ secrets.TRANSLATIONS_OPENAI_MODEL }}
TRANSLATIONS_SYSTEM_PROMPT: ${{ secrets.TRANSLATIONS_SYSTEM_PROMPT }}
TRANSLATIONS_USER_PROMPT: ${{ secrets.TRANSLATIONS_USER_PROMPT }}
run: |
node update-translations.js
working-directory: packages/ui
- name: Push changes
uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
with:
commit_message: Update translations
================================================
FILE: .gitignore
================================================
# Ignore node_modules anywhere they may be
node_modules
# Ignore all the bash stuff
.bash_history
.bash_logout
.bashrc
.profile
.ssh
.viminfo
.DS_Store
# Python bytecode
__pycache__
*.py[cod]
# umbrel-dev
docker-compose.override.yml
# Files and data directories created by services
# that we shouldn't accidently commit
*.dat
*.log
*.cookie
*.pid
*.env
bitcoin/*
db/*
electrs/*
nginx/*
events/signals/*
lnd/*
logs/*
statuses/*
tor/*
app-data/*
data/
# Commit these files
!statuses/update-status.json
# Commit these empty directories
!db/.gitkeep
!events/signals/.gitkeep
!lnd/.gitkeep
!logs/.gitkeep
!tor/data/.gitkeep
!tor/run/.gitkeep
.umbrel-dev
jwt
./bin
================================================
FILE: .prettierrc.js
================================================
/**
* @type {import('prettier').Config}
*/
export default {
"printWidth": 120,
"semi": false,
"useTabs": true,
"trailingComma": "all",
"singleQuote": true,
"bracketSpacing": false,
"jsxSingleQuote": true,
}
================================================
FILE: .umbrel
================================================
================================================
FILE: CONTRIBUTING.md
================================================
Contributing to Umbrel
======================
Umbrel is an open project and we love to receive contributions from our community! There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into Umbrel itself.
Bug reports
-----------
If you think you have found a bug in Umbrel, first make sure that you are on the [latest version of Umbrel](https://github.com/getumbrel/umbrel/releases/latest) because your issue may already have been fixed. If not, search our [issues list](https://github.com/getumbrel/umbrel/issues) on GitHub in case a similar issue has already been opened.
If there's no existing issue, please open a new issue and provide us as much information about the bug as you can. It would be very helpful if you can list down the steps for us to reproduce the bug, because the easier it is for us to recreate the bug, the faster it is likely to be fixed.
Feature requests
----------------
If you find yourself wishing for a feature that doesn't exist in Umbrel, you are probably not alone. There are bound to be others out there with similar needs. Please search our [issues list](https://github.com/getumbrel/umbrel/issues) on GitHub and our [community forum](https://community.umbrel.com). If you can't find an existing feature request, please open a new topic on the [community forum](https://community.umbrel.com) with your feature request.
Contributing code and documentation changes
-------------------------------------------
If you would like to contribute a new feature or a bugfix, please discuss your idea first on a GitHub issue. If there is no existing GitHub issue for your idea, please open one. There are often a number of ways to fix a problem and it is important to find the right approach before spending time on a PR that cannot be merged. So we request you to only start the work on implementing a feature once you received a green light from one of our maintainers on your idea and its implementation.
We add the [help wanted](https://github.com/getumbrel/umbrel/labels/help%20wanted) label to existing GitHub issues for which community contributions are particularly welcome, and we use the [good first issue](https://github.com/getumbrel/umbrel/labels/good%20first%20issue) label to mark issues that we think will be suitable for new contributors.
The process for contributing to any of the [Umbrel repositories](https://github.com/getumbrel/) is similar.
### Contributor License
By submitting your contribution, you agree that all of your present and past contributions to us are licensed from you under the MIT license (text below) and do not require us to include your copyright notice. However, if you want to be on our contributor list, please leave us a message [here](https://keybase.io/team/getumbrel).
```
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall
be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
```
================================================
FILE: LICENSE.md
================================================
> Umbrel is licensed under the PolyForm Noncommercial License 1.0.0. Please refer to our [License FAQ](https://github.com/getumbrel/umbrel/wiki/License-FAQ) if you have any questions or reach out to us directly at support@umbrel.com.
# PolyForm Noncommercial License 1.0.0
<https://polyformproject.org/licenses/noncommercial/1.0.0>
## Acceptance
In order to get any license under these terms, you must agree
to them as both strict obligations and conditions to all
your licenses.
## Copyright License
The licensor grants you a copyright license for the
software to do everything you might do with the software
that would otherwise infringe the licensor's copyright
in it for any permitted purpose. However, you may
only distribute the software according to [Distribution
License](#distribution-license) and make changes or new works
based on the software according to [Changes and New Works
License](#changes-and-new-works-license).
## Distribution License
The licensor grants you an additional copyright license
to distribute copies of the software. Your license
to distribute covers distributing the software with
changes and new works permitted by [Changes and New Works
License](#changes-and-new-works-license).
## Notices
You must ensure that anyone who gets a copy of any part of
the software from you also gets a copy of these terms or the
URL for them above, as well as copies of any plain-text lines
beginning with `Required Notice:` that the licensor provided
with the software. For example:
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
## Changes and New Works License
The licensor grants you an additional copyright license to
make changes and new works based on the software for any
permitted purpose.
## Patent License
The licensor grants you a patent license for the software that
covers patent claims the licensor can license, or becomes able
to license, that you would infringe by using the software.
## Noncommercial Purposes
Any noncommercial purpose is a permitted purpose.
## Personal Uses
Personal use for research, experiment, and testing for
the benefit of public knowledge, personal study, private
entertainment, hobby projects, amateur pursuits, or religious
observance, without any anticipated commercial application,
is use for a permitted purpose.
## Noncommercial Organizations
Use by any charitable organization, educational institution,
public research organization, public safety or health
organization, environmental protection organization,
or government institution is use for a permitted purpose
regardless of the source of funding or obligations resulting
from the funding.
## Fair Use
You may have "fair use" rights for the software under the
law. These terms do not limit them.
## No Other Rights
These terms do not allow you to sublicense or transfer any of
your licenses to anyone else, or prevent the licensor from
granting licenses to anyone else. These terms do not imply
any other licenses.
## Patent Defense
If you make any written claim that the software infringes or
contributes to infringement of any patent, your patent license
for the software granted under these terms ends immediately. If
your company makes such a claim, your patent license ends
immediately for work on behalf of your company.
## Violations
The first time you are notified in writing that you have
violated any of these terms, or done anything with the software
not covered by your licenses, your licenses can nonetheless
continue if you come into full compliance with these terms,
and take practical steps to correct past violations, within
32 days of receiving notice. Otherwise, all your licenses
end immediately.
## No Liability
***As far as the law allows, the software comes as is, without
any warranty or condition, and the licensor will not be liable
to you for any damages arising out of these terms or the use
or nature of the software, under any kind of legal claim.***
## Definitions
The **licensor** is the individual or entity offering these
terms, and the **software** is the software the licensor makes
available under these terms.
**You** refers to the individual or entity agreeing to these
terms.
**Your company** is any legal entity, sole proprietorship,
or other kind of organization that you work for, plus all
organizations that have control over, are under the control of,
or are under common control with that organization. **Control**
means ownership of substantially all the assets of an entity,
or the power to direct its management and policies by vote,
contract, or otherwise. Control can be direct or indirect.
**Your licenses** are all the licenses granted to you for the
software under these terms.
**Use** means anything you do with the software requiring one
of your licenses.
================================================
FILE: README.md
================================================
[](https://umbrel.com/umbrelos)
<p align="center">
<h1 align="center">umbrelOS</h1>
<p align="center">
A beautiful home server OS for self-hosting
<br />
<a href="https://umbrel.com"><strong>umbrel.com »</strong></a>
<br />
<br />
Get an <a href="https://umbrel.com/umbrel-pro">Umbrel Pro</a> or <a href="https://umbrel.com/umbrel-home">Umbrel Home</a> for the full experience, or install umbrelOS on a <a href="https://github.com/getumbrel/umbrel/wiki/Install-umbrelOS-on-a-Raspberry-Pi-5">Raspberry Pi 5</a> or <a href="https://github.com/getumbrel/umbrel/wiki/Install-umbrelOS-on-x86-systems">any x86 system</a> for free.
<br />
<br />
<a href="https://x.com/umbrel">
<img src="https://img.shields.io/twitter/follow/umbrel?style=social" />
</a>
<a href="https://discord.gg/efNtFzqtdx">
<img src="https://img.shields.io/discord/936694604231766046?logo=discord&logoColor=5351FB&label=Discord&labelColor=white&color=5351FB&cacheSeconds=60">
</a>
<a href="https://reddit.com/r/getumbrel">
<img src="https://img.shields.io/reddit/subreddit-subscribers/getumbrel?style=social">
</a>
<a href="https://community.umbrel.com">
<img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fcommunity.umbrel.com&style=flat&label=Community%20Forum&color=5351FB&cacheSeconds=60">
</a>
</p>
</p>
<br />
<p align="center">
At Umbrel, we believe that everyone should be able to enjoy the convenience and benefits of the cloud, without giving up ownership and control of their data.
</p>
<p align="center">
To achieve our vision, we're building a new kind of a home server OS. Instead of paying ransoms for storing your data on someone else's computer while they auction it off to advertisers — you can now easily spin up a server and self-host your data and services at home.
</p>
<p align="center">
Just like the cloud, but one that you own and control.
</p>
<br />
## Installing umbrelOS
umbrelOS is designed for the [Umbrel Pro](https://umbrel.com/umbrel-pro) and [Umbrel Home](https://umbrel.com/umbrel-home), where it includes first-class support for all features. On other devices (like Raspberry Pi or x86 systems), it’s freely available with core functionality, but support and feature availability are best-effort due to hardware differences.
For a detailed feature breakdown, see our [comparison guide](https://github.com/getumbrel/umbrel/wiki/umbrelOS-on-Umbrel-Home-vs.-DIY).
### Installation guides
- [Install umbrelOS on a Raspberry Pi 5](https://github.com/getumbrel/umbrel/wiki/Install-umbrelOS-on-a-Raspberry-Pi-5)
- [Install umbrelOS on any x86 system](https://github.com/getumbrel/umbrel/wiki/Install-umbrelOS-on-x86-Systems)
- [Install umbrelOS in a VM](https://github.com/getumbrel/umbrel/wiki/Install-umbrelOS-on-a-Linux-VM)
[](https://umbrel.com/umbrelos)
[](https://apps.umbrel.com)
[](https://umbrel.com/umbrelos)
[](https://umbrel.com/umbrelos)
[](https://umbrel.com/umbrelos)
[](https://umbrel.com/umbrelos)
## Building apps for umbrelOS
If you're interested in building an app for umbrelOS or packaging an existing one, please refer to the [Umbrel App Framework documentation](https://github.com/getumbrel/umbrel-apps/blob/master/README.md).
## License
umbrelOS is licensed under the PolyForm Noncommercial 1.0.0 license. TL;DR — You're free to use, fork, modify, and redistribute Umbrel for personal and nonprofit use under the same license. If you're interested in using umbrelOS for commercial purposes, such as selling plug-and-play home servers with umbrelOS, etc — please reach out to us at partner@umbrel.com.
[](https://github.com/getumbrel/umbrel/blob/master/LICENSE.md)
[umbrel.com](https://umbrel.com)
================================================
FILE: containers/app-auth/.dockerignore
================================================
Dockerfile
node_modules
.git
.github
test
dist
*.log
================================================
FILE: containers/app-auth/.gitignore
================================================
.DS_Store
node_modules
/dist
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Local dev env
.env.development
# Local todo file
.todo
================================================
FILE: containers/app-auth/Dockerfile
================================================
# UI build stage
FROM node:18.19.1-buster-slim AS umbrel-app-auth-ui-builder
# Set the working directory
WORKDIR /app
# Copy the package.json and package-lock.json
COPY packages/ui/package.json ./
COPY packages/ui/package-lock.json ./
# Install the dependencies
RUN npm ci
# Copy the rest of the files
COPY packages/ui/ .
# Build the app
RUN npm run app-auth:build
# Expose the port
EXPOSE 2003
# Start the app
CMD ["npm", "run", "app-auth:start"]
# Build Stage
FROM node:16-buster-slim AS umbrel-app-auth-builder
# Create app directory
WORKDIR /app
# Copy 'yarn.lock' and 'package.json'
COPY containers/app-auth/yarn.lock containers/app-auth/package.json ./
# Install dependencies
RUN yarn
# Copy project files and folders to the current working directory (i.e. '/app')
COPY containers/app-auth .
# Final image
FROM node:16-buster-slim AS umbrel-app-auth
# Copy built code from build stage to '/app' directory
COPY --from=umbrel-app-auth-builder /app /app
# Copy built ui to /app/dist
COPY --from=umbrel-app-auth-ui-builder /app/dist-app-auth /app/dist
# Change directory to '/app'
WORKDIR /app
CMD [ "yarn", "start" ]
================================================
FILE: containers/app-auth/README.md
================================================
[](https://github.com/getumbrel/umbrel-app-auth)
[](https://github.com/getumbrel/umbrel-app-auth/actions?query=workflow%3A"Docker+build+on+push")
[](https://hub.docker.com/repository/registry-1.docker.io/getumbrel/app-auth/tags?page=1)
# ☂️ App Auth
App-auth is a simple authentication and redirection system for Umbrel apps. It ensures (where applicable) that apps are password (and OTP) protected. It runs by-default as a containerized service.
## 🚀 Getting started
If you are looking to run Umbrel on your hardware, you do not need to run this service on it's own. Just download [Umbrel OS](https://github.com/getumbrel/umbrel-os/releases) and you're good to go.
## 🛠 Running app-auth
Make sure [`umbrel-manager`](https://github.com/getumbrel/umbrel-manager) are running and available.
### Development and testing
```sh
cd $UMBREL_ROOT/containers/app-auth/test
./test.sh
```
### Environment variables (dev/testing)
The following environment variables are set in `.env` file of the project's root:
| Variable | Description | Default |
| ------------- | ------------- | ------------- |
| `PORT` | Web server port within container | `2000` |
| `UMBREL_AUTH_SECRET` | A shared secret for manager, app-auth and app-proxy | `umbrel` |
| `MANAGER_IP` | Umbrel's manager IP | `10.21.21.4` |
| `MANAGER_PORT` | Umbrel's manager container port | `9005` |
================================================
FILE: containers/app-auth/bin/www
================================================
#!/usr/bin/env node
const cookieParser = require("cookie-parser");
const express = require('express');
const { StatusCodes } = require('http-status-codes');
const authRoutes = require('../routes/auth.js');
const handleErrorMiddleware = require('../middleware/handle_error.js');
const CONSTANTS = require('../utils/const.js');
const app = express();
app.disable('x-powered-by');
app.set('view engine', 'ejs');
app.use(cookieParser(CONSTANTS.UMBREL_AUTH_SECRET));
app.use('/', authRoutes);
app.use(handleErrorMiddleware);
app.use((req, res) => {
res.status(StatusCodes.NOT_FOUND).json();
});
app.listen(CONSTANTS.PORT, () => {
console.log(`Listening on port: ${CONSTANTS.PORT}`);
});
================================================
FILE: containers/app-auth/middleware/handle_error.js
================================================
function handleError(error, req, res, next) {
var statusCode = error.statusCode || 500;
var route = req.url || '';
var message = error.message || '';
res.status(statusCode).json(message);
}
module.exports = handleError;
================================================
FILE: containers/app-auth/middleware/validate_token.js
================================================
const url = require('url');
const hmacUtils = require('../utils/hmac.js');
const tokenUtils = require('../utils/token.js');
const expressUtils = require('../utils/express.js');
const appUtils = require("../utils/app.js");
const hostResolution = require('../utils/host_resolution.js');
const CONSTANTS = require('../utils/const.js');
const APP_PROXY_AUTH_TOKEN_PATH = "/umbrel_/api/v1/auth/token";
async function redirectState(token, req) {
const app = expressUtils.getQueryParam(req, "app");
const origin = expressUtils.getQueryParam(req, "origin");
const path = expressUtils.getQueryParam(req, "path");
// app ids are only allowed alpha-numeric characters
// plus the hyphen (-)
const appIdSanitised = appUtils.sanitiseId(app);
// This builds up a url as to where
// We're going to redirect/POST to
const redirectUrl = url.format({
protocol: req.protocol,
host: await hostResolution.host(req, appIdSanitised, origin),
pathname: APP_PROXY_AUTH_TOKEN_PATH
});
return {
url: redirectUrl,
params: {
"r": path,
"token": token,
"signature": hmacUtils.sign(token, CONSTANTS.UMBREL_AUTH_SECRET)
}
};
}
async function redirect(res, token, req) {
res.render("pages/redirect", await redirectState(token, req));
}
function mw () {
return async function (req, res, next) {
const token = req.cookies.UMBREL_PROXY_TOKEN;
// If we already have a valid token
// Then the user doesn't need to login again
// We can redirect to the app with the token
if(await tokenUtils.validate(token)) {
await redirect(res, token, req);
} else {
next();
}
};
}
module.exports = {
mw,
redirect,
redirectState
};
================================================
FILE: containers/app-auth/package.json
================================================
{
"name": "app-auth",
"version": "0.0.1",
"private": true,
"scripts": {
"lint": "eslint",
"start": "node ./bin/www",
"test": "mocha 'test/**/*.js'",
"coverage": "nyc --all mocha 'test/**/*.js'",
"postcoverage": "codecov",
"build": "docker buildx build --platform linux/amd64,linux/arm64 --tag getumbrel/auth-server --file Dockerfile ../../"
},
"dependencies": {
"animate.css": "^3.7.2",
"axios": "^0.19.2",
"bootstrap-vue": "^2.11.0",
"cookie-parser": "^1.4.6",
"core-js": "^3.4.4",
"express": "^4.17.3",
"http-status-codes": "^2.2.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"vue": "^2.6.10",
"vue-router": "^3.1.3",
"vuex": "^3.1.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",
"@vue/cli-plugin-eslint": "^4.1.0",
"@vue/cli-plugin-router": "^4.1.0",
"@vue/cli-plugin-vuex": "^4.1.0",
"@vue/cli-service": "^4.1.0",
"@vue/eslint-config-prettier": "^5.0.0",
"babel-eslint": "^10.0.3",
"chai": "^4.1.2",
"chai-http": "^4.2.0",
"codecov": "^3.7.1",
"eslint": "^5.16.0",
"eslint-plugin-prettier": "^3.1.1",
"eslint-plugin-vue": "^5.0.0",
"mocha": "^7.1.2",
"nyc": "15.0.1",
"prettier": "^1.19.1",
"sass": "^1.23.7",
"sass-loader": "^8.0.0",
"vue-template-compiler": "^2.6.10"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"@vue/prettier"
],
"rules": {
"no-console": "off"
},
"parserOptions": {
"parser": "babel-eslint"
}
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
================================================
FILE: containers/app-auth/routes/auth.js
================================================
const express = require("express");
const axios = require("axios");
const { StatusCodes } = require("http-status-codes");
const fs = require("fs").promises;
const yaml = require("js-yaml");
// const CONSTANTS = require("../utils/const.js");
const manager = require("../utils/manager.js");
const dashboard = require("../utils/dashboard.js");
const safeHandler = require("../utils/safe_handler.js");
const expressUtils = require("../utils/express.js");
const appUtils = require("../utils/app.js");
const validateToken = require("../middleware/validate_token.js");
const router = express.Router();
router.use(express.json());
// --- Public paths ---
const publicPaths = [
// "/app-auth",
"/assets",
"/favicon",
"/figma-exports",
"/locales",
"/wallpapers",
// not needed
// "/generated-tabler-icons"
];
publicPaths.forEach((path) => {
router.use(path, express.static(`/app/dist${path}`));
});
router.get("/", safeHandler(validateToken.mw()), (req, res) => {
res.sendFile("/app/dist/index.html");
});
// ---
router.post(
"/v1/account/login",
safeHandler(async (req, res) => {
let response;
try {
response = await axios.post(
`http://${process.env.UMBRELD_RPC_HOST}/trpc/user.login`,
req.body
);
} catch (e) {
console.log({ e });
if (e.isAxiosError === true) {
return res.status(e.response.status).send(e.response.data);
}
throw e;
}
let proxyToken = "";
const setCookieHeader = response.headers["set-cookie"];
if (setCookieHeader) {
// `set-cookie` header can be an array if multiple cookies are set
setCookieHeader.forEach((cookie) => {
if (cookie.startsWith("UMBREL_PROXY_TOKEN=")) {
proxyToken = cookie.split(";")[0].split("=")[1];
}
});
}
if (proxyToken) {
const ONE_SECOND = 1000;
const ONE_MINUTE = 60 * ONE_SECOND;
const ONE_HOUR = 60 * ONE_MINUTE;
const ONE_DAY = 24 * ONE_HOUR;
const ONE_WEEK = 7 * ONE_DAY;
const expires = new Date(Date.now() + ONE_WEEK);
res
.cookie("UMBREL_PROXY_TOKEN", proxyToken, {
httpOnly: true,
expires,
sameSite: "lax",
})
.json(await validateToken.redirectState(proxyToken, req));
} else {
// This case should never happen as an error is thrown
// if the credentials are bad and is handled above (catch block)
res.status(StatusCodes.UNAUTHORIZED).send("Failed to authenticate");
}
})
);
// Get wallpaper (public)
router.get(
"/v1/account/wallpaper",
safeHandler(async (req, res) => {
const store = yaml.load(await fs.readFile("/data/umbrel.yaml"));
const { wallpaper } = store.user;
res.send(wallpaper);
})
);
// Get basic info for an app
router.get(
"/v1/apps",
safeHandler(async (req, res) => {
const appIdSanitised = appUtils.sanitiseId(
expressUtils.getQueryParam(req, "app")
);
res.send(await appUtils.getBasicInfo(appIdSanitised));
})
);
// TODO: remove
router.get(
"/wallpapers/:filename(\\d+[.]\\w+)",
safeHandler(async (req, res) => {
const response = await dashboard.wallpaper.get(req.params.filename);
response.data.pipe(res);
})
);
module.exports = router;
================================================
FILE: containers/app-auth/test/docker-compose.yml
================================================
version: '3.7'
services:
auth:
image: getumbrel/app-auth1
user: "1000:1000"
build:
context: ..
dockerfile: Dockerfile.dev
ports:
- "2001:2000"
environment:
PORT: 2000
UMBREL_AUTH_SECRET: umbrel
MANAGER_IP: $MANAGER_IP
MANAGER_PORT: 3006
volumes:
- ..:/app
- ./fixtures/tor/data:/var/lib/tor:ro
- ./fixtures/app-data:/app-data:ro
networks:
default:
external:
name: umbrel_main_network
================================================
FILE: containers/app-auth/test/fixtures/app-data/mempool/umbrel-app.yml
================================================
id: mempool
category: Explorers
name: mempool
version: 2.3.1
tagline: A self-hosted explorer for the Bitcoin community
description: |-
Trusted third parties are security holes. Self-host your own instance of mempool.space on Umbrel for maximum privacy.
Features:
- Live dashboard visualizing the mempool and blockchain
- Live transaction tracking
- Search any transaction, block or address
- Fee estimations
- Mempool historical data
- TV View for larger displays as a TV in a cafe or bar
- View transaction scripts and op_return messages
- Audio notifications on transaction confirmed and address balance change
- Multiple languages support
- JSON APIs
developer: Mempool Space K.K.
website: https://mempool.space/about
dependencies:
- bitcoind
- electrum
repo: https://github.com/mempool/mempool
support: https://mempool.support
port: 4006
gallery:
- 1.jpg
- 2.jpg
- 3.jpg
path: ''
defaultUsername: ''
defaultPassword: ''
================================================
FILE: containers/app-auth/test/global.js
================================================
const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
chai.should();
global.expect = chai.expect;
global.assert = chai.assert;
before(() => {
});
global.reset = () => {
};
after(() => {
});
================================================
FILE: containers/app-auth/test/test.sh
================================================
#!/bin/bash
export MANAGER_IP="10.21.21.4"
docker-compose up
================================================
FILE: containers/app-auth/test/utils/hmac.js
================================================
const hmac = require("../../utils/hmac.js");
describe('hmac', () => {
it('should sign the message', () => {
assert.equal("4oCtD/Y2Xfb8J/tvCw9mrsRmMekbirseumiW4JrFahI=", hmac.sign("hello world", "my-secret-123"));
assert.equal("qENA22U3zGY2ZhBPh9Tes+Fjt/SS8pjvL6d2Z0ZMTJk=", hmac.sign("https://xkcd.com/386/", "my-secret-123"));
assert.equal("+fSwPHNg4EtsNpso1Iope7g3A7pPbTZubygel/9WdYc=", hmac.sign("https://xkcd.com/386/", "another-secret"));
});
it('should verify the signature for a message', () => {
assert.isTrue(hmac.verify("https://xkcd.com/386/", "my-secret-123", "qENA22U3zGY2ZhBPh9Tes+Fjt/SS8pjvL6d2Z0ZMTJk="));
assert.isTrue(hmac.verify("https://xkcd.com/386/", "another-secret", "+fSwPHNg4EtsNpso1Iope7g3A7pPbTZubygel/9WdYc="));
assert.isFalse(hmac.verify("https://xkcd.com/386/something/random", "another-secret", "+fSwPHNg4EtsNpso1Iope7g3A7pPbTZubygel/9WdYc="));
});
});
================================================
FILE: containers/app-auth/utils/app.js
================================================
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
const CONSTANTS = require('./const.js');
async function getBasicInfo(app){
try {
const manifestFile = path.join(CONSTANTS.APP_DATA_PATH, app, 'umbrel-app.yml');
const manifestYaml = await fs.readFile(manifestFile, "utf-8");
const manifest = yaml.load(manifestYaml, 'utf8');
return {
id: manifest.id,
name: manifest.name
};
} catch(e) {
throw new Error("App not found");
}
}
// App IDs are only allowed
// Alpha-numeric characters with hyphens
function sanitiseId(appId){
return appId.replace(/[^a-zA-Z0-9-]/g, "");
}
module.exports = {
getBasicInfo,
sanitiseId
};
================================================
FILE: containers/app-auth/utils/const.js
================================================
function readFromEnvOrTerminate(key) {
const value = process.env[key];
if(typeof(value) !== "string" || value.trim().length === 0) {
console.error(`The env. variable '${key}' is not set. Terminating...`);
process.exit(0);
}
return value;
}
module.exports = Object.freeze({
UMBREL_COOKIE_NAME: "UMBREL_SESSION",
LOG_LEVEL: process.env.LOG_LEVEL || "info",
PORT: parseInt(process.env.PORT) || 2000,
UMBREL_AUTH_SECRET: readFromEnvOrTerminate("UMBREL_AUTH_SECRET"),
TOR_PATH: process.env.TOR_PATH || "/var/lib/tor",
APP_DATA_PATH: process.env.APP_DATA_PATH || "/app-data",
MANAGER_IP: readFromEnvOrTerminate("MANAGER_IP"),
MANAGER_PORT: parseInt(readFromEnvOrTerminate("MANAGER_PORT")),
DASHBOARD_IP: readFromEnvOrTerminate("DASHBOARD_IP"),
DASHBOARD_PORT: parseInt(readFromEnvOrTerminate("DASHBOARD_PORT")),
});
================================================
FILE: containers/app-auth/utils/dashboard.js
================================================
const axios = require('axios');
const package = require('../package.json');
const CONSTANTS = require('./const.js');
const axiosInstance = axios.create({
baseURL: `http://${CONSTANTS.DASHBOARD_IP}:${CONSTANTS.DASHBOARD_PORT}`,
headers: {
common: {
"User-Agent": `${package.name}/${package.version}`
}
}
});
const wallpaper = {
get: async function(filename) {
return axiosInstance({
method: 'GET',
url: `/wallpapers/${filename}`,
responseType: 'stream'
});
}
};
module.exports = {
wallpaper
};
================================================
FILE: containers/app-auth/utils/express.js
================================================
function getQueryParam(req, key) {
const value = req.query[key];
if(typeof(value) !== "string" || value.trim().length == 0) {
throw new Error(`'${key}' is missing`);
}
return value;
}
module.exports = {
getQueryParam
};
================================================
FILE: containers/app-auth/utils/hmac.js
================================================
const crypto = require('crypto');
function sign(input, secret) {
return crypto
.createHmac('sha256', secret)
.update(input)
.digest('base64');
};
function verify(input, secret, signature){
const inputSignature = Buffer.from( sign(input, secret) );
const testSignature = Buffer.from( signature );
return inputSignature.length === testSignature.length && crypto.timingSafeEqual(inputSignature, testSignature);
};
module.exports = {
sign,
verify
};
================================================
FILE: containers/app-auth/utils/host_resolution.js
================================================
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
const CONSTANTS = require('./const.js');
async function getTorHostname(app) {
const torHostnameFile = path.join(CONSTANTS.TOR_PATH, `app-${app}`, "hostname");
return (await fs.readFile(torHostnameFile, "utf-8")).trim();
}
async function getAppPort(app) {
const appManifestFile = path.join(CONSTANTS.APP_DATA_PATH, app, 'umbrel-app.yml');
const appManifestYaml = await fs.readFile(appManifestFile, "utf-8");
const appManifest = yaml.load(appManifestYaml, 'utf8');
return appManifest.port;
}
async function host(req, app, origin) {
try {
switch(origin) {
case "tor":
return (await getTorHostname(app));
case "host":
const appPort = (await getAppPort(app));
return `${req.hostname}:${appPort}`;
}
} catch (e) {
throw new Error("Failed to determine host");
}
throw new Error("Unsupported origin");
}
module.exports = {
host
};
================================================
FILE: containers/app-auth/utils/manager.js
================================================
const axios = require('axios');
const package = require('../package.json');
const CONSTANTS = require('./const.js');
const axiosInstance = axios.create({
baseURL: `http://${CONSTANTS.MANAGER_IP}:${CONSTANTS.MANAGER_PORT}`,
headers: {
common: {
"User-Agent": `${package.name}/${package.version}`
}
}
});
const account = {
login: async function(body) {
return axiosInstance.post('/v1/account/login', body);
},
token: async function(token) {
return axiosInstance.get('/v1/account/token', {
params: {
token
}
});
},
wallpaper: async function(token) {
return axiosInstance.get('/v1/account/wallpaper');
}
};
module.exports = {
account
};
================================================
FILE: containers/app-auth/utils/safe_handler.js
================================================
// this safe handler is used to wrap our api methods
// so that we always fallback and return an exception if there is an error
// inside of an async function
// Mostly copied from vault/server/utils/safeHandler.js
function safeHandler(handler) {
return async (req, res, next) => {
try {
return await handler(req, res, next);
} catch (err) {
return next(err);
}
};
}
module.exports = safeHandler;
================================================
FILE: containers/app-auth/utils/token.js
================================================
const jwt = require("jsonwebtoken");
const JWT_ALGORITHM = "HS256";
const secret = process.env.JWT_SECRET;
function validate(token) {
if (typeof token !== "string") return false;
console.log(`Validating token: ${token.substr(0, 12)} ...`);
const payload = jwt.verify(token, secret, {
algorithms: [JWT_ALGORITHM],
});
return payload.proxyToken === true;
}
module.exports = {
validate,
};
================================================
FILE: containers/app-auth/views/pages/redirect.ejs
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Redirecting...</title>
</head>
<body onload="document.forms[0].submit();">
<form method="POST" action="<%- url %>">
<% for (var key in params ) { %>
<input type="hidden" name="<%- key %>" value="<%- params[key] %>">
<% } %>
</form>
</body>
</html>
================================================
FILE: containers/app-proxy/.dockerignore
================================================
Dockerfile
node_modules
.git
.github
test
================================================
FILE: containers/app-proxy/.gitignore
================================================
.DS_Store
node_modules
/dist
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Local dev env
.env.development
# Local todo file
.todo
================================================
FILE: containers/app-proxy/Dockerfile
================================================
# Build Stage
FROM node:16-buster-slim AS umbrel-app-proxy-builder
# Create app directory
WORKDIR /app
# Copy 'yarn.lock' and 'package.json'
COPY yarn.lock package.json ./
# Install dependencies
RUN yarn install --production
# Copy project files and folders to the current working directory (i.e. '/app')
COPY . .
# Final image
FROM node:16-buster-slim AS umbrel-app-proxy
# Copy built code from build stage to '/app' directory
COPY --from=umbrel-app-proxy-builder /app /app
# Change directory to '/app'
WORKDIR /app
CMD [ "yarn", "start" ]
================================================
FILE: containers/app-proxy/Dockerfile.dev
================================================
FROM node:16-buster-slim
# make the 'app' folder the current working directory
WORKDIR /app
ENTRYPOINT ["bash"]
CMD ["-c", "yarn && yarn start"]
================================================
FILE: containers/app-proxy/README.md
================================================
[](https://github.com/getumbrel/umbrel-app-proxy)
[](https://github.com/getumbrel/umbrel-app-proxy/actions?query=workflow%3A"Docker+build+on+push")
[](https://hub.docker.com/repository/registry-1.docker.io/getumbrel/app-proxy/tags?page=1)
# ☂️ App Proxy
App-proxy is a transparent HTTP proxy to add authentication to Umbrel apps. Every HTTP request and Websocket connection goes through the proxy and each request has the session token checked for validity. The session token is set via [App-auth](https://github.com/getumbrel/umbrel/tree/master/containers/app-auth). It runs by-default as a containerized service.
## 🚀 Getting started
If you are looking to run Umbrel on your hardware, you do not need to run this service on it's own. Just download [Umbrel OS](https://github.com/getumbrel/umbrel-os/releases) and you're good to go.
## 🛠 Running app-proxy
Make sure [`umbrel-manager`](https://github.com/getumbrel/umbrel-manager) and [`app-auth`](https://github.com/getumbrel/umbrel/tree/master/containers/app-auth) are running and available.
### Development and testing
```sh
cd $UMBREL_ROOT/containers/app-proxy/test
./test.sh docker-compose.app1.yml
```
Within the `test` directory there are several test apps to test different functionality such as Websocket and SSE with the proxy.
### Environment variables (dev/testing)
The following environment variables are set in `.env` file of the project's root:
| Variable | Description | Default |
| --------------------------------- | ----------------------------------------------------------------------------- | ---------------------------- |
| `LOG_LEVEL` | Log level for the proxy (`http-proxy-middleware`) | `info` |
| `PROXY_PORT` | HTTP proxy container port | `4000` |
| `PROXY_AUTH_ADD` | `true`/`false` as to whether the app should be protected with authentication | `true` |
| `PROXY_AUTH_WHITELIST` | A comma seperated list of paths that are whitelisted (e.g. `/public/*`) | |
| `PROXY_AUTH_BLACKLIST` | A comma seperated list of paths that are whitelisted (e.g. `/admin/*,/api/*`) | |
| `APP_HOST` | App's frontend container hostname/IP | |
| `APP_PORT` | App's frontend container port | |
| `APP_MANIFEST_FILE` | Location of app's manifest file | `/extra/umbrel-app.yml` |
| `UMBREL_AUTH_PORT` | App-auth's exposed (port-forwarded) port | `2000` |
| `UMBREL_AUTH_SECRET` | A shared secret for manager, app-auth and app-proxy | `umbrel` |
| `UMBREL_AUTH_HIDDEN_SERVICE_FILE` | Location of app-auth's Tor HS hostname | `/var/lib/tor/auth/hostname` |
| `MANAGER_IP` | Umbrel's manager IP | `10.21.21.4` |
| `MANAGER_PORT` | Umbrel's manager container port | `9005` |
================================================
FILE: containers/app-proxy/bin/www
================================================
#!/usr/bin/env node
const cookieParser = require('cookie-parser');
const express = require('express');
const waitPort = require('wait-port');
const proxy = require('../utils/proxy.js');
const umbrelRoutes = require('../routes/umbrel.js');
const handleErrorMiddleware = require('../middleware/handle_error.js');
const CONSTANTS = require('../utils/const.js');
const app = express();
app.disable('x-powered-by');
app.set('view engine', 'ejs');
app.use(cookieParser(CONSTANTS.UMBREL_AUTH_SECRET));
app.use('/umbrel_', umbrelRoutes);
app.use(handleErrorMiddleware);
const middleware = proxy.apply(app);
// We'll only open the app proxy's port
// Once the app's port is open
console.log(`Waiting for ${CONSTANTS.APP_HOST}:${CONSTANTS.APP_PORT} to open...`);
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
(async () => {
while(true) {
try {
await waitPort({
host: CONSTANTS.APP_HOST,
port: CONSTANTS.APP_PORT,
output: "silent",
// Wait indefinitely for the app to start
timeout: 0
});
// Exit loop on success
break;
} catch (error) {
console.log(`Error waiting for port: "${error.message}"`);
// Wait before retrying
await delay(100);
console.log('Retrying...');
}
}
console.log(`${CONSTANTS.APP.name} is now ready...`);
const server = app.listen(CONSTANTS.PROXY_PORT, () => {
console.log(`Listening on port: ${CONSTANTS.PROXY_PORT}`);
});
// This allows it to proxy WebSockets without the initial http request
server.on('upgrade', middleware.upgrade);
})();
================================================
FILE: containers/app-proxy/middleware/handle_error.js
================================================
function handleError(error, req, res, next) {
var statusCode = error.statusCode || 500;
var route = req.url || '';
var message = error.message || '';
res.status(statusCode).json(message);
}
module.exports = handleError;
================================================
FILE: containers/app-proxy/package.json
================================================
{
"name": "app-proxy",
"version": "0.0.1",
"private": true,
"scripts": {
"lint": "eslint",
"start": "node ./bin/www",
"test": "mocha 'test/**/*.js'",
"coverage": "nyc --all mocha 'test/**/*.js'",
"postcoverage": "codecov",
"build": "docker buildx build --platform linux/amd64,linux/arm64 --tag getumbrel/app-proxy ."
},
"dependencies": {
"axios": "^0.26.1",
"cookie-parser": "^1.4.6",
"dotenv": "^16.0.0",
"ejs": "^3.1.6",
"express": "^4.17.3",
"express-validator": "^6.14.0",
"http-proxy-middleware": "^2.0.4",
"http-status-codes": "^2.2.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"wait-port": "^0.2.9"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"chai": "^4.1.2",
"chai-http": "^4.2.0",
"codecov": "^3.7.1",
"eslint": "^7.0.0",
"mocha": "^7.1.2",
"node-mocks-http": "^1.11.0",
"nyc": "15.0.1"
}
}
================================================
FILE: containers/app-proxy/routes/umbrel.js
================================================
const { StatusCodes } = require("http-status-codes");
const bodyParser = require("body-parser");
const express = require("express");
const validator = require("express-validator");
const router = express.Router();
const CONSTANTS = require("../utils/const.js");
const tokenUtils = require("../utils/token.js");
const hmacUtils = require("../utils/hmac.js");
const safeHandler = require("../utils/safe_handler.js");
const ONE_SECOND = 1000;
const ONE_MINUTE = 60 * ONE_SECOND;
const ONE_HOUR = 60 * ONE_MINUTE;
const ONE_DAY = 24 * ONE_HOUR;
const ONE_WEEK = 7 * ONE_DAY;
const parseForm = bodyParser.urlencoded({ extended: false });
router.use(parseForm);
router.post(
"/api/v1/auth/token",
[
validator.body("token").isString(),
validator.body("signature").isString(),
validator.body("r").isString(),
],
safeHandler(async (req, res) => {
const errors = validator.validationResult(req);
if (!errors.isEmpty()) {
return res.status(StatusCodes.BAD_REQUEST).json({
errors: errors.array(),
});
}
const token = req.body.token;
const signature = req.body.signature;
const redirectTo = req.body.r;
// Before we validate the token, lets check the hmac
if (!hmacUtils.verify(token, CONSTANTS.UMBREL_AUTH_SECRET, signature)) {
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.send(`The signature is invalid`);
}
// Check that the token is valid before setting cookie...
if (!(await tokenUtils.validate(token))) {
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.send(`The token is invalid`);
}
const expires = new Date(Date.now() + ONE_WEEK);
res.cookie("UMBREL_PROXY_TOKEN", token, {
httpOnly: true,
expires,
sameSite: "lax",
});
res.redirect(redirectTo);
})
);
module.exports = router;
================================================
FILE: containers/app-proxy/test/.gitignore
================================================
apps/
================================================
FILE: containers/app-proxy/test/docker-compose.app1.yml
================================================
version: '3.7'
services:
app_proxy:
environment:
APP_HOST: nginxdemo
APP_PORT: 80
PROXY_AUTH_WHITELIST: "*"
PROXY_AUTH_BLACKLIST: "/admin/*,/admin2/*"
nginxdemo:
image: nginxdemos/hello
================================================
FILE: containers/app-proxy/test/docker-compose.app2.yml
================================================
version: '3.7'
services:
app_proxy:
environment:
APP_HOST: frontend
APP_PORT: 8888
frontend:
image: mendhak/http-https-echo
environment:
HTTP_PORT: 8888
================================================
FILE: containers/app-proxy/test/docker-compose.bleskomat.yml
================================================
version: "3.7"
services:
app_proxy:
environment:
APP_HOST: bleskomat_web
APP_PORT: 3333
PROXY_AUTH_ADD: "false"
bleskomat_db:
image: postgres:10.20-stretch@sha256:130e08bb19199bd055e585e8938c5ebb0555dc13b445fad5b0bd727e4b75149c
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
volumes:
- ./apps/bleskomat/data/db:/var/lib/postgresql/data
environment:
- POSTGRES_USER=bleskomat_server
- POSTGRES_DB=bleskomat_server
- POSTGRES_PASSWORD=moneyprintergobrrr
bleskomat_web:
image: bleskomat/bleskomat-server:1.3.4@sha256:7bd91b896c5ca4f69b7c9509b40ccfae273cc46120ec66b2e27b295b0186f230
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
depends_on:
- bleskomat_db
volumes:
- ./apps/bleskomat/data/web:/usr/src/app/data
- ./apps/bleskomat/data/lnd:/lnd:ro
ports:
- "3334:3333"
environment:
DEBUG: "bleskomat-server*,lnurl*"
BLESKOMAT_SERVER_HOST: "0.0.0.0"
BLESKOMAT_SERVER_PORT: "3333"
BLESKOMAT_SERVER_URL: "test.onion"
BLESKOMAT_SERVER_ENDPOINT: "/u"
BLESKOMAT_SERVER_AUTH_API_KEYS: '[]'
BLESKOMAT_SERVER_LIGHTNING: '{"backend":"lnd","config":{"cert":"/lnd/tls.cert","protocol":"https","hostname":"lnd:12345","macaroon":"/lnd/admin.macaroon"}}'
BLESKOMAT_SERVER_STORE: '{"backend":"knex","config":{"client":"postgres","connection":{"host":"bleskomat_db","port":5432,"user":"bleskomat_server","password":"moneyprintergobrrr","database":"bleskomat_server"}}}'
BLESKOMAT_SERVER_COINRATES_DEFAULTS_PROVIDER: "coinbase"
BLESKOMAT_SERVER_ADMIN_WEB: "true"
BLESKOMAT_SERVER_ADMIN_PASSWORD_PLAINTEXT: "$APP_PASSWORD"
BLESKOMAT_SERVER_ENV_FILEPATH: "./data/.env"
================================================
FILE: containers/app-proxy/test/docker-compose.error.yml
================================================
version: '3.7'
services:
app_proxy:
environment:
APP_HOST: app_wrong
APP_PORT: 80
PROXY_AUTH_WHITELIST: "*"
app:
image: nginxdemos/hello
================================================
FILE: containers/app-proxy/test/docker-compose.mempool.yml
================================================
version: "3.7"
services:
app_proxy:
environment:
APP_HOST: web_mempool
APP_PORT: 3006
PROXY_AUTH_WHITELIST: "/admin/"
web_mempool:
image: mempool/frontend:v2.3.1@sha256:38c955caeb58014b266904b059bfabbdab8321d20b11e7cccb139be6dfc8e36e
user: "1000:1000"
init: true
restart: on-failure
stop_grace_period: 1m
command: "./wait-for mariadb:3306 --timeout=720 -- nginx -g 'daemon off;'"
environment:
FRONTEND_HTTP_PORT: 3006
BACKEND_MAINNET_HTTP_HOST: "api"
api:
image: mempool/backend:v2.3.1@sha256:f7b16a6b00ea8aabf3b71a34ec05bb373fa0b6f1d31c7981b767edb2d1b7cf89
user: "1000:1000"
init: true
restart: on-failure
stop_grace_period: 1m
command: "./wait-for-it.sh mariadb:3306 --timeout=720 --strict -- ./start.sh"
volumes:
- ./apps/mempool/data:/backend/cache
environment:
ELECTRUM_TLS: "false"
DATABASE_HOST: "mariadb"
DATABASE_PORT: "3306"
DATABASE_DATABASE: "mempool"
DATABASE_USERNAME: "mempool"
DATABASE_PASSWORD: "mempool"
MEMPOOL_HTTP_PORT: "8999"
MEMPOOL_CACHE_DIR: "/backend/cache"
MEMPOOL_CLEAR_PROTECTION_MINUTES: "20"
mariadb:
image: mariadb:10.5.12@sha256:dfcba5641bdbfd7cbf5b07eeed707e6a3672f46823695a0d3aba2e49bbd9b1dd
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
volumes:
- ./apps/mempool/mysql/data:/var/lib/mysql
environment:
MYSQL_DATABASE: "mempool"
MYSQL_USER: "mempool"
MYSQL_PASSWORD: "mempool"
MYSQL_ROOT_PASSWORD: "moneyprintergobrrr"
================================================
FILE: containers/app-proxy/test/docker-compose.nextcloud.yml
================================================
version: "3.7"
services:
app_proxy:
environment:
- APP_HOST=web
- APP_PORT=80
db:
image: mariadb:10.5.12@sha256:dfcba5641bdbfd7cbf5b07eeed707e6a3672f46823695a0d3aba2e49bbd9b1dd
user: "1000:1000"
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
restart: on-failure
volumes:
- ./apps/nextcloud/data/db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=moneyprintergobrrr
- MYSQL_PASSWORD=moneyprintergobrrr
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
redis:
image: redis:6.2.2-buster@sha256:e10f55f92478715698a2cef97c2bbdc48df2a05081edd884938903aa60df6396
user: "1000:1000"
restart: on-failure
volumes:
- "./apps/nextcloud/data/redis:/data"
web:
image: nextcloud:22.1.1-apache@sha256:99d94124b2024c9f7f38dc12144a92bc0d68d110bcfd374169ebb7e8df0adf8e
# Currently needs to be run as root, if we run as uid 1000 this fails
# https://github.com/nextcloud/docker/blob/05026b029d37fc5cd488d4a4a2a79480e39841ba/21.0/apache/entrypoint.sh#L53-L77
# user: "1000:1000"
restart: on-failure
volumes:
- ./apps/nextcloud/data/nextcloud:/var/www/html
environment:
- MYSQL_HOST=db
- REDIS_HOST=redis
- MYSQL_PASSWORD=moneyprintergobrrr
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- NEXTCLOUD_ADMIN_USER=umbrel
- NEXTCLOUD_ADMIN_PASSWORD=${APP_PASSWORD}
- NEXTCLOUD_TRUSTED_DOMAINS=${APP_DOMAIN}:${APP_PORT}
depends_on:
- db
- redis
cron:
image: nextcloud:22.0.0-apache@sha256:55de721417c16ff110720217406778e16f1b63154d2e8d42fc7913c37dbe6d50
# Currently needs to be run as root, if we run as uid 1000 this fails
# https://github.com/nextcloud/docker/blob/05026b029d37fc5cd488d4a4a2a79480e39841ba/21.0/apache/entrypoint.sh#L53-L77
# user: "1000:1000"
restart: on-failure
volumes:
- ./apps/nextcloud/data/nextcloud:/var/www/html
entrypoint: /cron.sh
depends_on:
- db
- redis
================================================
FILE: containers/app-proxy/test/docker-compose.proxy.yml
================================================
version: '3.7'
services:
caddy:
image: caddy:2.5.1
command: caddy reverse-proxy --from :4007 --to app_proxy:4000
ports:
- "4007:4007"
app_proxy:
environment:
APP_HOST: frontend
APP_PORT: 8888
PROXY_AUTH_WHITELIST: "*"
PROXY_TRUST_UPSTREAM: "true"
frontend:
image: mendhak/http-https-echo
environment:
HTTP_PORT: 8888
================================================
FILE: containers/app-proxy/test/docker-compose.proxyhttps.yaml
================================================
version: '3.7'
services:
caddy:
image: caddy:2.5.1
volumes:
- "./test/Caddyfile-https:/etc/caddy/Caddyfile"
ports:
- "4007:4007"
app_proxy:
environment:
APP_HOST: frontend
APP_PORT: 8888
PROXY_AUTH_WHITELIST: "*"
PROXY_TRUST_UPSTREAM: "true"
frontend:
image: mendhak/http-https-echo
environment:
HTTP_PORT: 8888
================================================
FILE: containers/app-proxy/test/docker-compose.sse.yml
================================================
version: '3.7'
services:
app_proxy:
environment:
APP_HOST: sse_server
APP_PORT: 80
PROXY_AUTH_WHITELIST: "*"
sse_server:
image: getumbrel/sse-test-server
build: ./sse-test-server
================================================
FILE: containers/app-proxy/test/docker-compose.suredbits.yml
================================================
version: "3.7"
services:
app_proxy:
environment:
APP_HOST: web
APP_PORT: 3002
web:
image: bitcoinscala/wallet-server-ui:1.9.1@sha256:3eb479b106811d523c4e0cfde244949f6c76a27c7d1fe59be9b8b51ba2372649
user: "1000:1000"
restart: on-failure
stop_grace_period: 1m
volumes:
- ./apps/suredbits/data/wallet:/home/bitcoin-s/.bitcoin-s
- ./apps/suredbits/data/log:/log
environment:
LOG_PATH: "/log/"
BITCOIN_S_HOME: "/home/bitcoin-s/.bitcoin-s/"
MEMPOOL_API_URL: "http://umbrel.local:3004/api"
WALLET_SERVER_API_URL: "http://walletserver:9999/"
WALLET_SERVER_WS: "ws://walletserver:19999/events"
TOR_PROXY: socks5://localhost:5555
DEFAULT_UI_PASSWORD: "password123"
BITCOIN_S_SERVER_RPC_PASSWORD: "password123"
depends_on:
- walletserver
walletserver:
image: bitcoinscala/bitcoin-s-server:1.9.1-34-3dc70938-SNAPSHOT@sha256:1cd82d19059382f740f7b8acbc2d3aaeaf0c1fd7c662bdbc3ef7b97a27ee181f
user: "1000:1000"
restart: on-failure
volumes:
- ./apps/suredbits/data/wallet:/home/bitcoin-s/.bitcoin-s
environment:
BITCOIN_S_NODE_MODE: "bitcoind"
BITCOIN_S_NETWORK: "regtest"
BITCOIN_S_KEYMANAGER_ENTROPY: "a75a58071bd4c65a1e87c1b970e9d736d9185a427a4f3110d250ea3945d9108d"
BITCOIN_S_PROXY_ENABLED: "false"
BITCOIN_S_TOR_ENABLED: "false"
BITCOIN_S_TOR_PROVIDED: "true"
BITCOIN_S_DLCNODE_PROXY_ENABLED: "true"
BITCOIN_S_DLCNODE_PROXY_SOCKS5: "localhost:5555"
BITCOIN_S_DLCNODE_EXTERNAL_IP: "hiddenservice.onion"
BITCOIN_S_BITCOIND_HOST: "bitcoind"
BITCOIN_S_BITCOIND_PORT: "18443"
BITCOIN_S_BITCOIND_USER: "umbrel"
BITCOIN_S_BITCOIND_PASSWORD: "bitcoinbitcoin"
BITCOIN_S_SERVER_RPC_PASSWORD: "password123"
ports:
- "2862:2862"
bitcoind:
image: lncm/bitcoind:v22.0@sha256:37a1adb29b3abc9f972f0d981f45e41e5fca2e22816a023faa9fdc0084aa4507
command: "${APP_BITCOIN_COMMAND}"
restart: on-failure
stop_grace_period: 15m30s
volumes:
- ./apps/suredbits/data/bitcoin:/data/.bitcoin
================================================
FILE: containers/app-proxy/test/docker-compose.ws.yml
================================================
version: '3.7'
services:
app_proxy:
environment:
APP_HOST: ws_server
APP_PORT: 8010
ws_server:
image: ksdn117/web-socket-test
================================================
FILE: containers/app-proxy/test/docker-compose.yml
================================================
version: '3.7'
services:
app_proxy:
image: getumbrel/app-proxy
build:
context: ..
dockerfile: Dockerfile.dev
user: "1000:1000"
restart: on-failure
ports:
- "${APP_PORT}:4000"
volumes:
- ..:/app
- ./fixtures/mempool-umbrel-app.yml:/extra/umbrel-app.yml:ro
- ./fixtures/tor/data:/var/lib/tor:ro
- ./data:/app-data:ro
environment:
LOG_LEVEL: debug
PROXY_PORT: 4000
PROXY_AUTH_ADD: "true"
PROXY_AUTH_WHITELIST:
PROXY_AUTH_BLACKLIST:
PROXY_TRUST_UPSTREAM:
APP_HOST:
APP_PORT:
APP_MANIFEST_FILE: "/extra/umbrel-app.yml"
UMBREL_AUTH_PORT: "${AUTH_PORT}"
UMBREL_AUTH_SECRET: "${UMBREL_AUTH_SECRET}"
UMBREL_AUTH_HIDDEN_SERVICE_FILE: "/var/lib/tor/auth/hostname"
MANAGER_IP: "${MANAGER_IP}"
MANAGER_PORT: 3006
networks:
default:
external:
name: umbrel_main_network
================================================
FILE: containers/app-proxy/test/fixtures/mempool-umbrel-app.yml
================================================
id: mempool
category: Explorers
name: mempool
version: 2.3.1
tagline: A self-hosted explorer for the Bitcoin community
description: |-
Trusted third parties are security holes. Self-host your own instance of mempool.space on Umbrel for maximum privacy.
Features:
- Live dashboard visualizing the mempool and blockchain
- Live transaction tracking
- Search any transaction, block or address
- Fee estimations
- Mempool historical data
- TV View for larger displays as a TV in a cafe or bar
- View transaction scripts and op_return messages
- Audio notifications on transaction confirmed and address balance change
- Multiple languages support
- JSON APIs
developer: Mempool Space K.K.
website: https://mempool.space/about
dependencies:
- bitcoind
- electrum
repo: https://github.com/mempool/mempool
support: https://mempool.support
port: 4006
gallery:
- 1.jpg
- 2.jpg
- 3.jpg
path: ''
defaultUsername: ''
defaultPassword: ''
================================================
FILE: containers/app-proxy/test/global.js
================================================
const chai = require('chai');
const chaiHttp = require('chai-http');
chai.use(chaiHttp);
chai.should();
global.expect = chai.expect;
global.assert = chai.assert;
before(() => {
});
global.reset = () => {
};
after(() => {
});
================================================
FILE: containers/app-proxy/test/sse-test-server/.dockerignore
================================================
Dockerfile
node_modules
.git
.github
================================================
FILE: containers/app-proxy/test/sse-test-server/Dockerfile
================================================
# Build Stage
FROM node:16-buster-slim AS umbrel-sse-test-server-builder
# Create app directory
WORKDIR /app
# Copy 'yarn.lock' and 'package.json'
COPY yarn.lock package.json ./
# Install dependencies
RUN yarn install --production
# Copy project files and folders to the current working directory (i.e. '/app')
COPY . .
# Final image
FROM node:16-buster-slim AS umbrel-sse-test-server
# Copy built code from build stage to '/app' directory
COPY --from=umbrel-sse-test-server-builder /app /app
# Change directory to '/app'
WORKDIR /app
CMD [ "yarn", "start" ]
================================================
FILE: containers/app-proxy/test/sse-test-server/bin/www
================================================
#!/usr/bin/env node
const express = require('express');
const PORT = process.env.PORT || 80;
const app = express();
app.get('/clock', function(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
const clockInterval = setInterval(() => {
const data = (new Date()).toString();
res.write("data: " + data + "\n\n");
}, 1000);
req.on("close", () => {
clearInterval(clockInterval);
});
});
app.get('/', (req, res) => {
res.end(`<html>
<head>
<script>
if (!!window.EventSource) {
var source = new EventSource('/clock')
source.addEventListener('message', function(e) {
document.getElementById('data').innerHTML+= e.data + "<br>"
}, false)
source.addEventListener('open', function(e) {
document.getElementById('state').innerHTML = "Connected"
}, false)
source.addEventListener('error', function(e) {
const id_state = document.getElementById('state')
if (e.eventPhase == EventSource.CLOSED)
source.close()
if (e.target.readyState == EventSource.CLOSED) {
id_state.innerHTML = "Disconnected"
}
else if (e.target.readyState == EventSource.CONNECTING) {
id_state.innerHTML = "Connecting..."
}
}, false)
} else {
console.log("Your browser doesn't support SSE")
}
</script>
</head>
<body>
<h2>Status: <span id="state"></span></h2>
<h2>Data</h2>
<pre id="data"></pre>
</body>
</html>`);
});
app.listen(PORT, () => {
console.log(`Listening on port: ${PORT}`);
});
================================================
FILE: containers/app-proxy/test/sse-test-server/package.json
================================================
{
"name": "umbrel-sse-test-server",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"express": "^4.17.3"
}
}
================================================
FILE: containers/app-proxy/test/test/Caddyfile-https
================================================
https://umbrel-dev.local:4007 {
reverse_proxy app_proxy:4000
tls internal
}
================================================
FILE: containers/app-proxy/test/test.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
UMBREL_ENV_FILE="$(readlink -f $(dirname "${BASH_SOURCE[0]}")/../../../.env)"
COMPOSE_FILE="${1}"
# Some test env vars. for nextcloud
export APP_DOMAIN="localhost"
export APP_PASSWORD="password"
# Test env vars. for bitcoind
BIN_ARGS=()
BIN_ARGS+=( "-chain=regtest" )
BIN_ARGS+=( "-rpcport=18443" )
BIN_ARGS+=( "-rpcbind=0.0.0.0" )
BIN_ARGS+=( "-rpcallowip=0.0.0.0/0" )
BIN_ARGS+=( "-rpcuser=umbrel" )
BIN_ARGS+=( "-rpcpassword=bitcoinbitcoin" )
BIN_ARGS+=( "-txindex=1" )
BIN_ARGS+=( "-blockfilterindex=1" )
BIN_ARGS+=( "-peerbloomfilters=1" )
BIN_ARGS+=( "-peerblockfilters=1" )
BIN_ARGS+=( "-deprecatedrpc=addresses" )
BIN_ARGS+=( "-rpcworkqueue=128" )
export APP_BITCOIN_COMMAND=$(IFS=" "; echo "${BIN_ARGS[@]}")
# Env vars. specific to app proxy
export APP_PORT=$(cat fixtures/mempool-umbrel-app.yml | yq '.port')
echo "Proxy booting on port: ${APP_PORT}"
# Generate random project id
PROJECT=$(echo -n "${COMPOSE_FILE}" | sha256sum)
docker-compose --env-file "${UMBREL_ENV_FILE}" --project-name "${PROJECT}" -f ./docker-compose.yml -f "${COMPOSE_FILE}" up
================================================
FILE: containers/app-proxy/test/utils/express.js
================================================
const httpMocks = require('node-mocks-http');
const express = require("../../utils/express.js");
describe('express', () => {
it('should attempt to remove a cookie even if that cookie does not exist', () => {
const req = httpMocks.createRequest({
method: 'GET',
protocol: "http",
headers: {
host: "bitcoin.org:4444",
cookie: "session=abc123; csrf=a_value.das384jfdjsi4r2hf29f"
}
});
const cookieHeader = express.removeCookie(req, "abc");
assert.equal("session=abc123; csrf=a_value.das384jfdjsi4r2hf29f", cookieHeader);
});
it('should attempt to remove a cookie even if there are no cookies', () => {
const req = httpMocks.createRequest({
method: 'GET',
protocol: "http",
headers: {
host: "bitcoin.org:4444"
}
});
const cookieHeader = express.removeCookie(req, "does_not_exist");
assert.equal("", cookieHeader);
});
it('should remove the cookie when the cookie exists in the request header', () => {
const req = httpMocks.createRequest({
method: 'GET',
protocol: "http",
headers: {
host: "bitcoin.org:4444",
cookie: "session=abc123; another_cookie=some_value;csrf=a_value.das384jfdjsi4r2hf29f"
}
});
const cookieHeader = express.removeCookie(req, "session");
assert.equal("another_cookie=some_value; csrf=a_value.das384jfdjsi4r2hf29f", cookieHeader);
});
it('should remove the cookie when that 1 cookie exists in the request header', () => {
const req = httpMocks.createRequest({
method: 'GET',
protocol: "http",
headers: {
host: "bitcoin.org:4444",
cookie: "session=abc123"
}
});
const cookieHeader = express.removeCookie(req, "session");
assert.equal("", cookieHeader);
});
it('should remove the cookie when that 1 cookie exists with delimiter in the request header', () => {
const req = httpMocks.createRequest({
method: 'GET',
protocol: "http",
headers: {
host: "bitcoin.org:4444",
cookie: "session=abc123; "
}
});
const cookieHeader = express.removeCookie(req, "session");
assert.equal("", cookieHeader);
});
});
================================================
FILE: containers/app-proxy/test/utils/tor.js
================================================
const tor = require("../../utils/tor.js");
describe('tor', () => {
it('should return the auth HS url', async () => {
const url = await tor.authHsUrl();
assert.equal("the-auth-hs-url.onion", url);
});
});
================================================
FILE: containers/app-proxy/utils/const.js
================================================
const yaml = require('js-yaml');
const fs = require('fs');
const APP_MANIFEST_FILE = process.env.APP_MANIFEST_FILE || "/extra/umbrel-app.yml";
const CUSTOM_DOTENV_FILE = process.env.CUSTOM_DOTENV_FILE || "/data/.env.app_proxy";
if(fs.existsSync(CUSTOM_DOTENV_FILE)) {
require('dotenv').config({
path: CUSTOM_DOTENV_FILE,
override: true
});
}
function readUmbrelAppManifest() {
try {
return yaml.load(fs.readFileSync(APP_MANIFEST_FILE, 'utf8'));
} catch (e) {
console.error("Failed to open app manifest file", e);
process.exit(0);
}
}
function readFromEnvOrTerminate(key) {
const value = process.env[key];
if(typeof(value) !== "string" || value.trim().length === 0) {
console.error(`The env. variable '${key}' is not set. Terminating...`);
process.exit(0);
}
return value;
}
function cleanHttpPaths(str) {
return str.split(/[, ]+/)
.map(path => path.trim())
.filter(path => path.length > 0);
}
module.exports = Object.freeze({
UMBREL_COOKIE_NAME: "UMBREL_SESSION",
LOG_LEVEL: process.env.LOG_LEVEL || "info",
PROXY_PORT: parseInt(process.env.PROXY_PORT) || 4000,
PROXY_TIMEOUT: parseInt(process.env.PROXY_TIMEOUT) || 0, // milliseconds or 0 for disabled
PROXY_AUTH_ADD: (typeof(process.env.PROXY_AUTH_ADD) === "string") ? (process.env.PROXY_AUTH_ADD === "true") : true,
PROXY_AUTH_WHITELIST: cleanHttpPaths(process.env.PROXY_AUTH_WHITELIST || ""),
PROXY_AUTH_BLACKLIST: cleanHttpPaths(process.env.PROXY_AUTH_BLACKLIST || ""),
PROXY_TRUST_UPSTREAM: (typeof(process.env.PROXY_TRUST_UPSTREAM) === "string") ? (process.env.PROXY_TRUST_UPSTREAM === "true") : false,
APP: readUmbrelAppManifest(),
APP_PROTOCOL: process.env.APP_PROTOCOL || "http",
APP_HOST: readFromEnvOrTerminate("APP_HOST"),
APP_PORT: parseInt(readFromEnvOrTerminate("APP_PORT")),
UMBREL_AUTH_HIDDEN_SERVICE_FILE: process.env.UMBREL_AUTH_HIDDEN_SERVICE_FILE || "/var/lib/tor/auth/hostname",
UMBREL_AUTH_SECRET: readFromEnvOrTerminate("UMBREL_AUTH_SECRET"),
UMBREL_AUTH_PORT: parseInt(process.env.UMBREL_AUTH_PORT) || 2000,
MANAGER_IP: readFromEnvOrTerminate("MANAGER_IP"),
MANAGER_PORT: parseInt(process.env.MANAGER_PORT) || 3006,
});
================================================
FILE: containers/app-proxy/utils/express.js
================================================
function removeCookie(req, cookieName) {
const allCookies = req.headers.cookie || "";
// Split on '; ' (where space is optional)
// More details re http cookie delimter:
// https://www.rfc-editor.org/rfc/rfc6265#section-4.2.1
const cookiePairs = allCookies.split(/; */g).filter(pair => pair.length > 0);
// Filter out cookie and re-join
// to build http cookie string
// (using cookie delimiter)
return cookiePairs.filter(pair => ! pair.startsWith(`${cookieName}=`)).join("; ");
}
module.exports = {
removeCookie
};
================================================
FILE: containers/app-proxy/utils/hmac.js
================================================
const crypto = require('crypto');
function sign(input, secret) {
return crypto
.createHmac('sha256', secret)
.update(input)
.digest('base64');
};
function verify(input, secret, signature){
const inputSignature = Buffer.from( sign(input, secret) );
const testSignature = Buffer.from( signature );
return inputSignature.length === testSignature.length && crypto.timingSafeEqual(inputSignature, testSignature);
};
module.exports = {
sign,
verify
};
================================================
FILE: containers/app-proxy/utils/manager.js
================================================
const axios = require('axios');
const package = require('../package.json');
const CONSTANTS = require('./const.js');
const axiosInstance = axios.create({
baseURL: `http://${CONSTANTS.MANAGER_IP}:${CONSTANTS.MANAGER_PORT}`,
headers: {
common: {
"User-Agent": `${package.name}/${package.version}`
}
}
});
const account = {
login: async function(body) {
return axiosInstance.post('/v1/account/login', body);
},
token: async function(token) {
return axiosInstance.get('/v1/account/token', {
params: {
token
}
});
}
};
module.exports = {
account
};
================================================
FILE: containers/app-proxy/utils/proxy.js
================================================
const { createProxyMiddleware } = require("http-proxy-middleware");
const { StatusCodes } = require("http-status-codes");
const url = require("url");
const expressUtils = require("./express.js");
const tokenUtils = require("./token.js");
const torUtils = require("./tor.js");
const CONSTANTS = require("./const.js");
const safeHandler = require("./safe_handler.js");
function onProxyReq(proxyReq, req, res, config) {
// "Value may be undefined if the socket is destroyed (for example, if the client disconnected)."
// More details here: https://nodejs.org/api/net.html#socketremoteaddress
if (req.socket.remoteAddress === undefined) {
return res.end();
}
// If we don't trust the upstream, we'll set the x-forwarded headers
// Upstream could be a proxy and therefore trusted
// So we'll accept the incoming x-forwarded headers
if (!CONSTANTS.PROXY_TRUST_UPSTREAM) {
proxyReq.setHeader("x-forwarded-proto", req.protocol);
proxyReq.setHeader("x-forwarded-host", req.headers.host);
proxyReq.setHeader("x-forwarded-for", req.socket.remoteAddress);
}
// Remove umbrel session cookie from proxied request
const cookies = expressUtils.removeCookie(req, CONSTANTS.UMBREL_COOKIE_NAME);
if (cookies.trim().length === 0) {
// "the user agent sends a Cookie request header to the origin server if it has cookies"
// More info: https://datatracker.ietf.org/doc/html/rfc2109#section-4.3.4
proxyReq.removeHeader("cookie");
} else {
proxyReq.setHeader("cookie", cookies);
}
}
function onError(err, req, res, target) {
// ENOTFOUND = The proxy could not reach the target (check APP_HOST and APP_PORT)
// ETIMEDOUT = The proxy could reach the target, but the target was too slow to respond (potentially PROXY_TIMEOUT is too low)
console.error(`Proxy error: ${err.message}`);
if (typeof res.status === "function") {
res.status(StatusCodes.BAD_GATEWAY).render("pages/error", {
app: CONSTANTS.APP,
err,
});
}
}
function proxy() {
const proxyTarget = `${CONSTANTS.APP_PROTOCOL}://${CONSTANTS.APP_HOST}:${CONSTANTS.APP_PORT}`;
const proxyConfig = {
onProxyReq: onProxyReq,
onError: onError,
target: proxyTarget,
// Don't change the origin
// Pass through the origin ('host' header) from the browser
changeOrigin: false,
// Add websocket support, but this option assumes that
// an initial http request is made before the websocket connection
ws: true,
// If this is true, this will chain the x-forwarded header values
// Many applications don't handle multiple header values (e.g. BTC Pay Server)
xfwd: false,
logLevel: CONSTANTS.LOG_LEVEL,
proxyTimeout: CONSTANTS.PROXY_TIMEOUT,
// The proxy shouldn't follow redirect
// The browser should, therefore this must be off
followRedirects: false,
};
return createProxyMiddleware(proxyConfig);
}
function whitelist() {
return function (req, res, next) {
req.ignoreAuth = true;
next();
};
}
function blacklist() {
return function (req, res, next) {
req.ignoreAuth = false;
next();
};
}
function apply(app) {
if (CONSTANTS.PROXY_AUTH_ADD) {
if (CONSTANTS.PROXY_AUTH_WHITELIST.length > 0)
app.use(CONSTANTS.PROXY_AUTH_WHITELIST, whitelist());
if (CONSTANTS.PROXY_AUTH_BLACKLIST.length > 0)
app.use(CONSTANTS.PROXY_AUTH_BLACKLIST, blacklist());
}
const middleware = proxy();
app.use(
safeHandler(async (req, res, next) => {
// If route is part of the auth whitelist
// Then we ignore handling auth
if (CONSTANTS.PROXY_AUTH_ADD && req.ignoreAuth !== true) {
const token = req.cookies.UMBREL_PROXY_TOKEN;
// token could be false if hmac fails (ie. someone tampered with the token)
if (typeof token !== "string" || !(await tokenUtils.validate(token))) {
const origin = req.hostname.endsWith(".onion") ? "tor" : "host";
// Get the raw query string
// This could be null if there is no query string
let query = url.parse(req.url).query;
if (typeof query == "string") {
query = `?${query}`;
} else {
query = "";
}
const searchParams = new URLSearchParams({
origin: origin,
app: CONSTANTS.APP.id,
path: `${req.path}${query}`,
});
// If request came over Tor
// Then redirect to auth HS hosted on Tor
if (origin === "tor") {
const authHsUrl = await torUtils.authHsUrl();
return res.redirect(
`${req.protocol}://${authHsUrl}/?${searchParams.toString()}`
);
} else {
return res.redirect(
`${req.protocol}://${req.hostname}:${
CONSTANTS.UMBREL_AUTH_PORT
}/?${searchParams.toString()}`
);
}
}
}
middleware(req, res, next);
})
);
return middleware;
}
module.exports = {
proxy,
whitelist,
blacklist,
apply,
};
================================================
FILE: containers/app-proxy/utils/safe_handler.js
================================================
// this safe handler is used to wrap our api methods
// so that we always fallback and return an exception if there is an error
// inside of an async function
// Mostly copied from vault/server/utils/safeHandler.js
function safeHandler(handler) {
return async (req, res, next) => {
try {
return await handler(req, res, next);
} catch (err) {
return next(err);
}
};
}
module.exports = safeHandler;
================================================
FILE: containers/app-proxy/utils/token.js
================================================
const jwt = require("jsonwebtoken");
const JWT_ALGORITHM = "HS256";
const secret = process.env.JWT_SECRET;
function validate(token) {
const payload = jwt.verify(token, secret, {
algorithms: [JWT_ALGORITHM],
});
return payload.proxyToken === true;
}
module.exports = {
validate,
};
================================================
FILE: containers/app-proxy/utils/tor.js
================================================
const fs = require("fs").promises;
const CONSTANTS = require("./const.js");
async function authHsUrl() {
// Here is technically a race condition
// As the auth hs url may not yet be generated
try {
return (
await fs.readFile(CONSTANTS.UMBREL_AUTH_HIDDEN_SERVICE_FILE, "utf-8")
).trim();
} catch (e) {
return "not-yet-generated.onion";
}
}
module.exports = {
authHsUrl,
};
================================================
FILE: containers/app-proxy/views/pages/error.ejs
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
body {
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
padding: 0px;
margin: 0px;
text-align: center;
}
.container {
margin-top: 200px;
}
.app-icon {
height: 100px;
border-radius: 20px;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
font-weight: 500;
}
.error-text {
color: #6c757d;
}
</style>
<title>Error</title>
</head>
<body>
<div class="container">
<div>
<!-- Umbrel logo -->
<svg width="93" height="105" viewBox="0 0 93 105" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.1162 105C48.85 105 50.9245 104.679 53.3395 104.037C61.4926 100.971 65.5692 95.0672 65.5692 86.3255V61.3121C65.5692 60.8357 65.5383 60.4214 65.4764 60.0692C65.4764 60.0071 65.4712 59.9657 65.4609 59.9449C65.4506 59.9242 65.4248 59.8828 65.3835 59.8207C64.5372 57.687 63.1955 56.6202 61.3585 56.6202H61.1108L60.5844 56.6513H60.3058C60.2439 56.6513 60.1716 56.6823 60.0891 56.7445C57.963 57.5731 56.9 59.3235 56.9 61.9957V87.6305C56.9 89.0184 56.5491 90.3752 55.8473 91.701C53.8658 94.8289 50.9245 96.3929 47.0233 96.3929C44.0717 96.3929 41.5741 95.4711 39.5306 93.6275C37.8587 92.2603 37.0021 89.7434 36.9608 86.0769L36.8989 60.8771C36.8989 60.6493 36.8783 60.411 36.837 60.1624C36.837 60.0796 36.8318 60.0226 36.8215 59.9916C36.8112 59.9605 36.7751 59.9035 36.7131 59.8207C35.8462 57.6456 34.4736 56.558 32.5953 56.558C31.9967 56.6202 31.5942 56.6927 31.3878 56.7756C29.3237 57.6042 28.2916 59.0024 28.2916 60.9703L28.3845 87.1023C28.4052 90.9967 29.4785 94.5079 31.6045 97.6358C35.5676 102.545 40.6969 105 46.9924 105H47.1162Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4517 40.4302C12.7214 40.4302 9.82339 41.8182 7.98048 44.0001C6.47085 45.7874 3.80334 46.0081 2.02243 44.493C0.241521 42.978 0.0216106 40.3009 1.53125 38.5136C5.39717 33.9367 10.963 31.9453 17.4517 31.9453C22.9046 31.9453 27.9024 33.335 32.3379 36.1353C36.9332 33.4016 41.7749 31.9453 46.8188 31.9453C51.7503 31.9453 56.4062 33.3384 60.7341 35.9888C64.5971 33.3787 69.2338 32.1792 74.4003 32.1792C80.8802 32.1792 86.5877 34.0623 91.2259 38.0272C93.0032 39.5465 93.2168 42.2241 91.7029 44.0078C90.189 45.7914 87.521 46.0058 85.7437 44.4865C82.8402 42.0045 79.158 40.6641 74.4003 40.6641C69.9059 40.6641 66.6755 41.8574 64.3151 43.9022C62.4688 45.5017 59.7715 45.6251 57.7877 44.2009L57.7131 44.1474C54.1394 41.6012 50.5342 40.4302 46.8188 40.4302C43.0364 40.4302 39.1958 41.6452 35.232 44.3366C33.371 45.6002 30.9173 45.5447 29.1149 44.1982L28.984 44.1004C25.6669 41.6634 21.8736 40.4302 17.4517 40.4302Z" fill="#5351FB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M70.262 16.392C64.097 11.0258 56.1491 8.29985 46.0535 8.49453C35.9105 8.69011 28.0333 11.6155 22.0552 16.9826C16.0265 22.3952 11.53 30.6505 8.87493 42.2101C8.35052 44.4932 6.08115 45.9174 3.80615 45.3912C1.53115 44.8649 0.112013 42.5874 0.636423 40.3042C3.55057 27.6167 8.70808 17.5811 16.4183 10.6589C24.179 3.69135 34.1255 0.238118 45.8911 0.0112437C57.7039 -0.216542 67.7956 3.01361 75.8015 9.9822C83.7379 16.8903 89.2264 27.1251 92.5785 40.2C93.1603 42.4691 91.799 44.7819 89.538 45.3657C87.277 45.9496 84.9724 44.5834 84.3907 42.3143C81.3097 30.2972 76.4964 21.8187 70.262 16.392Z" fill="#5351FB"/>
</svg>
<!-- Right caret -->
<svg width="100" height="100" viewBox="0 0 32 32" viewBox="0 0 32 32"><path d="M18.629 15.997l-7.083-7.081L13.462 7l8.997 8.997L13.457 25l-1.916-1.916z" fill="#555555" /></svg>
<!-- App icon -->
<img alt="<%= app.name %>" src="https://getumbrel.github.io/umbrel-apps-gallery/<%= app.id %>/icon.svg" class="app-icon">
</div>
<h1 class="text-center mt-5 mb-2">Oops, there was an error</h1>
<p class="text-muted w-75 text-center error-text">There was an error connecting to <%= app.name %>. Error code: <%= err.code %></p>
</div>
</body>
</html>
================================================
FILE: containers/tor/Dockerfile
================================================
# Based on https://github.com/lncm/docker-tor/tree/927ebac9fb43ba4d09249ee27688a4612b7a1707
FROM debian:11-slim AS build
ARG VERSION=0.4.7.8
# Add Tor keys
ENV KEYS 514102454D0A87DB0767A1EBBE6A0531C18A9179 B74417EDDF22AC9F9E90F49142E86A2A11F48D36 7A02B3521DC75C542BA015456AFEE6D49E92B601
RUN apt update && \
# Packages for verification
apt -y install gpg gpg-agent wget && \
# Packages for Tor runtime and compilation
apt -y install libevent-dev libssl-dev zlib1g-dev build-essential
# Download Tor source and checksum
RUN wget https://dist.torproject.org/tor-$VERSION.tar.gz.sha256sum.asc && \
wget https://dist.torproject.org/tor-$VERSION.tar.gz.sha256sum && \
wget https://dist.torproject.org/tor-$VERSION.tar.gz
# Verify source
RUN gpg --keyserver keyserver.ubuntu.com --recv-keys $KEYS && \
gpg --list-keys | tail -n +3 | tee /tmp/keys.txt && \
gpg --list-keys $KEYS | diff - /tmp/keys.txt && \
gpg --verify tor-$VERSION.tar.gz.sha256sum.asc && \
sha256sum -c tor-$VERSION.tar.gz.sha256sum
# Extract source
RUN tar -xzf "/tor-$VERSION.tar.gz"
WORKDIR /tor-$VERSION/
# Build Tor
RUN ./configure --sysconfdir=/etc --datadir=/var/lib && \
make -j$(nproc) && \
make install
FROM debian:11-slim
# Copy linked libraries
COPY --from=build /usr/lib /usr/lib
# Copy Tor binaries
COPY --from=build /usr/local/bin/tor* /usr/local/bin/
ENTRYPOINT ["tor"]
================================================
FILE: containers/tor/README.md
================================================
[](https://github.com/getumbrel/umbrel-tor)
[](https://github.com/getumbrel/umbrel-tor/actions?query=workflow%3A"Docker+build+on+push")
[](https://hub.docker.com/repository/registry-1.docker.io/getumbrel/tor/tags?page=1)
# ☂️ Tor
A simple Docker image for Tor
## 🛠 Build Tor Docker image
### Build
```sh
docker build -t getumbrel/tor .
```
### Run
```sh
docker run --rm -u 1000:1000 -e HOME=/tmp getumbrel/tor
```
================================================
FILE: containers/tor/test/.gitignore
================================================
data
================================================
FILE: containers/tor/test/docker-compose.entrypoint.yml
================================================
version: '3.7'
services:
web:
image: mendhak/http-https-echo
environment:
HTTP_PORT: 8888
tor:
image: getumbrel/tor
build: ..
user: 1000:1000
environment:
HOME: /tmp
HS_DIR: "web2"
HS_VIRTUAL_PORT: "80"
HS_HOST: "web"
HS_PORT: "8888"
entrypoint: /umbrel/entrypoint.sh
volumes:
- ./data:/data
- ./entrypoint.sh:/umbrel/entrypoint.sh
================================================
FILE: containers/tor/test/docker-compose.yml
================================================
version: '3.7'
services:
web:
image: mendhak/http-https-echo
environment:
HTTP_PORT: 8888
tor:
image: getumbrel/tor
build: ..
user: 1000:1000
environment:
HOME: /tmp
volumes:
- ./torrc:/etc/tor/torrc
- ./data:/data
================================================
FILE: containers/tor/test/entrypoint.sh
================================================
#!/bin/bash
TORRC_PATH="/tmp/torrc"
echo "HiddenServiceDir /data/${HS_DIR}" > "${TORRC_PATH}"
echo "HiddenServicePort ${HS_VIRTUAL_PORT} ${HS_HOST}:${HS_PORT}" >> "${TORRC_PATH}"
tor -f "${TORRC_PATH}"
================================================
FILE: containers/tor/test/test-entrypoint.sh
================================================
#!/bin/bash
docker-compose -f docker-compose.entrypoint.yml up --detach web
docker-compose -f docker-compose.entrypoint.yml up --detach tor
echo
echo "Hostname:"
cat ./data/web2/hostname
================================================
FILE: containers/tor/test/test.sh
================================================
#!/bin/bash
docker-compose up --detach web
docker-compose up --detach tor
echo
echo "Hostname:"
cat ./data/web/hostname
================================================
FILE: containers/tor/test/torrc
================================================
HiddenServiceDir /data/web
HiddenServicePort 80 web:8888
================================================
FILE: info.json
================================================
{
"NOTE": "We must keep this file here forever to allow old umbrelOS 0.5.x Umbrel Homes to find the 1.0.0 update and bootstrap themselves into the new mender based update system.",
"version": "1.0.0",
"name": "umbrelOS 1.0",
"requires": ">=0.5.3",
"notes": "umbrelOS 1.0 brings a ground-up rebuild with a host of new features, a completely revamped architecture, and a brand new interface.\n\nLearn more: https://link.umbrel.com/os\n\n- Raspberry Pi users: Please follow the instructions at https://link.umbrel.com/pi-update to update to umbrelOS 1.0.\n\n- Umbrel Home users: You're ready to update now by clicking the 'Install Now' button below.\n\n- Linux users: Your update is on the horizon and will be available in April 2024: https://link.umbrel.com/linux-update.\n\nNote: Updates may overwrite any custom CLI scripts, files, packages, or settings (eg. WiFi) that you've manually installed or modified via command-line (SSH).\n\nWhat's new:\n\n- New UI: A visually stunning environment with seamless navigation, intuitive interactions, and a home screen you can personalize with widgets\n\n- Search (⌘K / Ctrl + K): Instantly find system settings, apps, or initiate actions with a simple keyboard shortcut.\n\n- Quick Actions: Interact with apps and system settings faster than ever with right-click shortcuts.\n\n- Architectural Overhaul: Re-engineered system for better stability, speed, and simplicity. umbrelOS 1.0 now sends device type information during the update check to ensure customized updates for different devices.\n\n- Live Usage: Stay informed with live updates on your device's storage, memory, and CPU usage.\n\n- Multilingual Support: Choose from 8 different system languages."
}
================================================
FILE: package.json
================================================
{
"type": "module",
"scripts": {
"dev": "./scripts/umbrel-dev",
"dev:help": "npm run dev help",
"test:vm": "npm --prefix packages/os run build:amd64:rugix && rm -rf packages/os/rugix/build && npm --prefix packages/umbreld run test:vm --",
"build:remote": "./scripts/remote-builder build",
"test:remote": "./scripts/remote-builder test"
}
}
================================================
FILE: packages/os/.gitignore
================================================
build
vm-state
================================================
FILE: packages/os/README.md
================================================
# umbrelOS
## Build Process
The following diagram visualizes the build process and various build artifacts:
```mermaid
graph TD
subgraph "Docker"
root-amd64
root-arm64
end
subgraph "Rugix (Pi)"
pi --> pi4
pi --> pi-tryboot
pi --> pi-mbr
end
subgraph "Rugix (AMD64)"
amd64
mender-amd64
end
root-amd64 --> amd64
root-amd64 --> mender-amd64
root-arm64 --> pi
amd64 --> amd64-img([amd64.img])
amd64 --> amd64-rugixb([amd64.rugixb])
mender-amd64 --> mender-amd64-mender([mender-amd64.mender])
mender-amd64 --> mender-amd64-rugixb([mender-amd64.rugixb])
pi-mbr --> pi-mbr-mender([pi.mender])
pi4 --> pi4-img([pi4.img])
pi-tryboot --> pi-tryboot-img([pi5.img])
pi-tryboot --> pi-tryboot-rugixb([pi.rugixb])
```
### OS Variants
There are three main umbrelOS *variants*:
- `umbrelos-pi`: Rugix-native umbrelOS for Raspberry Pi.
- `umbrelos-amd64`: Rugix-native umbrelOS for AMD64.
- `umbrelos-mender-amd64`: Rugix-based but Mender-compatible umbrelOS for AMD64.
> [!IMPORTANT]
> **Legacy devices provisioned with Mender require the `umbrelos-mender-amd64` variant.** This variant includes a Rugix configuration to enable a safe migration from Mender to Rugix. Rugix will interface with GRUB in the same way Mender does to facilitate A/B switching. In addition, a Rugix hook is installed to migrate state into Rugix's state management mechanism. This enables factory resets and all other Rugix state management features.
### Build Artifacts
The build process produces the following artifacts.
#### Images
Images for provisioning new umbrelOS devices.
- `umbrelos-amd64.img`: Image for provisioning AMD64 devices and VMs.
- `umbrelos-pi4.img`: Image for provisioning Raspberry Pi 4 devices (GPT-based).
- `umbrelos-pi5.img`: Image for provisioning Raspberry Pi 5 devices (GPT-based).
The image for Raspberry Pi 4 includes a firmware update to enable the `tryboot` mechanism used for A/B switching. Otherwise, the image is identical to the image for Raspberry Pi 5.
#### Mender Update Artifacts
Mender update artifacts for updating existing systems through Mender:
- `umbrelos-pi.mender`: Mender update artifact for Raspberry Pi.
- `umbrelos-mender-amd64.mender`: Mender update artifact for AMD64 devices.
The update artifact for Raspberry Pi works for Raspberry Pi 4 and 5.
#### Rugix Update Bundles
For each of the umbrelOS variants, there is a respective Rugix update bundle:
- `umbrelos-pi.rugixb`: Rugix update bundle for Raspberry Pi.
- `umbrelos-amd64.rugixb`: Rugix update bundle for Rugix-native AMD64 devices.
- `umbrelos-mender-amd64.rugixb`: Rugix update bundle for legacy Mender devices.
## Mender to Rugix Migration
Devices can be migrated to Rugix by installing the respective Mender update artifact.
### Raspberry Pi
As umbrelOS for Raspberry Pi has always been based on Rugix, no special migration is needed.
### AMD64
In case of Mender-based AMD64 devices, the directory structure on the data partition must be migrated such that Rugix's state management can be used.
This migration is performed by a `boot/post-init` Rugix hook: [`10-migrate-state.sh`](./rugix/recipes/setup-rugix-mender/files/migrate-state.sh).
This hook works as follows: Initially, a symlink `/data/umbrel-os` is placed in the `/data` directory managed by Rugix linking back to the bare data partition.
This way, after booting, all the bind mounts to `/data/umbrel-os` are set up correctly.
The migration hook replaces this symlink atomically with the directory from the data partition, **but only after the system has been committed**.
This is done to leave the state intact, in case there is a rollback.
Note that the system needs to be rebooted once after the commit to trigger the migration.
To this end, after committing, `umbreld` may check whether `/data/umbrel-os` is a symlink and reboot the system if it is.
## Factory Resets
Factory resets can be triggered through Rugix Ctrl's state management mechanism as usual.
To trigger a factory reset, run `rugix-ctrl state reset`.
This will reboot the system and remove the state on the data partition in the process.
In case of a RAID configuration, a [`state-reset/prepare` hook](./rugix/recipes/setup-rugix/files/factory-reset.sh) is used to remove the RAID configuration and reset the main data partition.
Note that Rugix only supports resetting the state on the actively used data partition (which is the RAID, if a RAID has been configured), hence, we need to use the hook here to make sure that the main data partition is wiped as well.
Removing state can take some time during the boot process.
It is therefore recommended to use Rugix's state reset mechanism with the `--backup` flag.
This will simply rename the old state directory according to the following pattern:
```
/run/rugix/mounts/data/state/default -> /run/rugix/mounts/data/state/default.XXXXXXXXXXXXXX
```
Here, `XXXXXXXXXXXXXX` is the date and time of the reset.
The residual state can then removed after booting by `umbreld`.
================================================
FILE: packages/os/build-steps/initialize.sh
================================================
#!/bin/bash
set -euo pipefail
SNAPSHOT_DATE=${1:-}
# Disable the resume from SWAP functionality. This takes significant time during the boot
# process, as it tries to resume using a swap device which never shows up.
mkdir -p /etc/initramfs-tools/conf.d/
echo "RESUME=none" >/etc/initramfs-tools/conf.d/resume
rm /etc/apt/sources.list.d/debian.sources
# All apt packages are pinned to a specific date to ensure reproducibility.
# This means building the same umbrelOS git tag always results in the same
# package versions.
# We should update this to the current date with each release to ensure we
# are always using the latest packages.
cat >/etc/apt/sources.list <<EOF
deb http://snapshot.debian.org/archive/debian/${SNAPSHOT_DATE} trixie main contrib non-free-firmware
deb-src http://snapshot.debian.org/archive/debian/${SNAPSHOT_DATE} trixie main contrib non-free-firmware
deb http://snapshot.debian.org/archive/debian-security/${SNAPSHOT_DATE} trixie-security main contrib non-free-firmware
deb-src http://snapshot.debian.org/archive/debian-security/${SNAPSHOT_DATE} trixie-security main contrib non-free-firmware
deb http://snapshot.debian.org/archive/debian/${SNAPSHOT_DATE} trixie-updates main contrib non-free-firmware
deb-src http://snapshot.debian.org/archive/debian/${SNAPSHOT_DATE} trixie-updates main contrib non-free-firmware
EOF
# This is also needed to avoid issues with apt refusing to install old packages from snapshot repos.
echo 'Acquire::Check-Valid-Until "false";' | tee /etc/apt/apt.conf.d/90snapshot-validuntil
apt-get update --yes
# Install systemd
#
# We do this here as the Rasperry Pi setup requires Systemd. Without it, it will not
# realize that it runs inside Docker and complain about missing mountpoints during
# the installation.
apt-get install --yes systemd-sysv
================================================
FILE: packages/os/build-steps/setup-raspberrypi/cmdline.txt
================================================
console=serial0,115200 console=tty1 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=memory swapaccount=1 loglevel=3 usb-storage.quirks=152d:1561:u,152d:1576:u,152d:0578:u,125f:a76a:u,04e8:61b6:u
================================================
FILE: packages/os/build-steps/setup-raspberrypi/config.txt
================================================
# Enable DRM VC4 V3D driver.
#
# MX: This has been enabled by default and is required for 3D graphics
# hardware acceleration. We just leave it enabled.
dtoverlay=vc4-kms-v3d
max_framebuffers=2
# We want to run the processor in its 64-bit mode.
arm_64bit=1
# Enable NVMe interface
# This is not needed if booting with NVMe so potentially
# we only want to support that and don't need this.
dtparam=nvme
# This may improve NVMe performance but is not stable:
# > The Raspberry Pi 5 is not certified for Gen 3.0 speeds. PCIe Gen 3.0 connections may be unstable.
# - https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#pcie-gen-3-0
# dtparam=pciex1_gen=3
================================================
FILE: packages/os/build-steps/setup-raspberrypi/raspberrypi.gpg.key
================================================
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)
mQENBE/d7o8BCACrwqQacGJfn3tnMzGui6mv2lLxYbsOuy/+U4rqMmGEuo3h9m92
30E2EtypsoWczkBretzLUCFv+VUOxaA6sV9+puTqYGhhQZFuKUWcG7orf7QbZRuu
TxsEUepW5lg7MExmAu1JJzqM0kMQX8fVyWVDkjchZ/is4q3BPOUCJbUJOsE+kK/6
8kW6nWdhwSAjfDh06bA5wvoXNjYoDdnSZyVdcYCPEJXEg5jfF/+nmiFKMZBraHwn
eQsepr7rBXxNcEvDlSOPal11fg90KXpy7Umre1UcAZYJdQeWcHu7X5uoJx/MG5J8
ic6CwYmDaShIFa92f8qmFcna05+lppk76fsnABEBAAG0IFJhc3BiZXJyeSBQaSBB
cmNoaXZlIFNpZ25pbmcgS2V5iQE4BBMBAgAiBQJP3e6PAhsDBgsJCAcDAgYVCAIJ
CgsEFgIDAQIeAQIXgAAKCRCCsSmSf6MwPk6vB/9pePB3IukU9WC9Bammh3mpQTvL
OifbkzHkmAYxzjfK6D2I8pT0xMxy949+ThzJ7uL60p6T/32ED9DR3LHIMXZvKtuc
mQnSiNDX03E2p7lIP/htoxW2hDP2n8cdlNdt0M9IjaWBppsbO7IrDppG2B1aRLni
uD7v8bHRL2mKTtIDLX42Enl8aLAkJYgNWpZyPkDyOqamjijarIWjGEPCkaURF7g4
d44HvYhpbLMOrz1m6N5Bzoa5+nq3lmifeiWKxioFXU+Hy5bhtAM6ljVb59hbD2ra
X4+3LXC9oox2flmQnyqwoyfZqVgSQa0B41qEQo8t1bz6Q1Ti7fbMLThmbRHiuQEN
BE/d7o8BCADNlVtBZU63fm79SjHh5AEKFs0C3kwa0mOhp9oas/haDggmhiXdzeD3
49JWz9ZTx+vlTq0s+I+nIR1a+q+GL+hxYt4HhxoA6vlDMegVfvZKzqTX9Nr2VqQa
S4Kz3W5ULv81tw3WowK6i0L7pqDmvDqgm73mMbbxfHD0SyTt8+fk7qX6Ag2pZ4a9
ZdJGxvASkh0McGpbYJhk1WYD+eh4fqH3IaeJi6xtNoRdc5YXuzILnp+KaJyPE5CR
qUY5JibOD3qR7zDjP0ueP93jLqmoKltCdN5+yYEExtSwz5lXniiYOJp8LWFCgv5h
m8aYXkcJS1xVV9Ltno23YvX5edw9QY4hABEBAAGJAR8EGAECAAkFAk/d7o8CGwwA
CgkQgrEpkn+jMD5Figf/dIC1qtDMTbu5IsI5uZPX63xydaExQNYf98cq5H2fWF6O
yVR7ERzA2w33hI0yZQrqO6pU9SRnHRxCFvGv6y+mXXXMRcmjZG7GiD6tQWeN/3wb
EbAn5cg6CJ/Lk/BI4iRRfBX07LbYULCohlGkwBOkRo10T+Ld4vCCnBftCh5x2OtZ
TOWRULxP36y2PLGVNF+q9pho98qx+RIxvpofQM/842ZycjPJvzgVQsW4LT91KYAE
4TVf6JjwUM6HZDoiNcX6d7zOhNfQihXTsniZZ6rky287htsWVDNkqOi5T3oTxWUo
m++/7s3K3L0zWopdhMVcgg6Nt9gcjzqN1c0gy55L/g==
=mNSj
-----END PGP PUBLIC KEY BLOCK-----
================================================
FILE: packages/os/build-steps/setup-raspberrypi/raspberrypi.list
================================================
deb http://archive.raspberrypi.com/debian/ RELEASE main
# Uncomment line below then 'apt-get update' to enable 'apt-get source'
#deb-src http://archive.raspberrypi.com/debian/ RELEASE main
================================================
FILE: packages/os/build-steps/setup-raspberrypi.sh
================================================
#!/bin/bash
set -euo pipefail
# Install GPG (required for dearmoring the key).
apt-get install -y gpg
# Remove any existing firmware and kernels.
rm -rf /boot/firmware
mkdir -p /boot/firmware
# Configure APT with Raspberry Pi sources.
install -m 644 "/build-steps/setup-raspberrypi/raspberrypi.list" "/etc/apt/sources.list.d/"
sed -i "s/RELEASE/trixie/g" "/etc/apt/sources.list.d/raspberrypi.list"
gpg --dearmor \
< "/build-steps/setup-raspberrypi/raspberrypi.gpg.key" \
> "/etc/apt/trusted.gpg.d/raspberrypi-archive-stable.gpg"
chmod 644 "/etc/apt/trusted.gpg.d/raspberrypi-archive-stable.gpg"
apt-get update -y
apt-get install -y raspberrypi-archive-keyring
# Install kernel and firmware for Pi 4 and 5.
apt-get install -y \
initramfs-tools \
raspi-firmware \
firmware-brcm80211 \
linux-image-rpi-v8 \
linux-headers-rpi-v8 \
linux-image-rpi-2712 \
linux-headers-rpi-2712
# Install boot configuration files.
install -m 644 "/build-steps/setup-raspberrypi/cmdline.txt" "/boot/firmware/"
install -m 644 "/build-steps/setup-raspberrypi/config.txt" "/boot/firmware/"
================================================
FILE: packages/os/build.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
# Pin the Rugix Docker image.
export RUGIX_BAKERY_IMAGE="ghcr.io/silitics/rugix-bakery@sha256:1abcf7791548aa441c06a8bc9b97acf170356cca8cd269b3fa8df4cc762d0251" # v0.8.15 + progress reporting for local delta hits (git-0c91222)
# export RUGIX_VERSION="branch-main"
# export RUGIX_DEV=true
# Allow running from anywhere
cd "$(dirname $(readlink -f "${BASH_SOURCE[0]}"))"
docker_buildx() {
docker buildx build --load $@
}
mender_artifact() {
docker run --rm -v "$(pwd):/data" umbrelos:builder /usr/bin/mender-artifact "$@"
}
# Run a command with sudo only in GitHub Actions
# These commands fail in GHA without sudo but they aren't needed locally and it's
# annoying for the script to get blocked and be prompted.
maybe_sudo() {
if [ "${GITHUB_ACTIONS:-}" = "true" ]; then
sudo "$@"
else
"$@"
fi
}
# Main entrypoint.
function main() {
release="${1:-}"
dev="false"
if [[ "${release}" == "" ]]
then
local git_hash
git_hash="$(git rev-parse --short HEAD 2>/dev/null || echo "dev")"
release="${git_hash}-$(date +%s)"
dev="true"
fi
# Enable QEMU/binfmt-based multi-platform support for building arm64 on
# amd64 or vice versa, e.g., to build for Pi 5 on an x86 system.
docker run --privileged --rm tonistiigi/binfmt --install all
if [ -z "${SKIP_ROOTS:-}" ]; then
if [ -z "${SKIP_ARM64:-}" ]; then
build_root_fs arm64 "${release}"
fi
if [ -z "${SKIP_AMD64:-}" ]; then
build_root_fs amd64 "${release}"
fi
fi
if [ -z "${SKIP_RUGIX_ARTIFACTS:-}" ]; then
build_rugix_artifacts "${release}" "${dev}"
fi
if [ -z "${SKIP_MENDER_ARTIFACTS:-}" ]; then
docker_buildx \
--platform "linux/amd64" \
--cache-from type=gha,scope=builder \
--cache-to type=gha,mode=max,scope=builder \
--file builder.Dockerfile \
--tag umbrelos:builder \
.
build_mender_artifacts "${release}"
fi
# Rename artifacts
# TODO: Maybe do this a cleaner way
# *.update are the new rugix native artifacts
# *-legacy.update are rugix update artifacts for legacy mender formatted devices
# *-legacy-migration.update are mender update artifacts to allow mender based update
# systems to migrate to the new rugix based update system.
mv build/umbrelos-amd64.rugixb build/umbrelos-amd64.update 2>/dev/null || true
mv build/umbrelos-mender-amd64.mender build/umbrelos-amd64-legacy-migration.update 2>/dev/null || true
mv build/umbrelos-mender-amd64.rugixb build/umbrelos-amd64-legacy.update 2>/dev/null || true
mv build/umbrelos-pi.mender build/umbrelos-pi-legacy-migration.update 2>/dev/null || true
mv build/umbrelos-pi.rugixb build/umbrelos-pi.update 2>/dev/null || true
# To boot from QEMU
# qemu-system-x86_64 -net nic -net user,hostfwd=tcp::2222-:22 -machine accel=tcg -cpu max -smp 4 -m 8192 -hda build/umbrelos.img -bios OVMF.fd
}
# Build the root filesystem.
#
# Arguments: <arch> <release>
function build_root_fs() {
local arch=$1;
local release=$2;
echo "Ensuring the build dir exists..."
mkdir -p build
# Ensure that the overlay directory exists.
mkdir -p "overlay-${arch}"
echo "Building Umbrel OS Docker image..."
# Note that we run the build context in ../../ so the build process has access to the
# entire repo to copy in umbreld stuff.
docker_buildx \
--cache-from type=gha,scope=umbrelos-${arch} \
--cache-to type=gha,mode=max,scope=umbrelos-${arch} \
--platform "linux/${arch}" \
--file umbrelos.Dockerfile \
--tag "umbrelos-${arch}" \
../../
echo "Dumping Umbrel OS Docker image filesystem into a tar archive..."
umbrel_os_container_id=$(docker run --platform "linux/${arch}" --detach "umbrelos-${arch}" /bin/true)
docker export --output "build/umbrelos-root-${arch}.tar" "${umbrel_os_container_id}"
docker rm "${umbrel_os_container_id}"
}
# Build the Rugix artifacts.
#
# Arguments: <release> <dev>
function build_rugix_artifacts() {
local release="$1"
local dev="$2"
# Make sure that the Rugix build directory exists.
mkdir -p rugix/build/umbrelos-root
# Copy the root filesystems previously build with Docker.
cp build/*.tar rugix/build/umbrelos-root
# Copy `/etc/hostname` and `/etc/hosts` such that Rugix can fix them.
cp overlay-common/etc/{hostname,hosts} rugix/recipes/fix-overlay/files
local compression="compression = { type = \"xz\", level = 9 }"
if [ "$dev" == "true" ]; then
compression=""
fi
pushd rugix
# Clean Rugix cache to force a clean build.
rm -rf .rugix || true
if [ -z "${SKIP_ARM64:-}" ] && [ -z "${SKIP_PI4:-}" ]; then
build_rugix_system "umbrelos-pi4" "$release" "$dev"
maybe_sudo mv -f "build/umbrelos-pi4/system.img" "../build/umbrelos-pi4.img"
fi
if [ -z "${SKIP_ARM64:-}" ] && [ -z "${SKIP_PI_TRYBOOT:-}" ]; then
build_rugix_system "umbrelos-pi-tryboot" "$release" "$dev"
maybe_sudo mv -f "build/umbrelos-pi-tryboot/system.img" "../build/umbrelos-pi5.img"
maybe_sudo mv -f "build/umbrelos-pi-tryboot/system.rugixb" "../build/umbrelos-pi.rugixb"
fi
if [ -z "${SKIP_ARM64:-}" ] && [ -z "${SKIP_PI_MBR:-}" ]; then
build_rugix_system "umbrelos-pi-mbr" "$release" "$dev"
# Truncate the image to the end of the last partition. This is required for
# compatibility with the legacy Mender-Rugpi update module.
popd
docker_buildx \
--platform "linux/amd64" \
--cache-from type=gha,scope=builder \
--cache-to type=gha,mode=max,scope=builder \
--file builder.Dockerfile \
--tag umbrelos:builder \
.
docker run --rm -v "$(pwd)/rugix:/data" umbrelos:builder /data/fix-umbrelos-pi-mbr.sh
pushd rugix
fi
if [ -z "${SKIP_AMD64:-}" ] && [ -z "${SKIP_AMD64_RUGIX:-}" ]; then
build_rugix_system "umbrelos-amd64" "$release" "$dev"
maybe_sudo mv -f "build/umbrelos-amd64/system.img" "../build/umbrelos-amd64.img"
maybe_sudo mv -f "build/umbrelos-amd64/system.rugixb" "../build/umbrelos-amd64.rugixb"
fi
if [ -z "${SKIP_AMD64:-}" ] && [ -z "${SKIP_AMD64_MENDER:-}" ]; then
./run-bakery bake image --release-version "$release" "umbrelos-mender-amd64"
maybe_sudo mkdir -p build/umbrelos-mender-amd64/bundle
maybe_sudo ln -s ../filesystems build/umbrelos-mender-amd64/bundle/payloads
cat <<EOF | maybe_sudo tee build/umbrelos-mender-amd64/bundle/rugix-bundle.toml > /dev/null
update-type = "full"
hash-algorithm = "sha512-256"
[[payloads]]
filename = "partition-1.img"
[payloads.delivery]
type = "slot"
slot = "system"
[payloads.block-encoding]
hash-algorithm = "sha512-256"
chunker = "casync-64"
$compression
deduplication = true
EOF
./run-bakery bundler bundle build/umbrelos-mender-amd64/bundle build/umbrelos-mender-amd64/system.rugixb
maybe_sudo mv -f "build/umbrelos-mender-amd64/system.rugixb" "../build/umbrelos-mender-amd64.rugixb"
fi
popd
}
# Build the image and update bundle for a given system.
#
# Arguments: <system> <release> <dev>
function build_rugix_system() {
local system="$1"
local release="$2"
local dev="$3"
local compression=""
if [ "$dev" == "true" ]; then
compression="--without-compression"
fi
./run-bakery bake bundle --release-version "$release" $compression "$system"
}
# Build the Mender update artifacts.
#
# Arguments: <release>
function build_mender_artifacts() {
local release="$1"
if [ -z "${SKIP_ARM64:-}" ] && [ -z "${SKIP_PI:-}" ]; then
if [ ! -e "rugix/build/umbrelos-pi-mbr/system.img" ]; then
echo "'umbrelos-pi-mbr' image is required to build Raspberry Pi Mender artifact."
exit 1
fi
echo "Build Raspberry Pi Mender artifact..."
mender_artifact write module-image \
--artifact-name "${release}" \
-t raspberrypi \
-T rugpi-image \
-f /data/rugix/build/umbrelos-pi-mbr/system.img \
-o /data/build/umbrelos-pi.mender
fi
if [ -z "${SKIP_AMD64:-}" ] && [ -z "${SKIP_AMD64_MENDER:-}" ]; then
if [ ! -e "rugix/build/umbrelos-mender-amd64/filesystems/partition-1.img" ]; then
echo "'umbrelos-mender-amd64' image is required to build AMD64 Mender artifact."
exit 1
fi
echo "Build AMD64 Mender artifact..."
mender_artifact write rootfs-image \
--artifact-name "${release}" \
-t amd64 \
-f /data/rugix/build/umbrelos-mender-amd64/filesystems/partition-1.img \
-o /data/build/umbrelos-mender-amd64.mender
fi
}
main "$@"
================================================
FILE: packages/os/builder.Dockerfile
================================================
FROM debian:bullseye
RUN apt-get -y update
# Install os image builder deps
RUN apt-get -y install fdisk gdisk qemu-utils dosfstools tree
# Install mender-convert
RUN apt-get -y install git
RUN git clone -b 4.0.1 https://github.com/mendersoftware/mender-convert.git /mender
RUN apt-get install -y sudo gdisk $(cat /mender/requirements-deb.txt)
RUN wget -q -O /usr/bin/mender-artifact https://downloads.mender.io/mender-artifact/3.10.0/linux/mender-artifact
RUN chmod +x /usr/bin/mender-artifact
================================================
FILE: packages/os/mender.cfg
================================================
DEPLOY_IMAGE_NAME="umbrelos"
MENDER_DEVICE_TYPE="amd64"
# This gives us:
# - 200Mb EFI
# - 2x ~10GB OS partitions
# - 512MB data partition (so that mkfs.ext4 can fit a 128MB journal contiguosly in one flex-group)
# We'll expand the data partition to consume 100% of the block device on first boot.
MENDER_STORAGE_TOTAL_SIZE_MB="$((1024 * 20))"
MENDER_BOOT_PART_SIZE_MB="200"
MENDER_DATA_PART_SIZE_MB="512"
# We don't want the Mender systemd cloud service.
MENDER_ENABLE_SYSTEMD="n"
# We don't want to let mender-convert inject mender-client
# since it appears to be broken on bookworm. Instead we
# install via apt in the image.
MENDER_CLIENT_INSTALL="n"
MENDER_CLIENT_VERSION="3.4.0"
# Rreasonably fast and offers good compression
MENDER_ARTIFACT_COMPRESSION="gzip"
# If we don't disable this the image is unbootable
MENDER_COPY_BOOT_GAP="n"
# We'll disable this and implement our own expansion logic.
# This successfulyl expands the filesystem to 100% of the partition
# but it doesn't expand the partition to 100% of the block device.
MENDER_DATA_PART_GROWFS="n"
MENDER_DATA_PART_FSTAB_OPTS="${MENDER_DATA_PART_FSTAB_OPTS},x-systemd.growfs"
MENDER_ROOT_PART_FSTAB_OPTS="${MENDER_ROOT_PART_FSTAB_OPTS},x-systemd.growfs"
# mkfs.ext4 options for the initial 128 MB Mender data partition
# We later grow this partition to 100% of the block device, so we must override
# mkfs.ext4’s size-based heuristics; otherwise it will default to values that are
# fine for a small 128 MB filesystem, but sub-optimal once the partition is expanded to
# real-world sized drives.
#
# -b 4096: 4 KB blocks. This would default to 1KB for a 128MB partition.
# -I 256: 256-byte inodes. This would default to 128 bytes for a 128MB partition.
# -i 65536: 1 inode per 64 KB of data → On a 1.8TB partition this is ~30 million inodes,
# which take up ~7 GB for inode tables.
# -m 0: Reserve 0 % of blocks for root processes. Custom -m percentage will not scale correctly
# during fs expansion, so to set a custom reserve we would need to set this via tune2fs after expansion.
# -J size=128: Creates a 128MB journal. We set the initial image partition size to 512MB to ensure a contiguous 128MB of free space in one flex-group for the journal.
# -O ...: Force-enable all ext4 features we rely on, even if some would already be on by default.
MENDER_DATA_PART_MKFS_OPTS="-b 4096 -I 256 -i 65536 -m 0 \
-J size=128 \
-O extent,flex_bg,sparse_super,large_file,huge_file,dir_index,filetype,64bit,metadata_csum,large_dir,extra_isize,has_journal \
-L data"
# Dealing with partition UUIDs instead of device paths
# allows us to produce a single image that is bootable
# on both QEMU (/dev/sda) and Umbrel Home (/dev/nvme0n1p).
MENDER_ENABLE_PARTUUID="y"
MENDER_BOOT_PART="/dev/disk/by-partuuid/14a31e9d-a8d7-4da0-9eb2-f268dd9d7ad9"
MENDER_ROOTFS_PART_A="/dev/disk/by-partuuid/2fe5a278-9b55-4266-8220-6665aa96940b"
MENDER_ROOTFS_PART_B="/dev/disk/by-partuuid/f5e6d27c-4a25-447b-8e08-a9d2e738345a"
MENDER_DATA_PART="/dev/disk/by-partuuid/d1d36e34-2753-4dc7-96eb-3c9b5584e867"
# Reduce noise on TTY
MENDER_GRUB_KERNEL_BOOT_ARGS="loglevel=3"
# Don't disable this for now since it's much faster since
# changing to gzip
# # Speed up development builds
# if [[ "${UMBREL_OS_DEV_BUILD}" == "true" ]]
# then
# MENDER_ARTIFACT_COMPRESSION="none"
# fi
================================================
FILE: packages/os/overlay-amd64/umbrelOS
================================================
================================================
FILE: packages/os/overlay-arm64/etc/systemd/system/umbrel-external-storage.service
================================================
# Umbrel External Storage Mounter
# Installed at /etc/systemd/system/umbrel-external-storage.service
[Unit]
Description=External Storage Mounter
Before=docker.service umbrel.service
[Service]
Type=oneshot
Restart=no
ExecStart=/opt/umbrel-external-storage/umbrel-external-storage
TimeoutStartSec=45min
User=root
Group=root
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=external storage mounter
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
================================================
FILE: packages/os/overlay-arm64/opt/umbrel-external-storage/umbrel-external-storage
================================================
#!/usr/bin/env bash
set -euo pipefail
# This script will:
# - Look for external storage devices
# - Check if they contain an Umbrel install
# - If yes
# - - Mount it
# - If no
# - - Format it
# - - Mount it
# - - Install Umbrel on it
# - Bind mount the external installation on top of the local installation
UMBREL_ROOT="/home/umbrel/umbrel"
MOUNT_POINT="/mnt/data"
EXTERNAL_UMBREL_ROOT="${MOUNT_POINT}/umbrel"
DOCKER_DIR="/var/lib/docker"
EXTERNAL_DOCKER_DIR="${MOUNT_POINT}/docker"
SWAP_DIR="/swap"
SWAP_FILE="${SWAP_DIR}/swapfile"
check_root () {
if [[ $UID != 0 ]]; then
echo "This script must be run as root"
exit 1
fi
}
check_dependencies () {
for cmd in "$@"; do
if ! command -v $cmd >/dev/null 2>&1; then
echo "This script requires \"${cmd}\" to be installed"
exit 1
fi
done
}
running_off_sdcard() {
if df -h | grep --silent /dev/mmcblk0
then
echo "true"
else
echo "false"
fi
}
# Returns a list of block device paths
list_block_devices () {
# We need to run sync here to make sure the filesystem is reflecting the
# the latest changes in /sys/block/sd*
sync
# We use "2>/dev/null || true" to swallow errors if there are
# no block devices. In that case the function just returns nothing
# instead of an error which is what we want.
#
# sed 's!.*/!!' is to return the device path so we get sda
# instead of /sys/block/sda
(ls -d /sys/block/sd* /sys/block/nvme0n* 2>/dev/null || true) | sed 's!.*/!!'
}
# Returns the vendor and model name of a block device
get_block_device_model () {
device="${1}"
if [[ "${device}" == nvme* ]]; then
vendor=$(cat "/sys/block/${device}/device/device/vendor")
else
vendor=$(cat "/sys/block/${device}/device/vendor")
fi
model=$(cat "/sys/block/${device}/device/model")
# We echo in a subshell without quotes to strip surrounding whitespace
echo "$(echo $vendor) $(echo $model)"
}
# Returns the path of the first partition of a block device
get_first_partition_path () {
device_path="${1}"
if [[ "${device_path}" == /dev/nvme* ]]; then
echo "${device_path}p1"
else
echo "${device_path}1"
fi
}
# Check if a block device contains partition layout that looks like
# an umbrelOS install
is_device_umbrelos_install () {
device_path="${1}"
# Having a 7th partition with the label "data" is a good indicator
# this is an umbrelOS install
if blkid | grep "${device_path}.*7: " | grep --silent 'LABEL="data"'
then
echo "true"
else
echo "false"
fi
}
is_partition_ext4 () {
partition_path="${1}"
# We need to run sync here to make sure the filesystem is reflecting the
# the latest changes in /dev/*
sync
blkid -o value -s TYPE "${partition_path}" | grep --quiet '^ext4$'
}
# Wipes a block device and reformats it with a single EXT4 partition
format_block_device () {
device="${1}"
device_path="/dev/${device}"
partition_path=$(get_first_partition_path "${device_path}")
wipefs -a "${device_path}"
parted --script "${device_path}" mklabel gpt
parted --script "${device_path}" mkpart primary ext4 0% 100%
# We need to run sync here to make sure the filesystem is reflecting the
# the latest changes in /dev/*
sync
mkfs.ext4 -F -L umbrel "${partition_path}"
}
# Mounts the device given in the first argument at $MOUNT_POINT
mount_partition () {
partition_path="${1}"
mkdir -p "${MOUNT_POINT}"
mount "${partition_path}" "${MOUNT_POINT}"
}
# Unmounts $MOUNT_POINT
unmount_partition () {
umount "${MOUNT_POINT}"
}
# Formats and sets up a new device
setup_new_device () {
block_device="${1}"
partition_path="${2}"
echo "Formatting device..."
format_block_device $block_device
echo "Mounting partition..."
mount_partition "${partition_path}"
echo "Creating Umbrel data directory on external storage..."
mkdir -p "${EXTERNAL_UMBREL_ROOT}"
echo "Touching dotfile on external storage so we recognise this device in the future..."
touch "${EXTERNAL_UMBREL_ROOT}"/.umbrel
}
# Copy Docker data dir to external storage
copy_docker_to_external_storage () {
mkdir -p "${EXTERNAL_DOCKER_DIR}"
cp --recursive \
--archive \
--no-target-directory \
"${DOCKER_DIR}" "${EXTERNAL_DOCKER_DIR}"
}
main () {
echo "Running external storage mount script..."
check_root
check_dependencies sed wipefs parted mount sync umount
if [[ "$(running_off_sdcard)" == "false" ]]
then
echo "This script should only run when umbrelOS boots from an SD card, exiting..."
exit
fi
no_of_block_devices=$(list_block_devices | wc -l)
retry_for_block_devices=1
while [[ $no_of_block_devices -lt 1 ]]; do
echo "No block devices found"
echo "Waiting for 5 seconds before checking again..."
sleep 5
no_of_block_devices=$(list_block_devices | wc -l)
retry_for_block_devices=$(( $retry_for_block_devices + 1 ))
if [[ $retry_for_block_devices -gt 20 ]]; then
echo "No block devices found in 20 tries..."
echo "Exiting mount script without doing anything"
exit 1
fi
done
if [[ $no_of_block_devices -gt 1 ]]; then
echo "Multiple block devices found, only one drive is supported"
echo "Exiting mount script without doing anything"
exit 1
fi
# Check if device is running with uas driver, if so balcklist it and reboot
echo "Checking if we need to blacklist UAS"
blacklist_uas_output=$(umbreld blacklist-uas || true)
# Check output includes text "mount-script-halt" which means we are rebooting and should not mount
if [[ "${blacklist_uas_output}" == *"mount-script-halt"* ]]; then
echo "UAS was blacklisted and device is rebooting, exiting"
return
fi
# At this point we know there is only one block device attached
block_device=$(list_block_devices)
block_device_path="/dev/${block_device}"
partition_path=$(get_first_partition_path "${block_device_path}")
block_device_model=$(get_block_device_model $block_device)
echo "Found device \"${block_device_model}\""
echo "Checking if the device contains an umbrelOS install..."
if [[ $(is_device_umbrelos_install "${block_device_path}") == "true" ]]
then
echo "Yes, it looks like this device is an umbrelOS install not a data drive, refusing to wipe it and exiting..."
exit 1
fi
echo "No, it's not"
echo "Checking if the device is ext4..."
if is_partition_ext4 "${partition_path}" ; then
echo "Yes, it is ext4"
echo "Checking filesystem for corruption..."
# Swallow non-zero exit because successful recovery returns 1 and
# we still want to attempt a best effort boot on a failed recovery.
fsck.ext4 -y "${partition_path}" || true
echo "Mounting partition..."
mount_partition "${partition_path}" || {
echo "Error mounting partition"
exit 1
}
echo "Checking if device contains an Umbrel install..."
if [[ -f "${EXTERNAL_UMBREL_ROOT}"/.umbrel ]]; then
echo "Yes, it contains an Umbrel install"
else
echo "No, it doesn't contain an Umbrel install"
echo "Unmounting partition..."
unmount_partition
setup_new_device $block_device $partition_path
fi
else
echo "No, it's not ext4"
setup_new_device $block_device $partition_path
fi
if [[ ! -d "${EXTERNAL_DOCKER_DIR}" ]]; then
echo "Copying Docker data directory to external storage..."
copy_docker_to_external_storage
fi
echo "Bind mounting external storage over local Umbrel installation..."
mkdir -p "${UMBREL_ROOT}" # Create the directory if it doesn't exist
mount --bind "${EXTERNAL_UMBREL_ROOT}" "${UMBREL_ROOT}"
echo "Bind mounting external storage over local Docker data dir..."
mount --bind "${EXTERNAL_DOCKER_DIR}" "${DOCKER_DIR}"
echo "Bind mounting external storage to ${SWAP_DIR}"
mkdir -p "${MOUNT_POINT}/swap" "${SWAP_DIR}"
mount --bind "${MOUNT_POINT}/swap" "${SWAP_DIR}"
echo "Bind mounting SD card root at /sd-card..."
[[ ! -d "/sd-root" ]] && mkdir -p "/sd-root"
mount --bind "/" "/sd-root"
echo "Checking Umbrel root is now on external storage..."
sync
sleep 1
df -h "${UMBREL_ROOT}" | grep --quiet '/dev/sd\|/dev/nvme'
echo "Checking ${DOCKER_DIR} is now on external storage..."
df -h "${DOCKER_DIR}" | grep --quiet '/dev/sd\|/dev/nvme'
echo "Checking ${SWAP_DIR} is now on external storage..."
df -h "${SWAP_DIR}" | grep --quiet '/dev/sd\|/dev/nvme'
echo "Setting up swapfile"
rm "${SWAP_FILE}" || true
fallocate -l 4G "${SWAP_FILE}"
chmod 600 "${SWAP_FILE}"
mkswap "${SWAP_FILE}"
swapon "${SWAP_FILE}"
echo "Checking SD Card root is bind mounted at /sd-root..."
df -h "/sd-root" | grep --quiet "overlay"
echo "Mount script completed successfully!"
}
main
================================================
FILE: packages/os/overlay-common/etc/NetworkManager/NetworkManager.conf
================================================
[main]
plugins=ifupdown,keyfile
[ifupdown]
managed=false
================================================
FILE: packages/os/overlay-common/etc/NetworkManager/conf.d/10-cloudflaredns.conf
================================================
# This is important, we use Cloudflare for DNS because some users have routers that provide
# unreliable DNS that results in Docker errors when pulling like:
# Get "https://registry-1.docker.io/v2/tailscale/tailscale/manifests/sha256:d488853664499d792b359ea8c18f9a918b92e805b403733fe1c9aac9006ac8c1": dial tcp [2600:1f18:2148:bc01:571f:e759:a87a:2961]:443: connect: network is unreachable
[global-dns-domain-*]
servers=1.1.1.1,1.0.0.1
================================================
FILE: packages/os/overlay-common/etc/acpi/events/power-button
================================================
event=button/power.*PBTN
action=systemd-cat -t umbrel-power-button /etc/acpi/power-button.sh &
================================================
FILE: packages/os/overlay-common/etc/acpi/power-button.sh
================================================
#!/bin/bash
# Configuration
RECOVERY_SEQUENCE_COUNT=10
LISTEN_TIME=1
STATE_FILE="/tmp/power_button_state"
PASSWORD_RESET_FLAG="/tmp/password_reset_flag"
reset_umbrel_password() {
yaml_file="/home/umbrel/umbrel/umbrel.yaml"
echo "umbrel:umbrel" | chpasswd
if ! [ -f "$yaml_file" ]; then
echo "Error: File not found."
return
fi
JSON=$(cat "$user_json" 2>/dev/null)
if ! yq eval . "${yaml_file}" >/dev/null 2>&1; then
echo "Error: Invalid YAML file."
return
fi
if ! yq -e .user.hashedPassword "${yaml_file}" >/dev/null 2>&1 <<<"$JSON"; then
echo "Error: Password property not found in the YAML file."
return
fi
yq eval '.user.hashedPassword = "$2b$10$PDwSSnPmfCQJh3x72KjKs.Nb7NgU62gftuic991GkRyFMcIowpTv2"' -i "${yaml_file}"
yq eval 'del(.user.totpUri)' -i "${yaml_file}"
echo "Password updated successfully."
}
# Function to handle power button presses
handle_press() {
# Append the current timestamp to the state file
echo "$(date +%s)" >> "${STATE_FILE}"
# Check if the last n button presses happened recently
num_entries=$(wc -l < "${STATE_FILE}")
last_timestamps=$(tail -n "${RECOVERY_SEQUENCE_COUNT}" "${STATE_FILE}")
first_timestamp=$(echo "${last_timestamps}" | head -n 1)
last_timestamp=$(echo "${last_timestamps}" | tail -n 1)
total_allowed_duration=$((LISTEN_TIME * RECOVERY_SEQUENCE_COUNT))
if [[ "${num_entries}" -ge "${RECOVERY_SEQUENCE_COUNT}" ]] && [[ $((last_timestamp - first_timestamp)) -lt "${total_allowed_duration}" ]]; then
echo "Power button pressed! Recovery sequence detected. Initiating factory reset."
# This flag indicates that a password reset has been initiated so the previous button presses
# don't also initiate a reset
touch "${PASSWORD_RESET_FLAG}"
reset_umbrel_password
# Remove the password reset flag after all previous button press event handlers have died
# so future resets will work.
sleep "${LISTEN_TIME}"
rm "${PASSWORD_RESET_FLAG}"
exit
fi
# Listen for additional button presses
echo "Power button pressed! Listening for additional button presses for ${LISTEN_TIME} seconds..."
sleep "${LISTEN_TIME}"
# Read the latest timestamp
latest_timestamp=$(tail -n 1 "${STATE_FILE}")
new_num_entries=$(wc -l < "${STATE_FILE}")
if [[ "${num_entries}" != "${new_num_entries}" ]] || [[ -e "${PASSWORD_RESET_FLAG}" ]]; then
# Another button press has been registered or a recovery has just been initiated.
# Exit this handler.
exit
fi
}
# Check if we should do any special handling and exit early if we do.
handle_press
# If we get here no special handling is needed and we should trigger a shutdown.
echo "Nothing else happened. Shutting down the system."
poweroff
================================================
FILE: packages/os/overlay-common/etc/fstab
================================================
# <device> <dir> <type> <options> <dump> <fsck>
/ /mnt/root none bind 0 0
/data/umbrel-os/var/log /var/log none bind 0 0
/data/umbrel-os/var/lib/docker /var/lib/docker none bind 0 0
/data/umbrel-os/home /home none bind 0 0
/data/umbrel-os/var/lib/systemd/timesync /var/lib/systemd/timesync none bind 0 0
/data/umbrel-os/kopia /kopia none bind 0 0
================================================
FILE: packages/os/overlay-common/etc/hostname
================================================
umbrel
================================================
FILE: packages/os/overlay-common/etc/hosts
================================================
127.0.0.1 umbrel
127.0.0.1 localhost
================================================
FILE: packages/os/overlay-common/etc/issue
================================================
================================================
FILE: packages/os/overlay-common/etc/locale.conf
================================================
# Fixes locale warnings when logging in via SSH
LANG=C.UTF-8
================================================
FILE: packages/os/overlay-common/etc/motd
================================================
,;###GGGGGGGGGGl#Sp
,##GGGlW""^' '`""%GGGG#S,
,#GGG" "lGG#o
#GGl^ '$GG#
,#GGb \GGG,
lGG" "GGG
#GGGlGGGl##p,,p##lGGl##p,,p###ll##GGGG
!GGGlW"""*GGGGGGG#""""WlGGGGG#W""*WGGGGS
"" "^ '" ""
- Warning ---------------------------------------------------------------------
| Terminal access is only enabled for debugging purposes. Any modifications |
| made to the umbrelOS system will not be persisted between software updates. |
| For use-cases where you want to run custom software in a Linux environment, |
| consider using the Portainer app available in the Umbrel App Store. |
-------------------------------------------------------------------------------
================================================
FILE: packages/os/overlay-common/etc/sudoers.d/umbrel
================================================
# Remove the silly outdated warning from sudo the first time it's used:
#
# We trust you have received the usual lecture from the local System
# Administrator. It usually boils down to these three things:
#
# #1) Respect the privacy of others.
# #2) Think before you type.
# #3) With great power comes great responsibility.
Defaults lecture_file = /etc/sudoers.lecture
================================================
FILE: packages/os/overlay-common/etc/sudoers.lecture
================================================
================================================
FILE: packages/os/overlay-common/etc/sysctl.d/99-vm-zram-parameters.conf
================================================
# Optimise swap for zram
# Our swap is super fast in-memory via zram so we can afford to be more aggressive with swappiness.
# https://docs.kernel.org/admin-guide/sysctl/vm.html#swappiness
vm.swappiness = 180
# With zstd zram the decompression adds enough overhead that there's essentially zero throughput gain
# from readahead. Use vm.page-cluster=0.
# (This is default on ChromeOS and Android)
# https://old.reddit.com/r/Fedora/comments/mzun99/new_zram_tuning_benchmarks/
# https://issues.chromium.org/issues/41028506#comment17
# https://cs.android.com/search?q=page-cluster&start=21
vm.page-cluster = 0
# Watermark boosting pre-emptively frees up memory to prevent a page allocation miss
# Broken feature that causes page thrashing, disabled by setting to "0"
# (This is default on Ubuntu)
# https://groups.google.com/g/linux.debian.user/c/YcDYu-jM-to
# https://lists.ubuntu.com/archives/kernel-team/2020-March/108587.html
vm.watermark_boost_factor = 0
# Controls the aggressiveness of kswapd. It defines the amount of memory left in a node/system before
# kswapd is woken up and how much memory needs to be free before kswapd goes back to sleep.
# PopOS did a lot of testing with users and arrived at this value to optimise for zram swap.
# https://wiki.archlinux.org/title/Zram#Optimizing_swap_on_zram
# https://www.reddit.com/r/pop_os/comments/104kbs4/zram_now_enabled_by_default_in_pop/
# https://github.com/pop-os/default-settings/pull/163/files#diff-8e1248486dec1681fa98c577efaaf729cb8655c9bb18ae19cc68f5a1baa8ab6bR3
vm.watermark_scale_factor = 125
================================================
FILE: packages/os/overlay-common/etc/systemd/logind.conf.d/lid-switch.conf
================================================
# Ignore lid switch events to allow users running umbrelOS on laptops to keep the device on when the lid is closed.
# The following settings result in ignoring lid switch events when the device is on battery power, when the device is on external power, and when the device is docked or connected to more than one display.
[Login]
HandleLidSwitch=ignore
# HandleLidSwitchExternalPower=ignore (optional, HandleLidSwitch is used if not specified)
# HandleLidSwitchDocked=ignore (default is set to ignore)
================================================
FILE: packages/os/overlay-common/etc/systemd/logind.conf.d/power-button.conf
================================================
# We want logind to ignore the power button press events.
# We will register custom acpi event handlers to handle this
# ourselves.
[Login]
HandlePowerKey=ignore
================================================
FILE: packages/os/overlay-common/etc/systemd/system/umbrel-dns-sync.service
================================================
[Unit]
Description=Synchronize DNS configuration before starting NetworkManager
Before=NetworkManager.service
[Service]
ExecStart=bash /opt/umbrel-dns-sync/umbrel-dns-sync
Type=oneshot
[Install]
WantedBy=multi-user.target
================================================
FILE: packages/os/overlay-common/etc/systemd/system/umbrel-ssh-host-key-hydration.service
================================================
[Unit]
Description=Hydrate SSH Host Keys
Before=ssh.service
[Service]
Type=oneshot
ExecStart=/opt/umbrel-ssh-host-key-hydration/umbrel-ssh-host-key-hydration
[Install]
WantedBy=multi-user.target
================================================
FILE: packages/os/overlay-common/etc/systemd/system/umbrel-tty-message.service
================================================
[Unit]
Description=Display Umbrel access information on TTY
After=umbrel.service
[Service]
ExecStart=/opt/umbrel-tty-message/umbrel-tty-message
Type=oneshot
[Install]
WantedBy=multi-user.target
================================================
FILE: packages/os/overlay-common/etc/systemd/system/umbrel.service
================================================
[Unit]
Description=Umbrel daemon
After=network-online.target docker.service
[Service]
TimeoutStopSec=15min
ExecStart=umbreld --data-directory=/home/umbrel/umbrel
Restart=always
# This prevents us hitting restart rate limits and ensures we keep restarting
# indefinitely.
StartLimitInterval=0
[Install]
WantedBy=multi-user.target
================================================
FILE: packages/os/overlay-common/etc/systemd/timesyncd.conf.d/cloudflare.conf
================================================
# We default to Cloudflare for NTP because some users have issues
# connecting to the default Debian ntp pool. If Cloudflare fails
# the Debian ntp pool is still used as a fallback.
[Time]
NTP=time.cloudflare.com
================================================
FILE: packages/os/overlay-common/etc/systemd/zram-generator.conf
================================================
# We create a virtual in-memory swap device that is 75% of the RAM and compress it
# with zstd which generally gives a ~3:1 compression ratio.
#
# In a 16GB RAM device this gives us 24GB of usable memory before hitting OOM errors.
# 16-((16*0.75)/3)+(16*0.75) = 16GB RAM - 4GB compressed swap device in RAM + 12GB usable swap = 24GB
#
# The zram device is allocated ondemand so the full 16GB of RAM is still usable up until swap is actually needed.
[zram0]
zram-size=ram * 0.75
compression-algorithm=zstd
================================================
FILE: packages/os/overlay-common/opt/umbrel-data/umbrel-data-mount
================================================
#!/bin/bash
set -euo pipefail
CONFIG_PARTITION=${CONFIG_PARTITION:-"/run/rugix/mounts/config"}
CONFIG_FILE="$CONFIG_PARTITION/umbrel.yaml"
MOUNT_POINT="$1"
DEFAULT_PARTITION="${2:-}"
wait_for_devices() {
local devices=("$@")
echo ">>> Waiting for devices to appear: ${devices[*]}"
# 50 checks with 0.1s sleep = 5 second timeout
for i in {1..50}; do
local all_devices_present="true"
for dev in "${devices[@]}"; do
if [ ! -b "$dev" ]; then
all_devices_present="false"
break
fi
done
if [ "$all_devices_present" = "true" ]; then
break
fi
sleep 0.1
done
}
# Handle migration from storage to failsafe mode
# This does the minimum at boot: final sync and pool rename
# The rest of the migration is completed by umbreld after boot
handle_failsafe_transition() {
local pool_name="$1"
local migration_pool="${pool_name}-migration"
local previous_pool="${pool_name}-previous-migration"
echo ">>> Migration pool detected, performing final sync"
# Import both pools
echo ">>> Importing ${pool_name} pool"
zpool import -o cachefile=none -f "$pool_name"
echo ">>> Importing ${migration_pool} pool"
zpool import -o cachefile=none -f "$migration_pool"
# Create final snapshot and sync incrementally
# Using --raw to preserve encryption (sends encrypted blocks without needing key loaded)
echo ">>> Creating final snapshot"
zfs snapshot -r "${pool_name}@migration-final"
echo ">>> Sending incremental changes to migration pool"
zfs send --raw --replicate --large-block --compressed -i @migration "${pool_name}@migration-final" | zfs receive -Fu "$migration_pool"
# Rename pools
# umbrelos > umbrelos-previous-migration
# umbrelos-migration > umbrelos
# If anything before this goes wrong, we boot back into the old pool.
# After the first rename succeeds, if the second rename fails, we rollback
# by renaming umbrelos-previous-migration back to umbrelos.
echo ">>> Renaming pools for migration"
zpool export "$pool_name"
zpool export "$migration_pool"
zpool import -o cachefile=none "$pool_name" "$previous_pool"
if zpool import -o cachefile=none "$migration_pool" "$pool_name"; then
echo ">>> Pool rename complete, umbreld will finish migration after boot"
else
# TODO: Should we signal some error in the raid config here?
echo ">>> ERROR: Failed to rename migration pool, rolling back"
zpool export "$previous_pool"
zpool import -o cachefile=none "$previous_pool" "$pool_name"
echo ">>> Rollback complete, migration aborted"
fi
# Export the pool so we can import it again in the usual mount process
zpool export "$pool_name"
}
# Parse YAML config to get RAID settings if config file exists
POOL_NAME=""
DEVICES=()
RAID_STATE=""
if [ -f "$CONFIG_FILE" ]; then
POOL_NAME=$(yq '.raid.poolName' "$CONFIG_FILE" 2>/dev/null || true)
mapfile -t DEVICES < <(yq '.raid.devices[]' "$CONFIG_FILE" 2>/dev/null || true)
RAID_STATE=$(yq '.raid.state' "$CONFIG_FILE" 2>/dev/null || true)
fi
if [ -n "$POOL_NAME" ] && [ ${#DEVICES[@]} -gt 0 ]; then
echo ">>> Found RAID config for pool '${POOL_NAME}' with ${#DEVICES[@]} devices"
# Load zfs
modprobe zfs
# Attempt to wait for devices
# Continue if timeout is reached to allow mounting array with missing devices
wait_for_devices "${DEVICES[@]}"
# Check if we're in the middle of a failsafe transition
if [ "$RAID_STATE" = "transitioning-to-failsafe" ]; then
handle_failsafe_transition "$POOL_NAME" || true
fi
# Import the pool normally
# -o cachefile=none means we don't read/write the cache files since
# we only have the read only image at this point.
# -f force import if it thinks the pool is active elsewhere. This often
# happens after ota updates where it thinks we're on a new machine.
echo ">>> Importing ${POOL_NAME} pool"
zpool import -o cachefile=none -f "$POOL_NAME"
# Load the encryption key for the data dataset
# We use a hardcoded encryption password for now. This obviously doesn't provide any security.
# However initialising encryption now means we can enable full disk encryption in the future
# by simply updating the password to something secure without requiring an entire backup and restore
# of all data into a new encrypted dataset.
echo ">>> Loading encryption key for ${POOL_NAME}/data"
echo "umbrelumbrel" | zfs load-key "${POOL_NAME}/data" 2>/dev/null
echo ">>> Mounting RAID array to '$MOUNT_POINT'"
mkdir -p "$MOUNT_POINT"
mount -t zfs "${POOL_NAME}/data" "$MOUNT_POINT"
else
echo ">>> No RAID config found, falling back to default data partition"
if [ -z "$DEFAULT_PARTITION" ]; then
echo "ERROR: No default partition provided"
exit 1
fi
echo ">>> Running fsck on '$DEFAULT_PARTITION'"
fsck -p "$DEFAULT_PARTITION"
echo ">>> Mounting '$DEFAULT_PARTITION' to '$MOUNT_POINT'"
mkdir -p "$MOUNT_POINT"
mount "$DEFAULT_PARTITION" "$MOUNT_POINT"
fi
================================================
FILE: packages/os/overlay-common/opt/umbrel-dns-sync/umbrel-dns-sync
================================================
#!/bin/bash
UMBREL_YAML=/home/umbrel/umbrel/umbrel.yaml
CLOUDFLARE_CONF=/etc/NetworkManager/conf.d/10-cloudflaredns.conf
CLOUDFLARE_CONF_DISABLED=/etc/NetworkManager/conf.d/10-cloudflaredns.conf.disabled
# Use CloudFlare DNS unless the setting is explicitly set to `false`
EXTERNAL_DNS=$(yq eval ".settings.externalDns != false" "$UMBREL_YAML" 2>/dev/null || echo "true")
if [[ "$EXTERNAL_DNS" == "false" ]]; then
if [[ -f "$CLOUDFLARE_CONF" ]]; then
mv -f "$CLOUDFLARE_CONF" "$CLOUDFLARE_CONF_DISABLED" || {
echo "Failed to move $CLOUDFLARE_CONF to $CLOUDFLARE_CONF_DISABLED"
exit 1
}
fi
else
if [[ ! -f "$CLOUDFLARE_CONF" ]]; then
mv -f "$CLOUDFLARE_CONF_DISABLED" "$CLOUDFLARE_CONF" || {
echo "Failed to move $CLOUDFLARE_CONF_DISABLED to $CLOUDFLARE_CONF"
exit 1
}
fi
fi
================================================
FILE: packages/os/overlay-common/opt/umbrel-ssh-host-key-hydration/umbrel-ssh-host-key-hydration
================================================
#!/bin/bash
set -euo pipefail
SSH_STATE_DIR=${SSH_STATE_DIR:-"/data/ssh"}
if [ ! -f "${SSH_STATE_DIR}"/ssh_host_rsa_key ]; then
rm -f /etc/ssh/ssh_host_*_key*
ssh-keygen -A
# Copy the keys to the data partition.
mkdir -p "${SSH_STATE_DIR}"
cp /etc/ssh/ssh_host_*_key* "${SSH_STATE_DIR}"
fi
# Restore the keys from the data partition.
cp "${SSH_STATE_DIR}"/ssh_host_*_key* /etc/ssh/
================================================
FILE: packages/os/overlay-common/opt/umbrel-tty-message/umbrel-tty-message
================================================
#!/bin/bash
get_addresses() {
# Get all active wifi and ethernet interfaces
local active_interfaces=$(nmcli --terse --fields TYPE,DEVICE con show --active | grep 'ethernet\|wireless' | cut -d ':' -f 2)
for interface in $active_interfaces; do
# Get the interface IP
local ip=$(nmcli --terse --fields IP4.ADDRESS dev show "${interface}" | grep 'IP4.ADDRESS' | cut -d':' -f2 | cut -d'/' -f1)
# Return the IP
echo "${ip}"
# Return the avahi address
avahi-resolve -a "${ip}" | cut -f 2
done
}
# Wait for addresses to be assigned
while true
do
addresses=$(get_addresses | sort -r | uniq)
[[ "${addresses}" != "" ]] && break
sleep 1
done
# Format addresses for printing
formatted_addresses=$(echo "${addresses}" | sed 's/^/ http:\/\//')
# Clear TTY
echo -e "\033c" > /dev/tty1
# Write TTY message
echo -n "Your Umbrel is now accessible at:
${formatted_addresses}
umbrel login: " > /dev/tty1
================================================
FILE: packages/os/overlay-common/umbrelOS
================================================
================================================
FILE: packages/os/package.json
================================================
{
"scripts": {
"build": "./build.sh",
"build:amd64": "SKIP_ARM64=true npm run build",
"build:amd64:rugix": "SKIP_AMD64_MENDER=true SKIP_MENDER_ARTIFACTS=true npm run build:amd64",
"build:amd64:usb-installer:prepare": "xz --keep --force --threads=0 build/umbrelos-amd64.img",
"build:amd64:usb-installer": "cd usb-installer && ./run.sh",
"build:arm64": "SKIP_AMD64=true npm run build",
"build:pi5": "SKIP_AMD64=true SKIP_PI4=true npm run build",
"vm": "./vm.sh"
},
"devDependencies": {
"opentimestamps": "^0.4.9"
}
}
================================================
FILE: packages/os/rugix/.gitignore
================================================
/.rugix
/build
================================================
FILE: packages/os/rugix/fix-umbrelos-pi-mbr.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
LAST_USED_SECTOR=$(sfdisk -l /data/build/umbrelos-pi-mbr/system.img -o end | tail -n1)
TARGET_SIZE=$(( (LAST_USED_SECTOR + 1) * 512 ))
truncate -s ${TARGET_SIZE} "/data/build/umbrelos-pi-mbr/system.img"
================================================
FILE: packages/os/rugix/layers/umbrelos-amd64.toml
================================================
parent = "umbrelos-root-amd64"
recipes = [
# Prepare umbrelOS base image for Rugix.
"umbrelos-prepare",
# Copy umbrelOS boot files to boot partition.
"umbrelos-boot",
# Install and configure Rugix.
"setup-rugix",
# Fix `/etc/hostname` and `/etc/hosts`.
"fix-overlay",
]
[parameters."core/rugix-ctrl"]
use_musl = false
================================================
FILE: packages/os/rugix/layers/umbrelos-mender-amd64.toml
================================================
parent = "umbrelos-root-amd64"
recipes = [
# Prepare umbrelOS base image for Rugix.
"umbrelos-prepare",
# Copy umbrelOS boot files to boot partition.
"umbrelos-boot",
# Install and configure Rugix.
"setup-rugix",
# Configure Rugix for compatibility with Mender-based legacy systems.
"setup-rugix-mender",
# Fix `/etc/hostname` and `/etc/hosts`.
"fix-overlay",
]
[parameters."core/rugix-ctrl"]
use_musl = false
================================================
FILE: packages/os/rugix/layers/umbrelos-pi.toml
================================================
parent = "umbrelos-root-arm64"
recipes = [
# Prepare umbrelOS base image for Rugix.
"umbrelos-prepare",
# Copy umbrelOS boot files to boot partition.
"umbrelos-boot",
# Install and configure Rugix.
"setup-rugix",
# Fix `/etc/hostname` and `/etc/hosts`.
"fix-overlay",
]
[parameters."core/rugix-ctrl"]
use_musl = false
================================================
FILE: packages/os/rugix/layers/umbrelos-pi4.toml
================================================
parent = "umbrelos-pi"
recipes = [
# Include the firmware update for Raspberry Pi 4.
"core/rpi-include-firmware",
]
[parameters."core/rpi-include-firmware"]
model = "pi4"
================================================
FILE: packages/os/rugix/layers/umbrelos-root-amd64.toml
================================================
url="file:///build/umbrelos-root/umbrelos-root-amd64.tar"
================================================
FILE: packages/os/rugix/layers/umbrelos-root-arm64.toml
================================================
url="file:///build/umbrelos-root/umbrelos-root-arm64.tar"
================================================
FILE: packages/os/rugix/recipes/fix-overlay/files/.gitignore
================================================
hostname
hosts
================================================
FILE: packages/os/rugix/recipes/fix-overlay/files/.gitkeep
================================================
================================================
FILE: packages/os/rugix/recipes/fix-overlay/recipe.toml
================================================
description = "fix `/etc/hostname` and `/etc/hosts`"
================================================
FILE: packages/os/rugix/recipes/fix-overlay/steps/00-install.sh
================================================
#!/bin/bash
set -euo pipefail
install -m 644 "${RECIPE_DIR}/files/hostname" "/etc/"
install -m 644 "${RECIPE_DIR}/files/hosts" "/etc/"
================================================
FILE: packages/os/rugix/recipes/setup-rugix/files/bootstrapping-amd64.toml
================================================
[layout]
type = "gpt"
partitions = [
{ name = "EFI", size = "256M", type = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" },
{ name = "boot-a", size = "512MiB" },
{ name = "boot-b", size = "512MiB" },
{ name = "system-a", size = "10GiB" },
{ name = "system-b", size = "10GiB" },
# For the data partition, we reserve 0.5% of blocks (-m 0.5) for root-only writes to prevent
# full-disk deadlocks and keep logs/Docker alive if the FS hits 100%.
# The ext4 default of 5% is excessive on multi-TB disks (10s to 100s of GB wasted).
# At 0.5%, we reserve around 2.5GB for 0.5TB, 5GB for 1TB, 10GB for 2TB, and 20GB for 4TB,
# which is a small cost that gives us enough headroom for safe recovery.
{ name = "data", filesystem = { type = "ext4", label = "data", additional-options = ["-m", "0.5"] } },
]
================================================
FILE: packages/os/rugix/recipes/setup-rugix/files/bootstrapping-arm64.toml
================================================
[layout]
type = "gpt"
partitions = [
{ name = "EFI", size = "256M", type = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B" },
{ name = "boot-a", size = "128MiB", type = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" },
{ name = "boot-b", size = "128MiB", type = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" },
{ name = "system-a", size = "5GiB" },
{ name = "system-b", size = "5GiB" },
# For the data partition, we reserve 0.5% of blocks (-m 0.5) for root-only writes to prevent
# full-disk deadlocks and keep logs/Docker alive if the FS hits 100%.
# The ext4 default of 5% is excessive on multi-TB disks (10s to 100s of GB wasted).
# At 0.5%, we reserve around 2.5GB for 0.5TB, 5GB for 1TB, 10GB for 2TB, and 20GB for 4TB,
# which is a small cost that gives us enough headroom for safe recovery.
{ name = "data", filesystem = { type = "ext4", label = "data", additional-options = ["-m", "0.5"] } },
]
================================================
FILE: packages/os/rugix/recipes/setup-rugix/files/hooks/state-reset/prepare.sh
================================================
#!/bin/bash
# Rugix `state-reset/prepare` hook to reset the RAID and main disk data partition. By
# default Rugix resets the state on the active data partition only. If the system is
# running from the RAID, then this does only reset the state on the RAID but not the
# RAID config on the config partition itself. This script checks whether a RAID has
# been configured and reformats the main disk data partition, and removes a RAID
# configuration from the config partition if one is detected.
set -euo pipefail
CONFIG_PARTITION=${CONFIG_PARTITION:-"/run/rugix/mounts/config"}
CONFIG_FILE="$CONFIG_PARTITION/umbrel.yaml"
if [ ! -f "$CONFIG_FILE" ]; then
echo "[INFO] no config state file detected, nothing to do"
exit 0
fi
# Parse YAML config to get devices array if config file exists
DEVICES=()
mapfile -t DEVICES < <(yq '.raid.devices[]' "$CONFIG_FILE" 2>/dev/null || true)
echo ">>> Removing RAID configuration from config partition"
if mountpoint -q "$CONFIG_PARTITION"; then
# We need to remove the write-protection on the config partition.
cleanup() {
mount -o remount,ro "$CONFIG_PARTITION"
}
trap cleanup EXIT
mount -o remount,rw "$CONFIG_PARTITION"
fi
rm -f "$CONFIG_FILE"
# If no RAID configuration is detected, nothing to do.
if [ ${#DEVICES[@]} -eq 0 ]; then
echo "[INFO] no RAID configuration detected, nothing to do"
exit 0
fi
# If we have a RAID configuration, we need to reformat the main disk data partition.
SYSTEM_INFO=$(rugix-ctrl system info)
BOOT_FLOW=$(echo "$SYSTEM_INFO" | jq -r ".boot.bootFlow")
# Determine the main disk data partition.
if [ "$BOOT_FLOW" == "mender-grub" ]; then
# On Mender legacy devices, the data partition is the 4th partition on the main disk.
MAIN_DATA_PARTITION=$(rugix-ctrl utils resolve-partition 4 | jq -r ".device" || true)
else
# On Rugix-native devices the main disk data partition is the last partition on the
# main disk, which is either the 7th (MBR) or the 6h (GPT) partition.
for partition in 7 6; do
MAIN_DATA_PARTITION=$(rugix-ctrl utils resolve-partition $partition 2>/dev/null | jq -r ".device" || true)
if [ ! -z "${MAIN_DATA_PARTITION}" ]; then
break
fi
done
fi
if [ -z "${MAIN_DATA_PARTITION}" ]; then
echo "[ERROR] unable to determine main data partition"
exit 1
fi
echo "[INFO] found main disk data partition: '$MAIN_DATA_PARTITION'"
# Ensure that the main disk data partition has not been mounted.
if [ ! -z $(lsblk -no MOUNTPOINT "$MAIN_DATA_PARTITION") ]; then
echo "[ERROR] main disk data partition appears to be mounted"
exit 1
fi
# At this point, we can either reformat the data partition or remove any state on it.
# Reformatting gives us a clean slate, so let's do that.
#
# We use -m 0.5 to reserve 0.5% of blocks for root-only writes (matching bootstrapping config).
mkfs.ext4 -F -m 0.5 -L data "$MAIN_DATA_PARTITION"
================================================
FILE: packages/os/rugix/recipes/setup-rugix/files/state-data.toml
================================================
[[persist]]
directory = "/data"
================================================
FILE: packages/os/rugix/recipes/setup-rugix/files/system.toml
================================================
[data-partition]
# This is safe to enable on all systems as it mounts the default
# data partition if no external RAID has been configured.
mount-script = "/opt/umbrel-data/umbrel-data-mount"
================================================
FILE: packages/os/rugix/recipes/setup-rugix/recipe.toml
================================================
description = "setup Rugix for umbrelOS"
dependencies = ["core/rugix-ctrl"]
================================================
FILE: packages/os/rugix/recipes/setup-rugix/steps/00-install.sh
================================================
#!/bin/bash
set -euo pipefail
apt-get install -y fdisk parted
install -D -m 644 \
"${RECIPE_DIR}/files/bootstrapping-${RUGIX_ARCH}.toml" \
"/etc/rugix/bootstrapping.toml"
install -D -m 644 \
"${RECIPE_DIR}/files/state-data.toml" \
"/etc/rugix/state/data.toml"
install -D -m 644 \
"${RECIPE_DIR}/files/system.toml" \
"/etc/rugix/system.toml"
# Install the factory reset hook.
install -D -m 755 \
"${RECIPE_DIR}/files/hooks/state-reset/prepare.sh" \
"/etc/rugix/hooks/state-reset/prepare/10-umbrel.sh"
================================================
FILE: packages/os/rugix/recipes/setup-rugix-mender/files/init
================================================
#!/bin/sh
DATA_DIR="/run/rugix/mounts/data"
# If the data directory exists, Rugix Ctrl initialized the system and we can proceed with Systemd.
if [ -d "$DATA_DIR" ]; then
echo "Directory $DATA_DIR exists, starting 'systemd'..."
exec /lib/systemd/systemd
else
echo "Directory $DATA_DIR does not exist, starting 'rugix-ctrl'..."
exec /usr/bin/rugix-ctrl
fi
================================================
FILE: packages/os/rugix/recipes/setup-rugix-mender/files/migrate-state.sh
================================================
#!/bin/bash
set -euo pipefail
# We check whether the old `umbrel-os` directory still exists. If it does not, then the
# migration has been completed, and we have nothing further to do.
if [ ! -d "/run/rugix/mounts/data/umbrel-os" ]; then
echo "State has already been migrated."
exit 0
fi
# We simply always copy `/data/ssh` here as it is small.
if [ ! -d "/data/ssh" ] && [ -d "/run/rugix/mounts/data/ssh" ]; then
rm -rf /data/ssh.tmp
cp -rp /run/rugix/mounts/data/ssh /data/ssh.tmp
# This ensures that the copy is atomic.
mv -T /data/ssh.tmp /data/ssh
fi
# Remove the existing `umbrel-os` symlink/directory and replace with migrated state.
rm -rf /data/umbrel-os
if [ "$RUGIX_REQUIRES_COMMIT" == "false" ]; then
# System has previously been committed. Perform the migration now.
# Remove old SSH host keys.
rm -rf /run/rugix/mounts/data/ssh
# Atomically move the old `umbrel-os` directory to the new place.
mv -T /run/rugix/mounts/data/umbrel-os /run/rugix/mounts/data/state/default/persist/data/umbrel-os
else
# State has not been migrated but system has also not been committed. Make sure that
# the `/data/umbrel-os` symlink exists such that the old state is used.
ln -s /run/rugix/mounts/data/umbrel-os /data/umbrel-os
fi
================================================
FILE: packages/os/rugix/recipes/setup-rugix-mender/files/system.toml
================================================
#:schema https://raw.githubusercontent.com/silitics/rugix/refs/tags/v0.8.0/schemas/rugix-ctrl-system.schema.json
[data-partition]
partition = 4
# This is safe to enable on all systems as it mounts the default
# data partition if no external RAID has been configured.
mount-script = "/opt/umbrel-data/umbrel-data-mount"
[boot-flow]
type = "mender-grub"
boot-dir = "/run/rugix/mounts/config"
[boot-groups.a]
slots = { system = "system-a" }
[boot-groups.b]
slots = { system = "system-b" }
[slots.system-a]
type = "block"
partition = 2
[slots.system-b]
type = "block"
partition = 3
================================================
FILE: packages/os/rugix/recipes/setup-rugix-mender/recipe.toml
================================================
description = "configure system for compatibility with legacy Mender systems"
priority = -100
dependencies = ["setup-rugix"]
================================================
FILE: packages/os/rugix/recipes/setup-rugix-mender/steps/00-install.sh
================================================
#!/bin/bash
set -euo pipefail
# Install custom Rugix system configuration for Mender-compatibility.
install -D -m 644 \
"${RECIPE_DIR}/files/system.toml" \
"/etc/rugix/system.toml"
# Create kernel and initrd symlinks as required by Mender's Grub configuration.
cd /boot
ln -s initrd* initrd
ln -s vmlinuz* kernel
# To enable state management, Rugix Ctrl must run as the init system prior to Systemd. As
# we cannot change the Kernel commandline parameters, we instead patch `/sbin/init`.
install -D -m 755 \
"${RECIPE_DIR}/files/init" \
"/sbin/init"
# Install the state migration hook.
install -D -m 755 \
"${RECIPE_DIR}/files/migrate-state.sh" \
"/etc/rugix/hooks/boot/post-init/10-migrate-state.sh"
================================================
FILE: packages/os/rugix/recipes/umbrelos-boot/files/grub.cfg
================================================
load_env -f /boot.grubenv
linux /vmlinuz console=ttyS0 console=tty1 loglevel=3 panic=5 ${rugpi_bootargs}
initrd /initrd.img
boot
================================================
FILE: packages/os/rugix/recipes/umbrelos-boot/recipe.toml
================================================
description = "copy umbrelOS boot files to boot partition"
priority = -800_000
================================================
FILE: packages/os/rugix/recipes/umbrelos-boot/steps/00-install.sh
================================================
#!/bin/bash
set -euo pipefail
BOOT_DIR="${RUGIX_LAYER_DIR}/roots/boot"
mkdir -p "${BOOT_DIR}"
case "${RUGIX_ARCH}" in
"amd64")
echo "Copying kernel and initrd..."
cp -L /vmlinuz "${BOOT_DIR}"
cp -L /initrd.img "${BOOT_DIR}"
echo "Installing second stage boot script..."
cp "${RECIPE_DIR}/files/grub.cfg" "${BOOT_DIR}"
;;
"arm64")
echo "Copying firmware files..."
cp -rp /boot/firmware/* "${BOOT_DIR}"
;;
*)
echo "Unsupported architecture '${RUGIX_ARCH}'."
exit 1
esac
================================================
FILE: packages/os/rugix/recipes/umbrelos-cleanup/recipe.toml
================================================
description = "cleanup and restore original umbrelOS configuration"
priority = -800_000
================================================
FILE: packages/os/rugix/recipes/umbrelos-cleanup/steps/00-install.sh
================================================
#!/bin/bash
set -euo pipefail
mv /etc/resolv.conf.original /etc/resolv.conf
rm -rf /var/log
================================================
FILE: packages/os/rugix/recipes/umbrelos-prepare/recipe.toml
================================================
description = "prepare umbrelOS base image for Rugix"
priority = 800_000
dependencies = ["umbrelos-cleanup"]
================================================
FILE: packages/os/rugix/recipes/umbrelos-prepare/steps/00-install.sh
================================================
#!/bin/bash
set -euo pipefail
mv /etc/resolv.conf /etc/resolv.conf.original
echo "nameserver 1.1.1.1" > /etc/resolv.conf
mkdir -p /var/log/apt
# Systemd uses this file to detect that it runs in Docker. This will prevent `reboot`
# from working as it should and may also lead to a bunch of other problems.
rm -f /.dockerenv
================================================
FILE: packages/os/rugix/rugix-bakery.toml
================================================
#:schema https://raw.githubusercontent.com/silitics/rugix/refs/tags/v0.8.0/schemas/rugix-bakery-project.schema.json
# Image for AMD64 EFI systems.
[systems.umbrelos-amd64]
layer = "umbrelos-amd64"
architecture = "amd64"
target = "generic-grub-efi"
# Configure a GPT layout to enlarge boot partitions to 512 MiB.
[systems.umbrelos-amd64.image.layout]
type = "gpt"
partitions = [
{ root = "config", type = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", size = "256M", filesystem = { type = "fat32" } },
{ root = "boot", size = "512MiB", filesystem = { type = "ext4" } },
{ size = "512M" },
{ root = "system", filesystem = { type = "ext4", additional-options = [
"-O",
"^has_journal",
"-E",
"hash_seed=035cb65d-0a86-404a-bad7-19c88d05e400",
"-U",
"12341234-a4ec-4304-a70f-c549ea829da9",
] } },
]
# Image for Raspberry Pi 4 including the required firmware update.
[systems.umbrelos-pi4]
layer = "umbrelos-pi4"
architecture = "arm64"
target = "rpi-tryboot"
# Configure a GPT layout to support devices disks larger than 2 TiB.
[systems.umbrelos-pi4.image.layout]
type = "gpt"
partitions = [
{ root = "config", type = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", size = "256M", filesystem = { type = "fat32" } },
{ root = "boot", type = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", size = "128M", filesystem = { type = "fat32" } },
{ type = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", size = "128M" },
{ root = "system", filesystem = { type = "ext4", additional-options = [
"-O",
"^has_journal",
"-E",
"hash_seed=035cb65d-0a86-404a-bad7-19c88d05e400",
"-U",
"12341234-a4ec-4304-a70f-c549ea829da9",
] } },
]
# Image for Raspberry Pi 4 and 5 without the firmware update.
[systems.umbrelos-pi-tryboot]
layer = "umbrelos-pi"
architecture = "arm64"
target = "rpi-tryboot"
# Configure a GPT layout to support devices disks larger than 2 TiB.
[systems.umbrelos-pi-tryboot.image.layout]
type = "gpt"
partitions = [
{ root = "config", type = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", size = "256M", filesystem = { type = "fat32" } },
{ root = "boot", type = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", size = "128M", filesystem = { type = "fat32" } },
{ type = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", size = "128M" },
{ root = "system", filesystem = { type = "ext4", additional-options = [
"-O",
"^has_journal",
"-E",
"hash_seed=035cb65d-0a86-404a-bad7-19c88d05e400",
"-U",
"12341234-a4ec-4304-a70f-c549ea829da9",
] } },
]
# Legacy MBR-based image to build Mender update artifacts.
[systems.umbrelos-pi-mbr]
layer = "umbrelos-pi"
architecture = "arm64"
target = "rpi-tryboot"
# Image for legacy Mender-based devices.
[systems.umbrelos-mender-amd64]
layer = "umbrelos-mender-amd64"
architecture = "amd64"
target = "unknown"
# We need a custom layout here to create a filesystem compatible with Mender's version of
# Grub. In particular, the feature `metadata_csum_seed` is incompatible, so we remove it.
# We also do not need/want a journal as the root filesystem is read-only. Note that adding
# a journal will also interfere with static delta updates.
[systems.umbrelos-mender-amd64.image.layout]
type = "gpt"
partitions = [
{ root = "system", filesystem = { type = "ext4", additional-options = [
"-O",
"^metadata_csum_seed,^has_journal",
"-E",
"hash_seed=035cb65d-0a86-404a-bad7-19c88d05e400",
"-U",
"12341234-a4ec-4304-a70f-c549ea829da9",
] } },
]
================================================
FILE: packages/os/rugix/run-bakery
================================================
#!/usr/bin/env bash
set -euo pipefail
DOCKER=${DOCKER:-"docker"}
DOCKER_FLAGS=${DOCKER_FLAGS:-""}
RUGIX_DEV=${RUGIX_DEV:-"false"}
RUGIX_CONTEXT_DIR=${RUGIX_CONTEXT_DIR:-""}
RUGIX_CACHE_VOLUME=${RUGIX_CACHE_VOLUME:-"rugix-build-cache"}
if [ "${RUGIX_DEV}" = "false" ]; then
RUGIX_VERSION=${RUGIX_VERSION:-"v0.8"}
else
RUGIX_VERSION=${RUGIX_VERSION:-"dev"}
fi
RUGIX_BAKERY_IMAGE=${RUGIX_BAKERY_IMAGE:-"ghcr.io/silitics/rugix-bakery:${RUGIX_VERSION}"}
if [ "${RUGIX_DEV}" = "false" ]; then
$DOCKER pull "${RUGIX_BAKERY_IMAGE}"
fi
RUGIX_BAKERY_IMAGE=$($DOCKER inspect --format='{{.Id}}' "${RUGIX_BAKERY_IMAGE}")
if [ -t 0 ] && [ -t 1 ]; then
DOCKER_FLAGS="${DOCKER_FLAGS} -it"
fi
if [ -n "${RUGIX_CACHE_VOLUME}" ]; then
if ! $DOCKER volume inspect "${RUGIX_CACHE_VOLUME}" >/dev/null 2>&1; then
$DOCKER volume create "${RUGIX_CACHE_VOLUME}" >/dev/null
fi
DOCKER_FLAGS="${DOCKER_FLAGS} -v ${RUGIX_CACHE_VOLUME}:/run/rugix/bakery/cache"
fi
if [ "${1:-}" == "run" ]; then
# Add port forwarding for SSH when running a system in a VM.
DOCKER_FLAGS="${DOCKER_FLAGS} -p 127.0.0.1:2222:2222 -p [::1]:2222:2222"
fi
exec $DOCKER run --rm --privileged \
$DOCKER_FLAGS \
-v "$(pwd)":/project \
-v "$(pwd)/${RUGIX_CONTEXT_DIR}":/run/rugix/bakery/context \
-v /dev:/dev \
-e "RUGIX_HOST_PROJECT_DIR=$(pwd)" \
-e "RUGIX_BAKERY_IMAGE=${RUGIX_BAKERY_IMAGE}" \
-e "RUGIX_DEV=${RUGIX_DEV}" \
"${RUGIX_BAKERY_IMAGE}" \
"$@"
================================================
FILE: packages/os/rugpi-image
================================================
#!/usr/bin/env python3
#
# Copyright 2023-2024 Silitics GmbH <info@silitics.com>
#
# This file is part of Rugpi (https://rugpi.io).
#
# SPDX-License-Identifier: MIT OR Apache-2.0
import pathlib
import subprocess
import sys
try:
# Mender provides us with two arguments:
# 1. The current state of the update process.
# 2. A directory where we can find the files of the artifact.
STATE = sys.argv[1]
FILES = pathlib.Path(sys.argv[2])
except IndexError:
raise RuntimeError(f"usage: {sys.argv[0]} <state> <files>")
def rugpi_commit_system():
"""Commit the current partition set."""
subprocess.check_call(["rugpi-ctrl", "system", "commit"])
def rugpi_install_image(image: pathlib.Path):
"""Install a Rugpi image without rebooting in streaming mode."""
with open(image, "rb") as image_file:
subprocess.check_call(
[
"rugpi-ctrl",
"update",
"install",
"--stream",
"--no-reboot",
"-",
],
stdin=image_file,
)
while True:
if not image_file.read(4096):
break
def query_supports_rollback():
"""The `SupportsRollback` query of the update process."""
# We do support rollbacks.
print("Yes")
def query_needs_artifact_reboot():
"""The `NeedsArtifactReboot` query of the update process."""
# We want Mender to take care of rebooting.
print("Automatic")
def state_download():
"""The `Download` state of the update process."""
image_found = False
# Commit the present system so that we can overwrite the cold partitions.
rugpi_commit_system()
with (FILES / "stream-next").open("rt") as stream_next:
while True:
next_file = stream_next.readline().strip()
if not next_file:
# No more files left in the stream.
break
if next_file.strip().endswith(".img"):
# We found an image, let's install it.
image_found = True
rugpi_install_image(FILES / next_file)
if not image_found:
raise RuntimeError("unable to find image in the artifact")
def state_artifact_install():
"""The `ArtifactInstall` state of the update process."""
# The image has already been installed in the downloade state. It remains
# to create the marker file such that Mender reboots via Rugpi.
pathlib.Path("/run/rugpi/.mender-reboot-spare").touch()
def state_artifact_rollback():
"""The `ArtifactRollback` state of the update process."""
# Rebooting will automatically roll back the system.
def state_artifact_verify_reboot():
"""The `ArtifactVerifyReboot` state of the update process."""
output = subprocess.check_output(["rugpi-ctrl", "system", "info"]).decode()
hot = None
default = None
for line in output.splitlines():
try:
key, _, value = line.partition(":")
except ValueError:
pass
else:
key = key.strip()
value = value.strip()
if key == "Hot":
hot = value
elif key == "Default":
default = value
if hot == default:
# Something went wrong!
sys.exit(1)
def state_artifact_commit():
"""The `ArtifactCommit` state of the update process."""
rugpi_commit_system()
def state_nop():
"""Called for all states we do not need to handle."""
{
"SupportsRollback": query_supports_rollback,
"NeedsArtifactReboot": query_needs_artifact_reboot,
"Download": state_download,
"ArtifactInstall": state_artifact_install,
"ArtifactRollback": state_artifact_rollback,
"ArtifactVerifyReboot": state_artifact_verify_reboot,
"ArtifactCommit": state_artifact_commit,
}.get(STATE, state_nop)()
================================================
FILE: packages/os/trigger-change
================================================
Thu 15 Jan 2026 17:12:57 +07
================================================
FILE: packages/os/umbrelos.Dockerfile
================================================
ARG DEBIAN_VERSION=trixie
ARG SNAPSHOT_DATE=20251229
ARG DOCKER_VERSION=28.5.0
ARG DOCKER_INSTALL_SCRIPT_COMMIT=5c8855edd778525564500337f5ac4ad65a0c168e
ARG YQ_VERSION=4.24.5
ARG YQ_SHA256_amd64=c93a696e13d3076e473c3a43c06fdb98fafd30dc2f43bc771c4917531961c760
ARG YQ_SHA256_arm64=8879e61c0b3b70908160535ea358ec67989ac4435435510e1fcb2eda5d74a0e9
ARG NODE_VERSION=22.13.0
ARG NODE_SHA256_amd64=9a33e89093a0d946c54781dcb3ccab4ccf7538a7135286528ca41ca055e9b38f
ARG NODE_SHA256_arm64=e0cc088cb4fb2e945d3d5c416c601e1101a15f73e0f024c9529b964d9f6dce5b
ARG KOPIA_VERSION=0.19.0
ARG KOPIA_SHA256_amd64=c07843822c82ec752e5ee749774a18820b858215aabd7da448ce665b9b9107aa
ARG KOPIA_SHA256_arm64=632db9d72f2116f1758350bf7c20aa57c22c220480aaccb5f839e75669210ed9
#########################################################################
# ui build stage
#########################################################################
FROM node:${NODE_VERSION}-bookworm-slim AS ui-build
# Set the working directory
WORKDIR /app
# Copy the package.json and package-lock.json
COPY packages/ui/ .
# The ui-build stage only has 'packages/ui' in '/app', but the ui imports runtime values
# via a relative path ('../../../umbreld/source/modules/server/trpc/common') that resolves outside '/app'.
# We copy the target file to the expected path for the build to succeed.
COPY packages/umbreld/source/modules/server/trpc/common.ts /umbreld/source/modules/server/trpc/common.ts
# Install the dependencies
RUN rm -rf node_modules || true
RUN npm ci
# Build the app
RUN npm run build
#########################################################################
# umbrelos-base-amd64 build stage
#########################################################################
FROM debian:${DEBIAN_VERSION}-${SNAPSHOT_DATE} AS umbrelos-base-amd64
ARG SNAPSHOT_DATE
COPY packages/os/build-steps /build-steps
RUN /build-steps/initialize.sh "${SNAPSHOT_DATE}"
# Install Linux kernel, non-free firmware and ZFS.
RUN apt-get install --yes \
zfs-dkms \
zfsutils-linux \
linux-headers-amd64 \
linux-image-amd64 \
intel-microcode \
amd64-microcode \
firmware-linux \
firmware-realtek \
firmware-iwlwifi \
firmware-atheros
# Cleanup build steps.
RUN rm -rf /build-steps
#########################################################################
# umbrelos-base-arm64 build stage
#########################################################################
FROM debian:${DEBIAN_VERSION}-${SNAPSHOT_DATE} AS umbrelos-base-arm64
ARG SNAPSHOT_DATE
COPY packages/os/build-steps /build-steps
RUN /build-steps/initialize.sh "${SNAPSHOT_DATE}"
RUN /build-steps/setup-raspberrypi.sh
# Cleanup build steps.
RUN rm -rf /build-steps
#########################################################################
# umbrelos build stage
#########################################################################
ARG TARGETARCH
# TODO: Instead of using the debian:trixie image as a base we should
# build a fresh rootfs from scratch. We can use the same tool the Docker
# images use for reproducible Debian builds: https://github.com/debuerreotype/debuerreotype
FROM umbrelos-base-${TARGETARCH} AS umbrelos
# We need to duplicate this such that we can also use the argument below.
ARG TARGETARCH
ARG DOCKER_VERSION
ARG DOCKER_INSTALL_SCRIPT_COMMIT
ARG YQ_VERSION
ARG YQ_SHA256_amd64
ARG YQ_SHA256_arm64
ARG NODE_VERSION
ARG NODE_SHA256_amd64
ARG NODE_SHA256_arm64
ARG KOPIA_VERSION
ARG KOPIA_SHA256_amd64
ARG KOPIA_SHA256_arm64
# Install acpid
# We use acpid to implement custom behaviour for power button presses
RUN apt-get install --yes acpid
RUN systemctl enable acpid
# Install zram-generator for swap
RUN apt-get install --yes systemd-zram-generator
# Install essential networking services
RUN apt-get install --yes network-manager systemd-timesyncd openssh-server avahi-daemon avahi-discover avahi-utils libnss-mdns
# Install bluetooth stack
# The default configuration enables all bluetooth controllers/adapters present on boot and plugged in after boot
RUN apt-get install --yes bluez
# Install essential system utilities
RUN apt-get install --yes sudo nano vim less man iproute2 iputils-ping curl wget ca-certificates usbutils whois build-essential e2fsprogs
# Install umbreld dependencies
# (many of these can be remove after the apps refactor)
RUN apt-get install --yes python3 fswatch jq rsync git gettext-base gnupg procps dmidecode unar imagemagick ffmpeg samba wsdd2 cifs-utils smbclient nvme-cli pciutils
# Disable automatically starting smbd and wsdd2 at boot so umbreld can initialize them only when they're needed
RUN systemctl disable smbd wsdd2
# Filessystem support
RUN apt-get install --yes gdisk parted e2fsprogs exfatprogs
# For some reason this always fails on arm64 but it's ok since we
# don't support external storage on Pi anyway.
RUN [ "${TARGETARCH}" = "amd64" ] && apt-get install --yes ntfs-3g || true
# Install Node.js
RUN NODE_ARCH=$([ "${TARGETARCH}" = "arm64" ] && echo "arm64" || echo "x64") && \
NODE_SHA256=$(eval echo \$NODE_SHA256_${TARGETARCH}) && \
curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.gz -o node.tar.gz && \
echo "${NODE_SHA256} node.tar.gz" | sha256sum -c - && \
tar -xz -f node.tar.gz -C /usr/local --strip-components=1 && \
rm -rf node.tar.gz
# Install yq from binary
# Debian repos have kislyuk/yq but we want mikefarah/yq
RUN YQ_SHA256=$(eval echo \$YQ_SHA256_${TARGETARCH}) && \
curl -L https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_${TARGETARCH} -o /usr/bin/yq && \
echo "${YQ_SHA256} /usr/bin/yq" | sha256sum -c && \
chmod +x /usr/bin/yq
RUN curl -fsSL https://raw.githubusercontent.com/docker/docker-install/${DOCKER_INSTALL_SCRIPT_COMMIT}/install.sh -o /tmp/install-docker.sh
RUN sh /tmp/install-docker.sh --version v${DOCKER_VERSION}
RUN rm /tmp/install-docker.sh
# Install kopia from binary
RUN KOPIA_ARCH=$([ "${TARGETARCH}" = "arm64" ] && echo "arm64" || echo "x64") && \
KOPIA_SHA256=$(eval echo \$KOPIA_SHA256_${TARGETARCH}) && \
curl -L https://github.com/kopia/kopia/releases/download/v${KOPIA_VERSION}/kopia-${KOPIA_VERSION}-linux-${KOPIA_ARCH}.tar.gz -o /tmp/kopia.tar.gz && \
echo "${KOPIA_SHA256} /tmp/kopia.tar.gz" | sha256sum -c && \
tar -xz -f /tmp/kopia.tar.gz -C /tmp && \
mv /tmp/kopia-${KOPIA_VERSION}-linux-${KOPIA_ARCH}/kopia /usr/bin/kopia && \
chmod +x /usr/bin/kopia
# kopia also requires fuse3 for mounting snapshots
RUN apt-get install --yes fuse3 bindfs
# Add Umbrel user
RUN adduser --gecos "" --disabled-password umbrel
RUN echo "umbrel:umbrel" | chpasswd
RUN usermod -aG sudo umbrel
# Preload images
RUN sudo apt-get install --yes skopeo
RUN mkdir -p /images
RUN skopeo copy docker://getumbrel/tor@sha256:2ace83f22501f58857fa9b403009f595137fa2e7986c4fda79d82a8119072b6a docker-archive:/images/tor
RUN skopeo copy docker://getumbrel/auth-server@sha256:b4a4b37896911a85fb74fa159e010129abd9dff751a40ef82f724ae066db3c2a docker-archive:/images/auth
# Install umbreld
COPY packages/umbreld /opt/umbreld
COPY --from=ui-build /app/dist /opt/umbreld/ui
WORKDIR /opt/umbreld
RUN rm -rf node_modules || true
RUN npm clean-install --omit dev && npm link
WORKDIR /
# Copy in filesystem overlay
COPY packages/os/overlay-common /
COPY "packages/os/overlay-${TARGETARCH}" /
# Move persistant locations to /data to be bind mounted over the OS.
# /data will exist on a seperate partition that survives OS updates.
# This step should always be last so things like /var/log/apt/
# exist while installing packages.
# Migrataing current data is required to not break journald, otherwise
# /var/log/journal will not exist and journald will log to RAM and not
# persist between reboots.
RUN mkdir -p /data/umbrel-os/var
RUN mv /var/log /data/umbrel-os/var/log
RUN mv /home /data/umbrel-os/home
================================================
FILE: packages/os/usb-installer/.gitignore
================================================
build
================================================
FILE: packages/os/usb-installer/build.sh
================================================
#!/usr/bin/env bash
set -euo pipefail
rootfs_dir="/tmp/rootfs"
iso_image="/tmp/umbrelos-amd64-usb-installer.iso"
echo "Creating directories for ISO image..."
mkdir -p "${rootfs_dir}/boot/grub"
echo "Extracting rootfs..."
tar -xf /data/build/rootfs.tar --directory "${rootfs_dir}"
echo "Creating grub.cfg..."
cat > "${rootfs_dir}/boot/grub/grub.cfg" <<EOF
set default=0
set timeout=5
set gfxmode=auto
insmod all_video
insmod gfxterm
terminal_output gfxterm
menuentry "umbrelOS installer" {
linux /vmlinuz root=LABEL=UMBRELINSTALLER ro quiet loglevel=0 nomodeset vga=normal fbcon=font:VGA8x16
initrd /initrd.img
}
menuentry "umbrelOS installer (alt graphics)" {
linux /vmlinuz root=LABEL=UMBRELINSTALLER ro quiet loglevel=0
initrd /initrd.img
}
EOF
echo "Creat
gitextract_9b_v3mll/
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── update-translations-in-pr.yml
├── .gitignore
├── .prettierrc.js
├── .umbrel
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── containers/
│ ├── app-auth/
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── www
│ │ ├── middleware/
│ │ │ ├── handle_error.js
│ │ │ └── validate_token.js
│ │ ├── package.json
│ │ ├── routes/
│ │ │ └── auth.js
│ │ ├── test/
│ │ │ ├── docker-compose.yml
│ │ │ ├── fixtures/
│ │ │ │ └── app-data/
│ │ │ │ └── mempool/
│ │ │ │ └── umbrel-app.yml
│ │ │ ├── global.js
│ │ │ ├── test.sh
│ │ │ └── utils/
│ │ │ └── hmac.js
│ │ ├── utils/
│ │ │ ├── app.js
│ │ │ ├── const.js
│ │ │ ├── dashboard.js
│ │ │ ├── express.js
│ │ │ ├── hmac.js
│ │ │ ├── host_resolution.js
│ │ │ ├── manager.js
│ │ │ ├── safe_handler.js
│ │ │ └── token.js
│ │ └── views/
│ │ └── pages/
│ │ └── redirect.ejs
│ ├── app-proxy/
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── Dockerfile
│ │ ├── Dockerfile.dev
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── www
│ │ ├── middleware/
│ │ │ └── handle_error.js
│ │ ├── package.json
│ │ ├── routes/
│ │ │ └── umbrel.js
│ │ ├── test/
│ │ │ ├── .gitignore
│ │ │ ├── docker-compose.app1.yml
│ │ │ ├── docker-compose.app2.yml
│ │ │ ├── docker-compose.bleskomat.yml
│ │ │ ├── docker-compose.error.yml
│ │ │ ├── docker-compose.mempool.yml
│ │ │ ├── docker-compose.nextcloud.yml
│ │ │ ├── docker-compose.proxy.yml
│ │ │ ├── docker-compose.proxyhttps.yaml
│ │ │ ├── docker-compose.sse.yml
│ │ │ ├── docker-compose.suredbits.yml
│ │ │ ├── docker-compose.ws.yml
│ │ │ ├── docker-compose.yml
│ │ │ ├── fixtures/
│ │ │ │ └── mempool-umbrel-app.yml
│ │ │ ├── global.js
│ │ │ ├── sse-test-server/
│ │ │ │ ├── .dockerignore
│ │ │ │ ├── Dockerfile
│ │ │ │ ├── bin/
│ │ │ │ │ └── www
│ │ │ │ └── package.json
│ │ │ ├── test/
│ │ │ │ └── Caddyfile-https
│ │ │ ├── test.sh
│ │ │ └── utils/
│ │ │ ├── express.js
│ │ │ └── tor.js
│ │ ├── utils/
│ │ │ ├── const.js
│ │ │ ├── express.js
│ │ │ ├── hmac.js
│ │ │ ├── manager.js
│ │ │ ├── proxy.js
│ │ │ ├── safe_handler.js
│ │ │ ├── token.js
│ │ │ └── tor.js
│ │ └── views/
│ │ └── pages/
│ │ └── error.ejs
│ └── tor/
│ ├── Dockerfile
│ ├── README.md
│ └── test/
│ ├── .gitignore
│ ├── docker-compose.entrypoint.yml
│ ├── docker-compose.yml
│ ├── entrypoint.sh
│ ├── test-entrypoint.sh
│ ├── test.sh
│ └── torrc
├── info.json
├── package.json
├── packages/
│ ├── os/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── build-steps/
│ │ │ ├── initialize.sh
│ │ │ ├── setup-raspberrypi/
│ │ │ │ ├── cmdline.txt
│ │ │ │ ├── config.txt
│ │ │ │ ├── raspberrypi.gpg.key
│ │ │ │ └── raspberrypi.list
│ │ │ └── setup-raspberrypi.sh
│ │ ├── build.sh
│ │ ├── builder.Dockerfile
│ │ ├── mender.cfg
│ │ ├── overlay-amd64/
│ │ │ └── umbrelOS
│ │ ├── overlay-arm64/
│ │ │ ├── etc/
│ │ │ │ └── systemd/
│ │ │ │ └── system/
│ │ │ │ └── umbrel-external-storage.service
│ │ │ └── opt/
│ │ │ └── umbrel-external-storage/
│ │ │ └── umbrel-external-storage
│ │ ├── overlay-common/
│ │ │ ├── etc/
│ │ │ │ ├── NetworkManager/
│ │ │ │ │ ├── NetworkManager.conf
│ │ │ │ │ └── conf.d/
│ │ │ │ │ └── 10-cloudflaredns.conf
│ │ │ │ ├── acpi/
│ │ │ │ │ ├── events/
│ │ │ │ │ │ └── power-button
│ │ │ │ │ └── power-button.sh
│ │ │ │ ├── fstab
│ │ │ │ ├── hostname
│ │ │ │ ├── hosts
│ │ │ │ ├── issue
│ │ │ │ ├── locale.conf
│ │ │ │ ├── motd
│ │ │ │ ├── sudoers.d/
│ │ │ │ │ └── umbrel
│ │ │ │ ├── sudoers.lecture
│ │ │ │ ├── sysctl.d/
│ │ │ │ │ └── 99-vm-zram-parameters.conf
│ │ │ │ └── systemd/
│ │ │ │ ├── logind.conf.d/
│ │ │ │ │ ├── lid-switch.conf
│ │ │ │ │ └── power-button.conf
│ │ │ │ ├── system/
│ │ │ │ │ ├── umbrel-dns-sync.service
│ │ │ │ │ ├── umbrel-ssh-host-key-hydration.service
│ │ │ │ │ ├── umbrel-tty-message.service
│ │ │ │ │ └── umbrel.service
│ │ │ │ ├── timesyncd.conf.d/
│ │ │ │ │ └── cloudflare.conf
│ │ │ │ └── zram-generator.conf
│ │ │ ├── opt/
│ │ │ │ ├── umbrel-data/
│ │ │ │ │ └── umbrel-data-mount
│ │ │ │ ├── umbrel-dns-sync/
│ │ │ │ │ └── umbrel-dns-sync
│ │ │ │ ├── umbrel-ssh-host-key-hydration/
│ │ │ │ │ └── umbrel-ssh-host-key-hydration
│ │ │ │ └── umbrel-tty-message/
│ │ │ │ └── umbrel-tty-message
│ │ │ └── umbrelOS
│ │ ├── package.json
│ │ ├── rugix/
│ │ │ ├── .gitignore
│ │ │ ├── fix-umbrelos-pi-mbr.sh
│ │ │ ├── layers/
│ │ │ │ ├── umbrelos-amd64.toml
│ │ │ │ ├── umbrelos-mender-amd64.toml
│ │ │ │ ├── umbrelos-pi.toml
│ │ │ │ ├── umbrelos-pi4.toml
│ │ │ │ ├── umbrelos-root-amd64.toml
│ │ │ │ └── umbrelos-root-arm64.toml
│ │ │ ├── recipes/
│ │ │ │ ├── fix-overlay/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── .gitignore
│ │ │ │ │ │ └── .gitkeep
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── setup-rugix/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── bootstrapping-amd64.toml
│ │ │ │ │ │ ├── bootstrapping-arm64.toml
│ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ └── state-reset/
│ │ │ │ │ │ │ └── prepare.sh
│ │ │ │ │ │ ├── state-data.toml
│ │ │ │ │ │ └── system.toml
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── setup-rugix-mender/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ ├── init
│ │ │ │ │ │ ├── migrate-state.sh
│ │ │ │ │ │ └── system.toml
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── umbrelos-boot/
│ │ │ │ │ ├── files/
│ │ │ │ │ │ └── grub.cfg
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ ├── umbrelos-cleanup/
│ │ │ │ │ ├── recipe.toml
│ │ │ │ │ └── steps/
│ │ │ │ │ └── 00-install.sh
│ │ │ │ └── umbrelos-prepare/
│ │ │ │ ├── recipe.toml
│ │ │ │ └── steps/
│ │ │ │ └── 00-install.sh
│ │ │ ├── rugix-bakery.toml
│ │ │ └── run-bakery
│ │ ├── rugpi-image
│ │ ├── trigger-change
│ │ ├── umbrelos.Dockerfile
│ │ ├── usb-installer/
│ │ │ ├── .gitignore
│ │ │ ├── build.sh
│ │ │ ├── builder.Dockerfile
│ │ │ ├── overlay/
│ │ │ │ ├── etc/
│ │ │ │ │ └── systemd/
│ │ │ │ │ └── system/
│ │ │ │ │ └── custom-tty.service
│ │ │ │ └── opt/
│ │ │ │ └── custom-tty
│ │ │ ├── run.sh
│ │ │ └── usb-installer.Dockerfile
│ │ └── vm.sh
│ ├── ui/
│ │ ├── .dockerignore
│ │ ├── .gitignore
│ │ ├── .prettierignore
│ │ ├── .prettierrc.js
│ │ ├── Dockerfile
│ │ ├── app-auth/
│ │ │ ├── README.md
│ │ │ ├── index.html
│ │ │ ├── src/
│ │ │ │ ├── login-with-umbrel.tsx
│ │ │ │ └── main.tsx
│ │ │ └── vite.config.ts
│ │ ├── components.json
│ │ ├── eslint.config.js
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── public/
│ │ │ ├── assets/
│ │ │ │ ├── onboarding/
│ │ │ │ │ └── onboarding-bg.webm
│ │ │ │ └── whats-new/
│ │ │ │ ├── backups.webm
│ │ │ │ ├── external-storage.webm
│ │ │ │ ├── network-devices.webm
│ │ │ │ ├── restore.webm
│ │ │ │ └── rewind.webm
│ │ │ ├── locales/
│ │ │ │ ├── de.json
│ │ │ │ ├── en.json
│ │ │ │ ├── es.json
│ │ │ │ ├── fr.json
│ │ │ │ ├── hu.json
│ │ │ │ ├── it.json
│ │ │ │ ├── ja.json
│ │ │ │ ├── ko.json
│ │ │ │ ├── nl.json
│ │ │ │ ├── pt.json
│ │ │ │ ├── tr.json
│ │ │ │ └── uk.json
│ │ │ └── site.webmanifest
│ │ ├── src/
│ │ │ ├── components/
│ │ │ │ ├── app-icon.tsx
│ │ │ │ ├── caret-right.tsx
│ │ │ │ ├── chevron-down.tsx
│ │ │ │ ├── cmdk-providers.tsx
│ │ │ │ ├── cmdk.tsx
│ │ │ │ ├── darken-layer.tsx
│ │ │ │ ├── fade-scroller.tsx
│ │ │ │ ├── iframe-checker.tsx
│ │ │ │ ├── install-button-connected.tsx
│ │ │ │ ├── install-button.tsx
│ │ │ │ ├── markdown.tsx
│ │ │ │ ├── onboarding-background.tsx
│ │ │ │ ├── progress-button.tsx
│ │ │ │ ├── ui/
│ │ │ │ │ ├── alert-dialog.tsx
│ │ │ │ │ ├── alert.tsx
│ │ │ │ │ ├── animated-number.tsx
│ │ │ │ │ ├── arc.tsx
│ │ │ │ │ ├── badge.tsx
│ │ │ │ │ ├── button-link.tsx
│ │ │ │ │ ├── button.tsx
│ │ │ │ │ ├── card.tsx
│ │ │ │ │ ├── carousel.tsx
│ │ │ │ │ ├── checkbox.tsx
│ │ │ │ │ ├── command.tsx
│ │ │ │ │ ├── context-menu.tsx
│ │ │ │ │ ├── copy-button.tsx
│ │ │ │ │ ├── copyable-field.tsx
│ │ │ │ │ ├── cover-message.tsx
│ │ │ │ │ ├── debug-only.tsx
│ │ │ │ │ ├── dialog-close-button.tsx
│ │ │ │ │ ├── dialog.tsx
│ │ │ │ │ ├── drawer.tsx
│ │ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ │ ├── error-boundary-card-fallback.tsx
│ │ │ │ │ ├── error-boundary-page-fallback.tsx
│ │ │ │ │ ├── fade-in-img.tsx
│ │ │ │ │ ├── form.tsx
│ │ │ │ │ ├── generic-error-text.tsx
│ │ │ │ │ ├── icon-button-link.tsx
│ │ │ │ │ ├── icon-button.tsx
│ │ │ │ │ ├── icon.tsx
│ │ │ │ │ ├── immersive-dialog.tsx
│ │ │ │ │ ├── input.tsx
│ │ │ │ │ ├── label.tsx
│ │ │ │ │ ├── list.tsx
│ │ │ │ │ ├── loading.tsx
│ │ │ │ │ ├── notification-badge.tsx
│ │ │ │ │ ├── numbered-list.tsx
│ │ │ │ │ ├── pagination.tsx
│ │ │ │ │ ├── pin-input.tsx
│ │ │ │ │ ├── popover.tsx
│ │ │ │ │ ├── progress.tsx
│ │ │ │ │ ├── radio-group.tsx
│ │ │ │ │ ├── root-error-fallback.tsx
│ │ │ │ │ ├── scroll-area.tsx
│ │ │ │ │ ├── segmented-control.tsx
│ │ │ │ │ ├── separator.tsx
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── dialog.ts
│ │ │ │ │ │ └── menu.ts
│ │ │ │ │ ├── sheet-scroll-area.tsx
│ │ │ │ │ ├── sheet.tsx
│ │ │ │ │ ├── switch.tsx
│ │ │ │ │ ├── table.tsx
│ │ │ │ │ ├── tabs.tsx
│ │ │ │ │ ├── toast.tsx
│ │ │ │ │ └── tooltip.tsx
│ │ │ │ ├── umbrel-logo.tsx
│ │ │ │ └── widget-check-icon.tsx
│ │ │ ├── constants/
│ │ │ │ ├── index.ts
│ │ │ │ └── links.ts
│ │ │ ├── features/
│ │ │ │ ├── backups/
│ │ │ │ │ ├── cmdk-search-provider.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── backup-device-icon.tsx
│ │ │ │ │ │ ├── backup-location-dropdown.tsx
│ │ │ │ │ │ ├── backups-exclusions.tsx
│ │ │ │ │ │ ├── configure-wizard.tsx
│ │ │ │ │ │ ├── floating-island/
│ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ ├── modals/
│ │ │ │ │ │ │ ├── already-configured-modal.tsx
│ │ │ │ │ │ │ └── connect-existing-modal.tsx
│ │ │ │ │ │ ├── restore-location-dropdown.tsx
│ │ │ │ │ │ ├── restore-wizard.tsx
│ │ │ │ │ │ ├── review-card.tsx
│ │ │ │ │ │ ├── setup-wizard.tsx
│ │ │ │ │ │ ├── tab-switcher.tsx
│ │ │ │ │ │ └── tiles.tsx
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── use-apps-auto-excluded-paths.ts
│ │ │ │ │ │ ├── use-apps-backup-ignore.ts
│ │ │ │ │ │ ├── use-backup-ignored-paths.ts
│ │ │ │ │ │ ├── use-backups.ts
│ │ │ │ │ │ └── use-existing-backup-detection.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── backup-location-helpers.ts
│ │ │ │ │ ├── error-messages.ts
│ │ │ │ │ ├── filepath-helpers.ts
│ │ │ │ │ └── sort.ts
│ │ │ │ ├── files/
│ │ │ │ │ ├── assets/
│ │ │ │ │ │ ├── add-folder-icon.tsx
│ │ │ │ │ │ ├── apps-icon.tsx
│ │ │ │ │ │ ├── caret-right.tsx
│ │ │ │ │ │ ├── chevron-left.tsx
│ │ │ │ │ │ ├── chevron-right.tsx
│ │ │ │ │ │ ├── copy-icon.tsx
│ │ │ │ │ │ ├── cursor-text-icon.tsx
│ │ │ │ │ │ ├── empty-folder-icon.tsx
│ │ │ │ │ │ ├── file-items-thumbnails/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── flame-icon.tsx
│ │ │ │ │ │ ├── grid-layout-icon.tsx
│ │ │ │ │ │ ├── home-icon.tsx
│ │ │ │ │ │ ├── list-layout-icon.tsx
│ │ │ │ │ │ ├── recents-icon.tsx
│ │ │ │ │ │ ├── rewind-icon.tsx
│ │ │ │ │ │ ├── search-icon.tsx
│ │ │ │ │ │ ├── shared-folder-badge.tsx
│ │ │ │ │ │ └── trash-icon.tsx
│ │ │ │ │ ├── cmdk-search-provider.tsx
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── cards/
│ │ │ │ │ │ │ └── server-cards.tsx
│ │ │ │ │ │ ├── dialogs/
│ │ │ │ │ │ │ ├── add-network-share-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── external-storage-unsupported-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── format-drive-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── permanently-delete-confirmation-dialog/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── share-info-dialog/
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── platform-instructions/
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ ├── inline-copyable-field.tsx
│ │ │ │ │ │ │ │ ├── instruction.tsx
│ │ │ │ │ │ │ │ ├── ios-instructions.tsx
│ │ │ │ │ │ │ │ ├── macos-instructions.tsx
│ │ │ │ │ │ │ │ ├── umbrelos-instructions.tsx
│ │ │ │ │ │ │ │ └── windows-instructions.tsx
│ │ │ │ │ │ │ ├── platform-selector.tsx
│ │ │ │ │ │ │ └── share-toggle.tsx
│ │ │ │ │ │ ├── embedded/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── file-viewer/
│ │ │ │ │ │ │ ├── audio-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── downloader/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── image-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── pdf-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── video-viewer/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── viewer-wrapper.tsx
│ │ │ │ │ │ ├── files-dnd-wrapper/
│ │ │ │ │ │ │ ├── files-dnd-overlay.tsx
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── floating-islands/
│ │ │ │ │ │ │ ├── audio-island/
│ │ │ │ │ │ │ │ ├── equalizer.tsx
│ │ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ │ ├── formatting-island/
│ │ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ │ ├── operations-island/
│ │ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ │ └── uploading-island/
│ │ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ │ ├── listing/
│ │ │ │ │ │ │ ├── actions-bar/
│ │ │ │ │ │ │ │ ├── actions-bar-context.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ ├── mobile-actions.tsx
│ │ │ │ │ │ │ │ ├── navigation-controls.tsx
│ │ │ │ │ │ │ │ ├── path-bar/
│ │ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ │ ├── path-bar-desktop.tsx
│ │ │ │ │ │ │ │ │ ├── path-bar-mobile.tsx
│ │ │ │ │ │ │ │ │ └── path-input.tsx
│ │ │ │ │ │ │ │ ├── search-input.tsx
│ │ │ │ │ │ │ │ ├── sort-dropdown.tsx
│ │ │ │ │ │ │ │ └── view-toggle.tsx
│ │ │ │ │ │ │ ├── apps-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── directory-listing/
│ │ │ │ │ │ │ │ ├── empty-state.tsx
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── file-item/
│ │ │ │ │ │ │ │ ├── circular-progress.tsx
│ │ │ │ │ │ │ │ ├── editable-name.tsx
│ │ │ │ │ │ │ │ ├── icons-view-file-item.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ ├── list-view-file-item.css
│ │ │ │ │ │ │ │ ├── list-view-file-item.tsx
│ │ │ │ │ │ │ │ └── truncated-filename.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── listing-and-file-item-context-menu.tsx
│ │ │ │ │ │ │ ├── listing-body.tsx
│ │ │ │ │ │ │ ├── marquee-selection.tsx
│ │ │ │ │ │ │ ├── recents-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── search-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ ├── trash-listing/
│ │ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ │ └── virtualized-list.tsx
│ │ │ │ │ │ ├── mini-browser/
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ ├── rewind/
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── overlay-context.tsx
│ │ │ │ │ │ │ ├── prerewind-dialog.tsx
│ │ │ │ │ │ │ ├── restore-grouping.ts
│ │ │ │ │ │ │ ├── restore-progress-dialog.tsx
│ │ │ │ │ │ │ ├── snapshot-carousel.tsx
│ │ │ │ │ │ │ ├── snapshot-date-label.ts
│ │ │ │ │ │ │ ├── timeline-bar.tsx
│ │ │ │ │ │ │ └── tooltip.tsx
│ │ │ │ │ │ ├── shared/
│ │ │ │ │ │ │ ├── circular-progress.tsx
│ │ │ │ │ │ │ ├── drag-and-drop.tsx
│ │ │ │ │ │ │ ├── file-item-icon/
│ │ │ │ │ │ │ │ ├── animated-folder-icon.tsx
│ │ │ │ │ │ │ │ ├── embedded-overlay-icons.tsx
│ │ │ │ │ │ │ │ ├── folder-icon.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── unknown-file-thumbnail.tsx
│ │ │ │ │ │ │ ├── file-upload-drop-zone.tsx
│ │ │ │ │ │ │ └── upload-input.tsx
│ │ │ │ │ │ └── sidebar/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── mobile-sidebar-wrapper.tsx
│ │ │ │ │ │ ├── sidebar-apps.tsx
│ │ │ │ │ │ ├── sidebar-external-storage-item.tsx
│ │ │ │ │ │ ├── sidebar-external-storage.tsx
│ │ │ │ │ │ ├── sidebar-favorites.tsx
│ │ │ │ │ │ ├── sidebar-home.tsx
│ │ │ │ │ │ ├── sidebar-item.tsx
│ │ │ │ │ │ ├── sidebar-network-share-item.tsx
│ │ │ │ │ │ ├── sidebar-network-storage.tsx
│ │ │ │ │ │ ├── sidebar-recents.tsx
│ │ │ │ │ │ ├── sidebar-shares.tsx
│ │ │ │ │ │ └── sidebar-trash.tsx
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── use-drag-and-drop.ts
│ │ │ │ │ │ ├── use-external-storage.ts
│ │ │ │ │ │ ├── use-favorites.ts
│ │ │ │ │ │ ├── use-files-keyboard-shortcuts.ts
│ │ │ │ │ │ ├── use-files-operations.ts
│ │ │ │ │ │ ├── use-home-directory-name.ts
│ │ │ │ │ │ ├── use-is-touch-device.ts
│ │ │ │ │ │ ├── use-item-click.ts
│ │ │ │ │ │ ├── use-list-directory.ts
│ │ │ │ │ │ ├── use-list-recents.ts
│ │ │ │ │ │ ├── use-navigate.ts
│ │ │ │ │ │ ├── use-network-device-type.ts
│ │ │ │ │ │ ├── use-network-storage.ts
│ │ │ │ │ │ ├── use-new-folder.ts
│ │ │ │ │ │ ├── use-preferences.ts
│ │ │ │ │ │ ├── use-rewind-action.ts
│ │ │ │ │ │ ├── use-rewind.ts
│ │ │ │ │ │ ├── use-search-files.ts
│ │ │ │ │ │ └── use-shares.ts
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── providers/
│ │ │ │ │ │ └── files-capabilities-context.tsx
│ │ │ │ │ ├── routes.tsx
│ │ │ │ │ ├── store/
│ │ │ │ │ │ ├── slices/
│ │ │ │ │ │ │ ├── clipboard-slice.ts
│ │ │ │ │ │ │ ├── drag-and-drop-slice.ts
│ │ │ │ │ │ │ ├── file-viewer-slice.ts
│ │ │ │ │ │ │ ├── interaction-slice.ts
│ │ │ │ │ │ │ ├── new-folder-slice.ts
│ │ │ │ │ │ │ ├── rename-slice.ts
│ │ │ │ │ │ │ └── selection-slice.ts
│ │ │ │ │ │ └── use-files-store.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── utils/
│ │ │ │ │ │ ├── error-messages.ts
│ │ │ │ │ │ ├── format-filesystem-date.ts
│ │ │ │ │ │ ├── format-filesystem-name.ts
│ │ │ │ │ │ ├── format-filesystem-size.ts
│ │ │ │ │ │ ├── get-grid-column-count.ts
│ │ │ │ │ │ ├── get-item-key.ts
│ │ │ │ │ │ ├── is-directory-a-network-device-or-share.ts
│ │ │ │ │ │ ├── is-directory-an-external-drive-partition.ts
│ │ │ │ │ │ ├── is-directory-an-umbrel-backup.ts
│ │ │ │ │ │ ├── path-alias.ts
│ │ │ │ │ │ └── sort-filesystem-items.ts
│ │ │ │ │ └── widgets.tsx
│ │ │ │ └── storage/
│ │ │ │ ├── components/
│ │ │ │ │ ├── dialogs/
│ │ │ │ │ │ ├── add-to-raid-dialog.tsx
│ │ │ │ │ │ ├── install-ssd-dialog.tsx
│ │ │ │ │ │ ├── install-tips-collapsible.tsx
│ │ │ │ │ │ ├── operation-in-progress-banner.tsx
│ │ │ │ │ │ ├── replace-failed-drive-dialog.tsx
│ │ │ │ │ │ ├── shutdown-confirmation-dialog.tsx
│ │ │ │ │ │ ├── ssd-health-dialog.tsx
│ │ │ │ │ │ └── swap-dialog.tsx
│ │ │ │ │ ├── floating-island/
│ │ │ │ │ │ ├── data-stream-icon.tsx
│ │ │ │ │ │ ├── expanded.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── minimized.tsx
│ │ │ │ │ ├── ssd-shape.tsx
│ │ │ │ │ ├── storage-donut-chart.tsx
│ │ │ │ │ └── storage-mode-display.tsx
│ │ │ │ ├── hooks/
│ │ │ │ │ ├── use-active-raid-operation.ts
│ │ │ │ │ ├── use-raid-progress.ts
│ │ │ │ │ └── use-storage.ts
│ │ │ │ ├── index.tsx
│ │ │ │ ├── providers/
│ │ │ │ │ └── pending-operation-context.tsx
│ │ │ │ └── utils.ts
│ │ │ ├── hooks/
│ │ │ │ ├── use-2fa.ts
│ │ │ │ ├── use-app-install.ts
│ │ │ │ ├── use-apps-with-updates.ts
│ │ │ │ ├── use-auto-height-animation.tsx
│ │ │ │ ├── use-color-thief.ts
│ │ │ │ ├── use-cpu-temperature.ts
│ │ │ │ ├── use-cpu.ts
│ │ │ │ ├── use-debug-install-random-apps.ts
│ │ │ │ ├── use-device-info.ts
│ │ │ │ ├── use-disk.ts
│ │ │ │ ├── use-is-externaldns.ts
│ │ │ │ ├── use-is-home-or-pro.ts
│ │ │ │ ├── use-is-mobile.ts
│ │ │ │ ├── use-is-umbrel-home.tsx
│ │ │ │ ├── use-is-umbrel-pro.ts
│ │ │ │ ├── use-language.ts
│ │ │ │ ├── use-launch-app.ts
│ │ │ │ ├── use-memory.ts
│ │ │ │ ├── use-notifications.ts
│ │ │ │ ├── use-password.ts
│ │ │ │ ├── use-prefixed-local-storage.ts
│ │ │ │ ├── use-query-params.ts
│ │ │ │ ├── use-scroll-restoration.ts
│ │ │ │ ├── use-settings-notification-count.ts
│ │ │ │ ├── use-software-update.ts
│ │ │ │ ├── use-temperature-unit.ts
│ │ │ │ ├── use-tor-enabled.ts
│ │ │ │ ├── use-update-all-apps.ts
│ │ │ │ ├── use-user-name.ts
│ │ │ │ ├── use-version.ts
│ │ │ │ └── use-widgets.ts
│ │ │ ├── index.css
│ │ │ ├── init.tsx
│ │ │ ├── layouts/
│ │ │ │ ├── README.md
│ │ │ │ ├── app-store.tsx
│ │ │ │ ├── bare/
│ │ │ │ │ ├── bare-page.tsx
│ │ │ │ │ ├── bare.tsx
│ │ │ │ │ ├── onboarding-page.tsx
│ │ │ │ │ ├── onboarding.tsx
│ │ │ │ │ └── shared.tsx
│ │ │ │ ├── desktop.tsx
│ │ │ │ └── sheet.tsx
│ │ │ ├── lib/
│ │ │ │ └── utils.ts
│ │ │ ├── main.tsx
│ │ │ ├── modules/
│ │ │ │ ├── app-store/
│ │ │ │ │ ├── app-page/
│ │ │ │ │ │ ├── about-section.tsx
│ │ │ │ │ │ ├── app-content.tsx
│ │ │ │ │ │ ├── app-settings-dialog.tsx
│ │ │ │ │ │ ├── default-credentials-dialog.tsx
│ │ │ │ │ │ ├── dependencies.tsx
│ │ │ │ │ │ ├── get-recommendations.ts
│ │ │ │ │ │ ├── info-section.tsx
│ │ │ │ │ │ ├── recommendations-section.tsx
│ │ │ │ │ │ ├── release-notes-section.tsx
│ │ │ │ │ │ ├── settings-section.tsx
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ └── top-header.tsx
│ │ │ │ │ ├── app-store-nav.tsx
│ │ │ │ │ ├── community-app-store-dialog.tsx
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── discover/
│ │ │ │ │ │ ├── apps-grid-section.tsx
│ │ │ │ │ │ ├── apps-row-section.tsx
│ │ │ │ │ │ └── apps-three-column-section.tsx
│ │ │ │ │ ├── gallery-section.tsx
│ │ │ │ │ ├── os-update-required.tsx
│ │ │ │ │ ├── select-dependencies-dialog.tsx
│ │ │ │ │ ├── shared.tsx
│ │ │ │ │ ├── updates-button.tsx
│ │ │ │ │ ├── updates-dialog.tsx
│ │ │ │ │ └── utils.ts
│ │ │ │ ├── auth/
│ │ │ │ │ ├── ensure-backend-available.tsx
│ │ │ │ │ ├── ensure-logged-in.tsx
│ │ │ │ │ ├── ensure-no-raid-mount-failure.tsx
│ │ │ │ │ ├── ensure-pro-device.tsx
│ │ │ │ │ ├── ensure-user-exists.tsx
│ │ │ │ │ ├── redirects.tsx
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ └── use-auth.tsx
│ │ │ │ ├── bare/
│ │ │ │ │ ├── alert.tsx
│ │ │ │ │ ├── failed-layout.tsx
│ │ │ │ │ ├── progress-layout.tsx
│ │ │ │ │ ├── progress.tsx
│ │ │ │ │ ├── shared.tsx
│ │ │ │ │ └── success-layout.tsx
│ │ │ │ ├── community-app-store/
│ │ │ │ │ └── community-badge.tsx
│ │ │ │ ├── desktop/
│ │ │ │ │ ├── app-grid/
│ │ │ │ │ │ ├── app-grid.tsx
│ │ │ │ │ │ ├── app-pagination-utils.tsx
│ │ │ │ │ │ └── paginator.tsx
│ │ │ │ │ ├── app-icon.tsx
│ │ │ │ │ ├── blur-below-dock.tsx
│ │ │ │ │ ├── desktop-content.tsx
│ │ │ │ │ ├── desktop-context-menu.tsx
│ │ │ │ │ ├── desktop-misc.tsx
│ │ │ │ │ ├── desktop-preview.tsx
│ │ │ │ │ ├── dock-item.tsx
│ │ │ │ │ ├── dock.tsx
│ │ │ │ │ ├── greeting-message.ts
│ │ │ │ │ ├── header.tsx
│ │ │ │ │ ├── install-first-app.tsx
│ │ │ │ │ ├── logout-dialog.tsx
│ │ │ │ │ ├── uninstall-confirmation-dialog.tsx
│ │ │ │ │ └── uninstall-these-first-dialog.tsx
│ │ │ │ ├── floating-island/
│ │ │ │ │ ├── bare-island.tsx
│ │ │ │ │ └── container.tsx
│ │ │ │ ├── immersive-picker/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── migrate/
│ │ │ │ │ ├── migrate-image.tsx
│ │ │ │ │ └── migrate-inner.tsx
│ │ │ │ ├── sheet-top-fixed.tsx
│ │ │ │ ├── widgets/
│ │ │ │ │ ├── four-stats-widget.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── list-emoji-widget.tsx
│ │ │ │ │ ├── list-widget.tsx
│ │ │ │ │ ├── shared/
│ │ │ │ │ │ ├── backdrop-blur-context.tsx
│ │ │ │ │ │ ├── constants.ts
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ ├── stat-text.tsx
│ │ │ │ │ │ ├── tabler-icon.tsx
│ │ │ │ │ │ └── widget-wrapper.tsx
│ │ │ │ │ ├── text-with-buttons-widget.tsx
│ │ │ │ │ ├── text-with-progress-widget.tsx
│ │ │ │ │ ├── three-stats-widget.tsx
│ │ │ │ │ └── two-stats-with-guage-widget.tsx
│ │ │ │ └── wifi/
│ │ │ │ ├── desktop-wifi-button-connected.tsx
│ │ │ │ ├── icon.tsx
│ │ │ │ ├── wifi-drawer-or-dialog.tsx
│ │ │ │ ├── wifi-item-content.tsx
│ │ │ │ └── wifi-list-row-connected-description.tsx
│ │ │ ├── providers/
│ │ │ │ ├── apps.tsx
│ │ │ │ ├── auth-bootstrap.tsx
│ │ │ │ ├── available-apps.tsx
│ │ │ │ ├── confirmation/
│ │ │ │ │ ├── confirmation-context.tsx
│ │ │ │ │ ├── confirmation-provider.tsx
│ │ │ │ │ ├── generic-confirmation-dialog.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── use-confirmation.ts
│ │ │ │ ├── global-files.tsx
│ │ │ │ ├── global-system-state/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── migrate.tsx
│ │ │ │ │ ├── reset.tsx
│ │ │ │ │ ├── restart.tsx
│ │ │ │ │ ├── restore.tsx
│ │ │ │ │ ├── shutdown.tsx
│ │ │ │ │ └── update.tsx
│ │ │ │ ├── immersive-dialog.tsx
│ │ │ │ ├── language.tsx
│ │ │ │ ├── prefetch.tsx
│ │ │ │ ├── sheet-sticky-header.tsx
│ │ │ │ └── wallpaper.tsx
│ │ │ ├── router.tsx
│ │ │ ├── routes/
│ │ │ │ ├── README.md
│ │ │ │ ├── app-store/
│ │ │ │ │ ├── app-page/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ ├── category-page.tsx
│ │ │ │ │ ├── discover.tsx
│ │ │ │ │ └── use-discover-query.tsx
│ │ │ │ ├── community-app-store/
│ │ │ │ │ ├── app-page/
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── edit-widgets/
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ └── widget-selector.tsx
│ │ │ │ ├── factory-reset/
│ │ │ │ │ ├── _components/
│ │ │ │ │ │ ├── confirm-with-password.tsx
│ │ │ │ │ │ ├── misc.tsx
│ │ │ │ │ │ └── review-data.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── live-usage.tsx
│ │ │ │ ├── login.tsx
│ │ │ │ ├── not-found.tsx
│ │ │ │ ├── notifications.tsx
│ │ │ │ ├── onboarding/
│ │ │ │ │ ├── account-created.tsx
│ │ │ │ │ ├── create-account.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── onboarding-footer.tsx
│ │ │ │ │ ├── raid/
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── raid-error.tsx
│ │ │ │ │ │ ├── setup.tsx
│ │ │ │ │ │ ├── ssd-health-dialog.tsx
│ │ │ │ │ │ ├── ssd-tray.tsx
│ │ │ │ │ │ └── use-raid-setup.ts
│ │ │ │ │ ├── restore.tsx
│ │ │ │ │ └── use-onboarding-device.ts
│ │ │ │ ├── raid-error/
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── settings/
│ │ │ │ │ ├── 2fa-disable.tsx
│ │ │ │ │ ├── 2fa-enable.tsx
│ │ │ │ │ ├── 2fa.tsx
│ │ │ │ │ ├── _components/
│ │ │ │ │ │ ├── app-store-preferences-content.tsx
│ │ │ │ │ │ ├── cpu-card-content.tsx
│ │ │ │ │ │ ├── cpu-temperature-card-content.tsx
│ │ │ │ │ │ ├── device-info-content.tsx
│ │ │ │ │ │ ├── device-info-umbrel-home.tsx
│ │ │ │ │ │ ├── device-info-umbrel-pro.tsx
│ │ │ │ │ │ ├── language-dropdown.tsx
│ │ │ │ │ │ ├── laser-engraving.tsx
│ │ │ │ │ │ ├── list-row.tsx
│ │ │ │ │ │ ├── memory-card-content.tsx
│ │ │ │ │ │ ├── no-forgot-password-message.tsx
│ │ │ │ │ │ ├── progress-card-content.tsx
│ │ │ │ │ │ ├── settings-content-mobile.tsx
│ │ │ │ │ │ ├── settings-content.tsx
│ │ │ │ │ │ ├── settings-summary.tsx
│ │ │ │ │ │ ├── shared.tsx
│ │ │ │ │ │ ├── software-update-list-row.tsx
│ │ │ │ │ │ ├── storage-card-content.tsx
│ │ │ │ │ │ └── wallpaper-picker.tsx
│ │ │ │ │ ├── advanced.tsx
│ │ │ │ │ ├── app-store-preferences.tsx
│ │ │ │ │ ├── change-name.tsx
│ │ │ │ │ ├── change-password.tsx
│ │ │ │ │ ├── device-info.tsx
│ │ │ │ │ ├── file-sharing.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── migration-assistant.tsx
│ │ │ │ │ ├── mobile/
│ │ │ │ │ │ ├── account.tsx
│ │ │ │ │ │ ├── app-store-preferences.tsx
│ │ │ │ │ │ ├── backups-mobile-drawer.tsx
│ │ │ │ │ │ ├── device-info.tsx
│ │ │ │ │ │ ├── language.tsx
│ │ │ │ │ │ ├── software-update.tsx
│ │ │ │ │ │ ├── start-migration-drawer-or-dialog.tsx
│ │ │ │ │ │ ├── tor.tsx
│ │ │ │ │ │ └── wallpaper.tsx
│ │ │ │ │ ├── restart.tsx
│ │ │ │ │ ├── shutdown.tsx
│ │ │ │ │ ├── software-update-confirm.tsx
│ │ │ │ │ ├── terminal/
│ │ │ │ │ │ ├── _shared.tsx
│ │ │ │ │ │ ├── app.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── umbrelos.tsx
│ │ │ │ │ ├── troubleshoot/
│ │ │ │ │ │ ├── _shared.tsx
│ │ │ │ │ │ ├── app.tsx
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ └── umbrelos.tsx
│ │ │ │ │ ├── wifi-unsupported.tsx
│ │ │ │ │ └── wifi.tsx
│ │ │ │ └── whats-new-modal.tsx
│ │ │ ├── trpc/
│ │ │ │ ├── loading-indicator.tsx
│ │ │ │ ├── trpc-provider.tsx
│ │ │ │ └── trpc.ts
│ │ │ ├── types.d.ts
│ │ │ └── utils/
│ │ │ ├── call-every-interval.ts
│ │ │ ├── date-time.ts
│ │ │ ├── dialog.ts
│ │ │ ├── element-classes.ts
│ │ │ ├── i18n.ts
│ │ │ ├── language.ts
│ │ │ ├── logs.ts
│ │ │ ├── misc.ts
│ │ │ ├── number.ts
│ │ │ ├── pretty-bytes.ts
│ │ │ ├── search.ts
│ │ │ ├── seconds-to-eta.ts
│ │ │ ├── system.ts
│ │ │ ├── temperature.ts
│ │ │ ├── tw.ts
│ │ │ └── wifi.ts
│ │ ├── tailwind.config.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ ├── update-translations.js
│ │ └── vite.config.ts
│ └── umbreld/
│ ├── .gitignore
│ ├── .prettierignore
│ ├── package.json
│ ├── scripts/
│ │ └── validate-manifests.ts
│ ├── source/
│ │ ├── cli.ts
│ │ ├── constants.ts
│ │ ├── index.ts
│ │ └── modules/
│ │ ├── apps/
│ │ │ ├── app-repository.integration.test.ts
│ │ │ ├── app-repository.ts
│ │ │ ├── app-store.integration.test.ts
│ │ │ ├── app-store.ts
│ │ │ ├── app.ts
│ │ │ ├── apps.integration.test.ts
│ │ │ ├── apps.ts
│ │ │ ├── legacy-compat/
│ │ │ │ ├── app-environment.ts
│ │ │ │ ├── app-script
│ │ │ │ ├── app-script.ts
│ │ │ │ ├── bin/
│ │ │ │ │ ├── bitcoin-cli
│ │ │ │ │ └── lncli
│ │ │ │ ├── docker-compose.app_proxy.yml
│ │ │ │ ├── docker-compose.common.yml
│ │ │ │ ├── docker-compose.tor.yml
│ │ │ │ ├── docker-compose.yml
│ │ │ │ ├── tor-entrypoint.sh
│ │ │ │ ├── tor-proxy-torrc
│ │ │ │ └── tor-server-torrc
│ │ │ ├── routes.ts
│ │ │ └── schema.ts
│ │ ├── backups/
│ │ │ ├── backups.backupProgress.test.ts
│ │ │ ├── backups.integration.test.ts
│ │ │ ├── backups.ts
│ │ │ └── routes.ts
│ │ ├── blacklist-uas/
│ │ │ └── blacklist-uas.ts
│ │ ├── cli-client.ts
│ │ ├── dbus/
│ │ │ └── dbus.ts
│ │ ├── development.ts
│ │ ├── event-bus/
│ │ │ ├── event-bus.ts
│ │ │ └── routes.ts
│ │ ├── files/
│ │ │ ├── api.download.integration.test.ts
│ │ │ ├── api.thumbnail.integration.test.ts
│ │ │ ├── api.ts
│ │ │ ├── api.upload.integration.test.ts
│ │ │ ├── api.view.integration.test.ts
│ │ │ ├── archive.integration.test.ts
│ │ │ ├── archive.ts
│ │ │ ├── external-storage.integration.test.ts
│ │ │ ├── external-storage.ts
│ │ │ ├── favorites.integration.test.ts
│ │ │ ├── favorites.ts
│ │ │ ├── files-reflink-copy.vm.test.ts
│ │ │ ├── files.copy.integration.test.ts
│ │ │ ├── files.createDirectory.integration.test.ts
│ │ │ ├── files.delete.test.ts
│ │ │ ├── files.emptyTrash.test.ts
│ │ │ ├── files.list.integration.test.ts
│ │ │ ├── files.move.integration.test.ts
│ │ │ ├── files.operationProgress.test.ts
│ │ │ ├── files.preferences.integration.test.ts
│ │ │ ├── files.rename.integration.test.ts
│ │ │ ├── files.restore.test.ts
│ │ │ ├── files.trash.test.ts
│ │ │ ├── files.ts
│ │ │ ├── fixtures/
│ │ │ │ └── thumbnails/
│ │ │ │ └── master-lossless-video.mkv
│ │ │ ├── network-storage.integration.test.ts
│ │ │ ├── network-storage.ts
│ │ │ ├── recents.test.ts
│ │ │ ├── recents.ts
│ │ │ ├── routes.ts
│ │ │ ├── samba.integration.test.ts
│ │ │ ├── samba.ts
│ │ │ ├── search.integration.test.ts
│ │ │ ├── search.ts
│ │ │ ├── thumbnails.integration.test.ts
│ │ │ ├── thumbnails.ts
│ │ │ ├── watcher.ts
│ │ │ └── widgets.ts
│ │ ├── hardware/
│ │ │ ├── hardware.ts
│ │ │ ├── internal-storage-rounding.vm.test.ts
│ │ │ ├── internal-storage-slot-detection.vm.test.ts
│ │ │ ├── internal-storage.ts
│ │ │ ├── raid-failsafe-space-reporting.vm.test.ts
│ │ │ ├── raid-failsafe.vm.test.ts
│ │ │ ├── raid-foreign-pool.vm.test.ts
│ │ │ ├── raid-operations-different-sizes.vm.test.ts
│ │ │ ├── raid-preused-drives.vm.test.ts
│ │ │ ├── raid-recovery-mode.vm.test.ts
│ │ │ ├── raid-replace-larger-capacity.vm.test.ts
│ │ │ ├── raid-replace.vm.test.ts
│ │ │ ├── raid-size-rounding.vm.test.ts
│ │ │ ├── raid-slot-swap.vm.test.ts
│ │ │ ├── raid-storage.vm.test.ts
│ │ │ ├── raid-transition-different-size.vm.test.ts
│ │ │ ├── raid-transition-full-storage.vm.test.ts
│ │ │ ├── raid-transition.vm.test.ts
│ │ │ ├── raid.getRoundedDeviceSize.unit.test.ts
│ │ │ ├── raid.ts
│ │ │ ├── routes.ts
│ │ │ └── umbrel-pro.ts
│ │ ├── is-umbrel-home.ts
│ │ ├── jwt.ts
│ │ ├── migration/
│ │ │ ├── migration.ts
│ │ │ └── routes.ts
│ │ ├── notifications/
│ │ │ ├── notifications.integration.test.ts
│ │ │ ├── notifications.ts
│ │ │ └── routes.ts
│ │ ├── server/
│ │ │ ├── index.ts
│ │ │ ├── terminal-socket.ts
│ │ │ └── trpc/
│ │ │ ├── common.ts
│ │ │ ├── context.ts
│ │ │ ├── index.ts
│ │ │ ├── is-authenticated.ts
│ │ │ ├── trpc.ts
│ │ │ └── websocket-logger.ts
│ │ ├── startup-migrations/
│ │ │ ├── index.ts
│ │ │ └── startup-migrations.integration.test.ts
│ │ ├── system/
│ │ │ ├── factory-reset.ts
│ │ │ ├── routes.ts
│ │ │ ├── system-widgets.ts
│ │ │ ├── system.integration.test.ts
│ │ │ ├── system.ts
│ │ │ ├── system.unit.test.ts
│ │ │ ├── update.ts
│ │ │ └── wifi-routes.ts
│ │ ├── test-utilities/
│ │ │ ├── create-test-umbreld.ts
│ │ │ ├── fixtures/
│ │ │ │ ├── another-community-repo/
│ │ │ │ │ ├── another-sparkles-hello-world/
│ │ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ │ └── umbrel-app.yml
│ │ │ │ │ └── umbrel-app-store.yml
│ │ │ │ └── community-repo/
│ │ │ │ ├── app-with-invalid-id/
│ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ └── umbrel-app.yml
│ │ │ │ ├── app-with-invalid-manifest/
│ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ └── umbrel-app.yml
│ │ │ │ ├── sparkles-hello-world/
│ │ │ │ │ ├── docker-compose.yml
│ │ │ │ │ └── umbrel-app.yml
│ │ │ │ └── umbrel-app-store.yml
│ │ │ └── run-git-server.ts
│ │ ├── user/
│ │ │ ├── routes.ts
│ │ │ ├── user.integration.test.ts
│ │ │ └── user.ts
│ │ ├── utilities/
│ │ │ ├── copy-with-progress.ts
│ │ │ ├── dependencies.ts
│ │ │ ├── docker-pull.ts
│ │ │ ├── file-store.integration.test.ts
│ │ │ ├── file-store.ts
│ │ │ ├── get-directory-size.ts
│ │ │ ├── get-or-create-file.ts
│ │ │ ├── logger.ts
│ │ │ ├── package-directory.ts
│ │ │ ├── random-token.ts
│ │ │ ├── regexp.ts
│ │ │ ├── run-every.ts
│ │ │ ├── temporary-directory.ts
│ │ │ └── totp.ts
│ │ └── widgets/
│ │ ├── routes.ts
│ │ └── widget.integration.test.ts
│ ├── trigger-change
│ ├── tsconfig.json
│ └── umbreld
└── scripts/
├── data-export
├── install
├── remote-builder
├── umbrel-dev
└── update-script
SYMBOL INDEX (1857 symbols across 573 files)
FILE: containers/app-auth/middleware/handle_error.js
function handleError (line 1) | function handleError(error, req, res, next) {
FILE: containers/app-auth/middleware/validate_token.js
constant CONSTANTS (line 8) | const CONSTANTS = require('../utils/const.js');
constant APP_PROXY_AUTH_TOKEN_PATH (line 10) | const APP_PROXY_AUTH_TOKEN_PATH = "/umbrel_/api/v1/auth/token";
function redirectState (line 12) | async function redirectState(token, req) {
function redirect (line 39) | async function redirect(res, token, req) {
function mw (line 43) | function mw () {
FILE: containers/app-auth/utils/app.js
constant CONSTANTS (line 5) | const CONSTANTS = require('./const.js');
function getBasicInfo (line 7) | async function getBasicInfo(app){
function sanitiseId (line 24) | function sanitiseId(appId){
FILE: containers/app-auth/utils/const.js
function readFromEnvOrTerminate (line 1) | function readFromEnvOrTerminate(key) {
FILE: containers/app-auth/utils/dashboard.js
constant CONSTANTS (line 4) | const CONSTANTS = require('./const.js');
FILE: containers/app-auth/utils/express.js
function getQueryParam (line 1) | function getQueryParam(req, key) {
FILE: containers/app-auth/utils/hmac.js
function sign (line 3) | function sign(input, secret) {
function verify (line 10) | function verify(input, secret, signature){
FILE: containers/app-auth/utils/host_resolution.js
constant CONSTANTS (line 5) | const CONSTANTS = require('./const.js');
function getTorHostname (line 7) | async function getTorHostname(app) {
function getAppPort (line 13) | async function getAppPort(app) {
function host (line 21) | async function host(req, app, origin) {
FILE: containers/app-auth/utils/manager.js
constant CONSTANTS (line 4) | const CONSTANTS = require('./const.js');
FILE: containers/app-auth/utils/safe_handler.js
function safeHandler (line 5) | function safeHandler(handler) {
FILE: containers/app-auth/utils/token.js
constant JWT_ALGORITHM (line 3) | const JWT_ALGORITHM = "HS256";
function validate (line 7) | function validate(token) {
FILE: containers/app-proxy/middleware/handle_error.js
function handleError (line 1) | function handleError(error, req, res, next) {
FILE: containers/app-proxy/routes/umbrel.js
constant CONSTANTS (line 8) | const CONSTANTS = require("../utils/const.js");
constant ONE_SECOND (line 13) | const ONE_SECOND = 1000;
constant ONE_MINUTE (line 14) | const ONE_MINUTE = 60 * ONE_SECOND;
constant ONE_HOUR (line 15) | const ONE_HOUR = 60 * ONE_MINUTE;
constant ONE_DAY (line 16) | const ONE_DAY = 24 * ONE_HOUR;
constant ONE_WEEK (line 17) | const ONE_WEEK = 7 * ONE_DAY;
FILE: containers/app-proxy/utils/const.js
constant APP_MANIFEST_FILE (line 4) | const APP_MANIFEST_FILE = process.env.APP_MANIFEST_FILE || "/extra/umbre...
constant CUSTOM_DOTENV_FILE (line 5) | const CUSTOM_DOTENV_FILE = process.env.CUSTOM_DOTENV_FILE || "/data/.env...
function readUmbrelAppManifest (line 14) | function readUmbrelAppManifest() {
function readFromEnvOrTerminate (line 24) | function readFromEnvOrTerminate(key) {
function cleanHttpPaths (line 36) | function cleanHttpPaths(str) {
FILE: containers/app-proxy/utils/express.js
function removeCookie (line 1) | function removeCookie(req, cookieName) {
FILE: containers/app-proxy/utils/hmac.js
function sign (line 3) | function sign(input, secret) {
function verify (line 10) | function verify(input, secret, signature){
FILE: containers/app-proxy/utils/manager.js
constant CONSTANTS (line 4) | const CONSTANTS = require('./const.js');
FILE: containers/app-proxy/utils/proxy.js
constant CONSTANTS (line 8) | const CONSTANTS = require("./const.js");
function onProxyReq (line 11) | function onProxyReq(proxyReq, req, res, config) {
function onError (line 38) | function onError(err, req, res, target) {
function proxy (line 52) | function proxy() {
function whitelist (line 78) | function whitelist() {
function blacklist (line 86) | function blacklist() {
function apply (line 94) | function apply(app) {
FILE: containers/app-proxy/utils/safe_handler.js
function safeHandler (line 5) | function safeHandler(handler) {
FILE: containers/app-proxy/utils/token.js
constant JWT_ALGORITHM (line 3) | const JWT_ALGORITHM = "HS256";
function validate (line 7) | function validate(token) {
FILE: containers/app-proxy/utils/tor.js
constant CONSTANTS (line 3) | const CONSTANTS = require("./const.js");
function authHsUrl (line 5) | async function authHsUrl() {
FILE: packages/ui/app-auth/src/login-with-umbrel.tsx
type Step (line 15) | type Step = 'password' | '2fa'
function LoginWithUmbrel (line 17) | function LoginWithUmbrel() {
function useLogin (line 85) | function useLogin() {
type App (line 148) | type App = {
function useApp (line 154) | function useApp(appId: string) {
function useWallpaperId (line 169) | function useWallpaperId() {
function LoginWithLayout (line 188) | function LoginWithLayout({children}: {children: ReactNode}) {
FILE: packages/ui/src/components/app-icon.tsx
type AppIconProps (line 6) | type AppIconProps = {src?: string; size?: number; ref?: React.Ref<HTMLIm...
function AppIcon (line 8) | function AppIcon({src, style, size, className, ref, ...props}: AppIconPr...
FILE: packages/ui/src/components/chevron-down.tsx
function ChevronDown (line 4) | function ChevronDown() {
FILE: packages/ui/src/components/cmdk-providers.tsx
type CmdkSearchProviderProps (line 35) | interface CmdkSearchProviderProps {
type CmdkSearchProvider (line 42) | type CmdkSearchProvider = React.FC<CmdkSearchProviderProps>
FILE: packages/ui/src/components/cmdk.tsx
function useCmdkOpen (line 38) | function useCmdkOpen() {
function CmdkProvider (line 46) | function CmdkProvider({children}: {children: React.ReactNode}) {
function CmdkMenu (line 65) | function CmdkMenu() {
function CmdkContent (line 79) | function CmdkContent() {
function FrequentApps (line 314) | function FrequentApps({onLaunchApp}: {onLaunchApp: () => void}) {
function appsByFrequency (line 356) | function appsByFrequency(lastOpenedApps: string[], count: number) {
function FrequentApp (line 375) | function FrequentApp({
function appStateToString (line 448) | function appStateToString(appState: AppState) {
FILE: packages/ui/src/components/darken-layer.tsx
function DarkenLayer (line 6) | function DarkenLayer({className}: {className?: string}) {
FILE: packages/ui/src/components/fade-scroller.tsx
type FadeScrollerProps (line 4) | type FadeScrollerProps = ComponentPropsWithoutRef<'div'> & {
constant FADE_SCROLLER_CLASS_X (line 10) | const FADE_SCROLLER_CLASS_X = 'umbrel-fade-scroller-x'
constant FADE_SCROLLER_CLASS_Y (line 11) | const FADE_SCROLLER_CLASS_Y = 'umbrel-fade-scroller-y'
function useFadeScroller (line 14) | function useFadeScroller(direction: 'x' | 'y', debug?: boolean) {
function FadeScroller (line 75) | function FadeScroller({direction, debug, className, ref, ...props}: Fade...
FILE: packages/ui/src/components/iframe-checker.tsx
function IframeChecker (line 3) | function IframeChecker({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/components/install-button-connected.tsx
function InstallButtonConnected (line 17) | function InstallButtonConnected({app, ref}: {app: RegistryApp; ref?: Rea...
FILE: packages/ui/src/components/install-button.tsx
type Props (line 15) | type Props = {
function InstallButton (line 24) | function InstallButton({installSize, progress, state, onInstallClick, on...
function ButtonContentForState (line 51) | function ButtonContentForState({
FILE: packages/ui/src/components/markdown.tsx
function Markdown (line 12) | function Markdown({className, ...props}: React.ComponentProps<typeof Mar...
FILE: packages/ui/src/components/onboarding-background.tsx
function OnboardingBackground (line 7) | function OnboardingBackground({className}: {className?: string}) {
FILE: packages/ui/src/components/progress-button.tsx
type Props (line 22) | type Props = {
function ProgressButton (line 29) | function ProgressButton({variant, size, progress, state, children, class...
FILE: packages/ui/src/components/ui/alert-dialog.tsx
function useDialogState (line 27) | function useDialogState() {
function AlertDialogOverlay (line 53) | function AlertDialogOverlay({
function AlertDialogContent (line 69) | function AlertDialogContent({
function AlertDialogTitle (line 117) | function AlertDialogTitle({
function AlertDialogDescription (line 136) | function AlertDialogDescription({
function AlertDialogAction (line 155) | function AlertDialogAction({
function AlertDialogCancel (line 170) | function AlertDialogCancel({
FILE: packages/ui/src/components/ui/alert.tsx
function ErrorAlert (line 7) | function ErrorAlert({
type AlertProps (line 53) | type AlertProps = React.HTMLAttributes<HTMLDivElement> &
function Alert (line 59) | function Alert({className, variant, icon, children, ref, ...props}: Aler...
function WarningAlert (line 72) | function WarningAlert({
FILE: packages/ui/src/components/ui/animated-number.tsx
type CounterProps (line 5) | type CounterProps = {
function AnimatedNumber (line 9) | function AnimatedNumber({to}: CounterProps) {
FILE: packages/ui/src/components/ui/arc.tsx
type ProgressArcProps (line 3) | interface ProgressArcProps {
FILE: packages/ui/src/components/ui/badge.tsx
type BadgeProps (line 25) | interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, Varia...
function Badge (line 29) | function Badge({className, variant, icon, children, ...props}: BadgeProp...
FILE: packages/ui/src/components/ui/button-link.tsx
type CustomProps (line 9) | type CustomProps = VariantProps<typeof buttonVariants>
type ButtonLinkProps (line 11) | type ButtonLinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, key...
function ButtonLink (line 18) | function ButtonLink({className, variant, text, size, ref, ...props}: But...
FILE: packages/ui/src/components/ui/button.tsx
type ButtonProps (line 53) | interface ButtonProps
function Button (line 58) | function Button({
FILE: packages/ui/src/components/ui/card.tsx
function Card (line 6) | function Card({
FILE: packages/ui/src/components/ui/carousel.tsx
type CarouselApi (line 8) | type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters (line 9) | type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions (line 10) | type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin (line 11) | type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps (line 13) | type CarouselProps = {
type CarouselContextProps (line 20) | type CarouselContextProps = {
function useCarousel (line 31) | function useCarousel() {
function Carousel (line 41) | function Carousel({
function CarouselContent (line 140) | function CarouselContent({
function CarouselItem (line 158) | function CarouselItem({
function CarouselPrevious (line 176) | function CarouselPrevious({
function CarouselNext (line 207) | function CarouselNext({
FILE: packages/ui/src/components/ui/checkbox.tsx
function Checkbox (line 8) | function Checkbox({
FILE: packages/ui/src/components/ui/command.tsx
function Command (line 16) | function Command({
type CommandDialogProps (line 28) | type CommandDialogProps = DialogProps
function CommandInput (line 55) | function CommandInput({
function CommandList (line 77) | function CommandList({
function CommandEmpty (line 94) | function CommandEmpty({
function CommandGroup (line 103) | function CommandGroup({
function CommandSeparator (line 113) | function CommandSeparator({
type CommandItemIcon (line 124) | type CommandItemIcon = string | React.ReactNode
function CommandItem (line 126) | function CommandItem({
function BlurOverlay (line 184) | function BlurOverlay({ref}: {ref?: React.Ref<HTMLDivElement>}) {
FILE: packages/ui/src/components/ui/context-menu.tsx
function ContextMenuSubTrigger (line 21) | function ContextMenuSubTrigger({
function ContextMenuSubContent (line 43) | function ContextMenuSubContent({
function ContextMenuContent (line 64) | function ContextMenuContent({
function ContextMenuItem (line 87) | function ContextMenuItem({
function ContextMenuCheckboxItem (line 105) | function ContextMenuCheckboxItem({
function ContextMenuRadioItem (line 131) | function ContextMenuRadioItem({
function ContextMenuLabel (line 151) | function ContextMenuLabel({
function ContextMenuSeparator (line 173) | function ContextMenuSeparator({
FILE: packages/ui/src/components/ui/copy-button.tsx
function CopyButton (line 9) | function CopyButton({value}: {value: string}) {
FILE: packages/ui/src/components/ui/copyable-field.tsx
function CopyableField (line 11) | function CopyableField({
FILE: packages/ui/src/components/ui/cover-message.tsx
function useDelayedShow (line 11) | function useDelayedShow(ms: number) {
function BareCoverMessage (line 21) | function BareCoverMessage({
function CoverMessage (line 42) | function CoverMessage({
constant COVER_MESSAGE_TARGET_ID (line 70) | const COVER_MESSAGE_TARGET_ID = 'cover-message-id'
function CoverMessageTarget (line 72) | function CoverMessageTarget() {
function CoverMessageContent (line 75) | function CoverMessageContent({children}: {children: React.ReactNode}) {
function CoverMessageParagraph (line 80) | function CoverMessageParagraph({children, className}: {children: React.R...
FILE: packages/ui/src/components/ui/debug-only.tsx
function DebugOnly (line 3) | function DebugOnly({children}: {children: React.ReactNode}) {
function DebugOnlyBare (line 15) | function DebugOnlyBare({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/components/ui/dialog.tsx
function DialogOverlay (line 23) | function DialogOverlay({
function DialogContent (line 33) | function DialogContent({
function DialogTitle (line 96) | function DialogTitle({
function DialogDescription (line 112) | function DialogDescription({
FILE: packages/ui/src/components/ui/drawer.tsx
function DrawerOverlay (line 17) | function DrawerOverlay({
function DrawerContent (line 27) | function DrawerContent({
function DrawerTitle (line 71) | function DrawerTitle({
function DrawerDescription (line 81) | function DrawerDescription({
function DrawerScroller (line 98) | function DrawerScroller({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/components/ui/dropdown-menu.tsx
function DropdownMenuSubTrigger (line 21) | function DropdownMenuSubTrigger({
function DropdownMenuSubContent (line 43) | function DropdownMenuSubContent({
function DropdownMenuContent (line 64) | function DropdownMenuContent({
function DropdownMenuItem (line 89) | function DropdownMenuItem({
function DropdownMenuCheckboxItem (line 107) | function DropdownMenuCheckboxItem({
function DropdownMenuRadioItem (line 131) | function DropdownMenuRadioItem({
function DropdownMenuLabel (line 151) | function DropdownMenuLabel({
function DropdownMenuSeparator (line 169) | function DropdownMenuSeparator({
FILE: packages/ui/src/components/ui/error-boundary-card-fallback.tsx
function useRouteErrorSafe (line 9) | function useRouteErrorSafe() {
function ErrorBoundaryCardFallback (line 20) | function ErrorBoundaryCardFallback({error, resetErrorBoundary}: Partial<...
FILE: packages/ui/src/components/ui/error-boundary-page-fallback.tsx
function useRouteErrorSafe (line 23) | function useRouteErrorSafe() {
function getErrorMessage (line 31) | function getErrorMessage(error: unknown): string {
function ErrorBoundaryPageFallback (line 40) | function ErrorBoundaryPageFallback({error}: Partial<FallbackProps> = {}) {
FILE: packages/ui/src/components/ui/fade-in-img.tsx
function FadeInImg (line 5) | function FadeInImg({src, alt, className, ...props}: React.ImgHTMLAttribu...
FILE: packages/ui/src/components/ui/form.tsx
type FormFieldContextValue (line 19) | type FormFieldContextValue<
type FormItemContextValue (line 64) | type FormItemContextValue = {
function FormItem (line 70) | function FormItem({className, ...props}: React.ComponentProps<'div'>) {
function FormLabel (line 80) | function FormLabel({className, ...props}: React.ComponentProps<typeof La...
function FormControl (line 94) | function FormControl({...props}: React.ComponentProps<typeof Slot>) {
function FormDescription (line 108) | function FormDescription({className, ...props}: React.ComponentProps<'p'...
function FormMessage (line 121) | function FormMessage({className, ...props}: React.ComponentProps<'p'>) {
FILE: packages/ui/src/components/ui/generic-error-text.tsx
function getErrorMessage (line 6) | function getErrorMessage(error: unknown): string {
function GenericErrorText (line 12) | function GenericErrorText({error}: {error?: unknown}) {
function GenericErrorDetails (line 37) | function GenericErrorDetails({error}: {error: unknown}) {
FILE: packages/ui/src/components/ui/icon-button-link.tsx
type CustomProps (line 11) | type CustomProps = VariantProps<typeof buttonVariants> & {
type IconButtonLinkProps (line 15) | type IconButtonLinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>,...
function IconButtonLink (line 22) | function IconButtonLink({className, variant, text, size, icon, children,...
FILE: packages/ui/src/components/ui/icon-button.tsx
type ButtonProps (line 9) | interface ButtonProps
function IconButton (line 15) | function IconButton({className, variant, text, size, icon, children, ref...
FILE: packages/ui/src/components/ui/icon.tsx
type SizeVariant (line 8) | type SizeVariant = VariantProps<typeof buttonVariants>['size']
type Size (line 9) | type Size = NonNullable<SizeVariant>
type IconTypes (line 11) | type IconTypes = IconType | LucideIcon
type IconProps (line 13) | type IconProps = {
function Icon (line 32) | function Icon({component, size = 'default', style, className, ...props}:...
FILE: packages/ui/src/components/ui/immersive-dialog.tsx
function ImmersiveDialogSeparator (line 23) | function ImmersiveDialogSeparator() {
function ImmersiveDialog (line 29) | function ImmersiveDialog({open, children, ...props}: ComponentPropsWitho...
function ImmersiveDialogContent (line 49) | function ImmersiveDialogContent({
function ImmersiveDialogSplitContent (line 92) | function ImmersiveDialogSplitContent({
function ImmersiveDialogOverlay (line 133) | function ImmersiveDialogOverlay({ref}: {ref?: React.Ref<HTMLDivElement>}) {
function ImmersiveDialogClose (line 145) | function ImmersiveDialogClose() {
function ImmersiveDialogBody (line 160) | function ImmersiveDialogBody({
function AnimateIn (line 192) | function AnimateIn({children}: {children: React.ReactNode}) {
function ImmersiveDialogFooter (line 210) | function ImmersiveDialogFooter({children, className}: {children: React.R...
function ImmersiveDialogIconMessage (line 214) | function ImmersiveDialogIconMessage({
function ImmersiveDialogIconMessageKeyValue (line 252) | function ImmersiveDialogIconMessageKeyValue({
FILE: packages/ui/src/components/ui/input.tsx
type InputProps (line 32) | interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>...
function Labeled (line 36) | function Labeled({children, label}: {children: React.ReactNode; label: s...
function Input (line 45) | function Input({
function PasswordInput (line 71) | function PasswordInput({
function AnimatedInputError (line 121) | function AnimatedInputError({children}: {children: React.ReactNode}) {
function InputError (line 157) | function InputError({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/components/ui/label.tsx
function Label (line 9) | function Label({
FILE: packages/ui/src/components/ui/list.tsx
function ListRadioItem (line 6) | function ListRadioItem({
FILE: packages/ui/src/components/ui/loading.tsx
function Loading (line 5) | function Loading({children}: {children?: React.ReactNode}) {
function Loading2 (line 14) | function Loading2({children}: {children?: React.ReactNode}) {
function Spinner (line 23) | function Spinner({size = '4'}: {size?: string}) {
FILE: packages/ui/src/components/ui/notification-badge.tsx
function NotificationBadge (line 1) | function NotificationBadge({count}: {count: number}) {
FILE: packages/ui/src/components/ui/pagination.tsx
function PaginationContent (line 16) | function PaginationContent({
function PaginationItem (line 24) | function PaginationItem({className, ref, ...props}: React.ComponentProps...
type PaginationLinkProps (line 28) | type PaginationLinkProps = {
FILE: packages/ui/src/components/ui/pin-input.tsx
type CodeState (line 17) | type CodeState = 'input' | 'loading' | 'error' | 'success'
type PinInputProps (line 18) | type PinInputProps = {
FILE: packages/ui/src/components/ui/popover.tsx
function PopoverContent (line 16) | function PopoverContent({
FILE: packages/ui/src/components/ui/progress.tsx
function Progress (line 31) | function Progress({
FILE: packages/ui/src/components/ui/radio-group.tsx
function RadioGroup (line 6) | function RadioGroup({
function RadioGroupItem (line 16) | function RadioGroupItem({
FILE: packages/ui/src/components/ui/root-error-fallback.tsx
function getErrorMessage (line 8) | function getErrorMessage(error: unknown): string {
constant NETWORK_ERROR_PATTERNS (line 22) | const NETWORK_ERROR_PATTERNS = [
function isNetworkError (line 32) | function isNetworkError(error: unknown): boolean {
function RootErrorFallback (line 50) | function RootErrorFallback({error}: {error: unknown}) {
FILE: packages/ui/src/components/ui/scroll-area.tsx
type Props (line 8) | type Props = React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.R...
function ScrollArea (line 16) | function ScrollArea({
function ScrollBar (line 52) | function ScrollBar({
FILE: packages/ui/src/components/ui/segmented-control.tsx
type Tab (line 6) | type Tab<T extends string> = {id: T; label: string}
function SegmentedControl (line 10) | function SegmentedControl<T extends string>({
FILE: packages/ui/src/components/ui/separator.tsx
function Separator (line 6) | function Separator({
FILE: packages/ui/src/components/ui/sheet-scroll-area.tsx
type Props (line 8) | type Props = React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.R...
function ScrollArea (line 13) | function ScrollArea({className, children, viewportRef, ref, ...props}: P...
function ScrollBar (line 39) | function ScrollBar({
FILE: packages/ui/src/components/ui/sheet.tsx
type SheetContentProps (line 36) | interface SheetContentProps
function SheetContent (line 43) | function SheetContent({
function SheetTitle (line 101) | function SheetTitle({
function SheetDescription (line 117) | function SheetDescription({
FILE: packages/ui/src/components/ui/switch.tsx
function Switch (line 6) | function Switch({
FILE: packages/ui/src/components/ui/table.tsx
function Table (line 5) | function Table({
function TableHeader (line 17) | function TableHeader({
function TableBody (line 25) | function TableBody({
function TableFooter (line 33) | function TableFooter({
function TableRow (line 43) | function TableRow({
function TableHead (line 57) | function TableHead({
function TableCell (line 74) | function TableCell({
function TableCaption (line 82) | function TableCaption({
FILE: packages/ui/src/components/ui/tabs.tsx
function TabsList (line 8) | function TabsList({
function TabsTrigger (line 27) | function TabsTrigger({
function TabsContent (line 46) | function TabsContent({
FILE: packages/ui/src/components/ui/toast.tsx
function Toaster (line 9) | function Toaster() {
function ToastIcon (line 52) | function ToastIcon({component, hexColor}: {component: IconType; hexColor...
FILE: packages/ui/src/components/ui/tooltip.tsx
function TooltipContent (line 12) | function TooltipContent({
FILE: packages/ui/src/components/umbrel-logo.tsx
function UmbrelLogo (line 3) | function UmbrelLogo({style, ref, ...props}: SVGProps<SVGSVGElement> & {r...
FILE: packages/ui/src/constants/index.ts
constant LOADING_DASH (line 5) | const LOADING_DASH = '–'
constant SETTINGS_SYSTEM_CARDS_ID (line 7) | const SETTINGS_SYSTEM_CARDS_ID = 'settings-system-cards'
type UmbrelHostEnvironment (line 10) | type UmbrelHostEnvironment = (typeof hostEnvironments)[number]
FILE: packages/ui/src/features/backups/components/backup-device-icon.tsx
function BackupDeviceIcon (line 9) | function BackupDeviceIcon({
FILE: packages/ui/src/features/backups/components/backup-location-dropdown.tsx
type RestoreLocationDropdownProps (line 7) | type RestoreLocationDropdownProps = {
function RestoreLocationDropdown (line 11) | function RestoreLocationDropdown({onSelect}: RestoreLocationDropdownProp...
FILE: packages/ui/src/features/backups/components/backups-exclusions.tsx
function BackupsExclusions (line 25) | function BackupsExclusions({showTitle = false}: {showTitle?: boolean}) {
function useFileItemForPath (line 213) | function useFileItemForPath(path: string) {
function FilePathRow (line 224) | function FilePathRow({path, rightSlot}: {path: string; rightSlot?: React...
function AppRow (line 250) | function AppRow({
FILE: packages/ui/src/features/backups/components/configure-wizard.tsx
function BackupsConfigureWizard (line 43) | function BackupsConfigureWizard() {
function ConnectivityDot (line 136) | function ConnectivityDot({connected}: {connected: boolean}) {
function CircularProgress (line 147) | function CircularProgress({percent, className}: {percent: number; classN...
function InlineBackupProgress (line 170) | function InlineBackupProgress({percent}: {percent: number}) {
function BackupNowButton (line 180) | function BackupNowButton({repoId, hidden}: {repoId: string; hidden: bool...
function LocationsSection (line 194) | function LocationsSection({
function RepositoryDetails (line 296) | function RepositoryDetails({
function BackupsList (line 495) | function BackupsList({
FILE: packages/ui/src/features/backups/components/floating-island/expanded.tsx
type Progress (line 7) | type Progress = {name: string; percent: number; path?: string}
function ExpandedContent (line 9) | function ExpandedContent({progresses}: {progresses: Progress[]}) {
FILE: packages/ui/src/features/backups/components/floating-island/index.tsx
function BackupsIsland (line 10) | function BackupsIsland() {
FILE: packages/ui/src/features/backups/components/floating-island/minimized.tsx
function MinimizedContent (line 6) | function MinimizedContent({progress}: {count: number; progress: number}) {
FILE: packages/ui/src/features/backups/components/modals/already-configured-modal.tsx
function AlreadyConfiguredModal (line 8) | function AlreadyConfiguredModal({
FILE: packages/ui/src/features/backups/components/modals/connect-existing-modal.tsx
function ConnectExistingModal (line 11) | function ConnectExistingModal({
FILE: packages/ui/src/features/backups/components/restore-location-dropdown.tsx
type RestoreLocationDropdownProps (line 23) | type RestoreLocationDropdownProps = {
function RestoreLocationDropdown (line 28) | function RestoreLocationDropdown({onSelect, isExternalStorageSupported =...
FILE: packages/ui/src/features/backups/components/restore-wizard.tsx
type RestoreWizardValues (line 67) | type RestoreWizardValues = {repositoryId: string; backupId?: string}
type Step (line 78) | enum Step {
function BackupsRestoreWizard (line 104) | function BackupsRestoreWizard() {
function RepositoryStep (line 357) | function RepositoryStep({
function BackupsStep (line 579) | function BackupsStep({
function ReviewStep (line 724) | function ReviewStep({repository, backup}: {repository?: BackupRepository...
FILE: packages/ui/src/features/backups/components/review-card.tsx
function ReviewCard (line 3) | function ReviewCard({
FILE: packages/ui/src/features/backups/components/setup-wizard.tsx
type FormValues (line 77) | type FormValues = z.infer<typeof formSchema>
type Step (line 90) | enum Step {
function BackupsSetupWizard (line 123) | function BackupsSetupWizard() {
function DestinationStep (line 395) | function DestinationStep({
function FolderPickerStep (line 670) | function FolderPickerStep({
function EncryptionStep (line 759) | function EncryptionStep() {
function ReviewStep (line 809) | function ReviewStep({values}: {values: FormValues}) {
FILE: packages/ui/src/features/backups/components/tab-switcher.tsx
function TabSwitcher (line 4) | function TabSwitcher({
FILE: packages/ui/src/features/backups/components/tiles.tsx
function SelectableTile (line 6) | function SelectableTile({
function ClickableTile (line 28) | function ClickableTile({children, onClick}: {children: React.ReactNode; ...
function LoadingTile (line 39) | function LoadingTile() {
function EmptyTile (line 47) | function EmptyTile({text}: {text: string}) {
function RadioTile (line 55) | function RadioTile({
FILE: packages/ui/src/features/backups/hooks/use-apps-auto-excluded-paths.ts
function useAppsAutoExcludedPaths (line 7) | function useAppsAutoExcludedPaths() {
FILE: packages/ui/src/features/backups/hooks/use-apps-backup-ignore.ts
function useAppsBackupIgnoredSummary (line 8) | function useAppsBackupIgnoredSummary() {
FILE: packages/ui/src/features/backups/hooks/use-backup-ignored-paths.ts
function useBackupIgnoredPaths (line 8) | function useBackupIgnoredPaths(options?: {excludeSystemPaths?: boolean}) {
FILE: packages/ui/src/features/backups/hooks/use-backups.ts
type BackupDestination (line 9) | type BackupDestination =
type SetupBackupInput (line 13) | type SetupBackupInput = {
type BackupRepository (line 19) | type BackupRepository = RouterOutput['backups']['getRepositories'][number]
type Backup (line 21) | type Backup = RouterOutput['backups']['listBackups'][number]
function useBackups (line 23) | function useBackups(options?: {repositoriesEnabled?: boolean}) {
function useBackupProgress (line 142) | function useBackupProgress(refetchIntervalMs = 1000) {
function useRestoreStatus (line 183) | function useRestoreStatus(refetchIntervalMs = 500) {
function useRepositorySize (line 189) | function useRepositorySize(repositoryId: string | undefined, options?: {...
function useRepositoryBackups (line 199) | function useRepositoryBackups(
function useRestoreBackup (line 213) | function useRestoreBackup() {
function useConnectToRepository (line 232) | function useConnectToRepository() {
function useMountBackup (line 256) | function useMountBackup() {
function useUnmountBackup (line 275) | function useUnmountBackup() {
function useTriggerBackupForRepo (line 296) | function useTriggerBackupForRepo(repositoryId: string) {
FILE: packages/ui/src/features/backups/hooks/use-existing-backup-detection.ts
type ExistingRepoStatus (line 7) | type ExistingRepoStatus = 'none' | 'exists-not-configured' | 'already-co...
function useExistingBackupDetection (line 9) | function useExistingBackupDetection(folder: string | undefined, reposito...
FILE: packages/ui/src/features/backups/index.tsx
function SplitDialog (line 13) | function SplitDialog({
function SplitLeftContent (line 45) | function SplitLeftContent({titleKey = 'backup'}: {titleKey?: string}) {
function BackupsRestoreDialog (line 55) | function BackupsRestoreDialog() {
FILE: packages/ui/src/features/backups/utils/backup-location-helpers.ts
type DeviceKind (line 4) | type DeviceKind = 'NAS' | 'DRIVE'
function getDeviceType (line 6) | function getDeviceType(path: string): DeviceKind {
function getDeviceNameFromPath (line 17) | function getDeviceNameFromPath(path: string): string {
function isRepoConnected (line 29) | function isRepoConnected(
FILE: packages/ui/src/features/backups/utils/error-messages.ts
function getUserFriendlyErrorMessage (line 4) | function getUserFriendlyErrorMessage(error: any): string {
FILE: packages/ui/src/features/backups/utils/filepath-helpers.ts
constant BACKUP_FILE_NAME (line 4) | const BACKUP_FILE_NAME = 'Umbrel Backup.backup'
function getDisplayRepositoryPath (line 12) | function getDisplayRepositoryPath(path: string): string {
function formatAppPathForDisplay (line 31) | function formatAppPathForDisplay(path: string) {
function getLastPathSegment (line 38) | function getLastPathSegment(p?: string) {
function getRelativePathFromRoot (line 47) | function getRelativePathFromRoot(path: string, root: string): string {
function getRepositoryDisplayName (line 61) | function getRepositoryDisplayName(path: string): string {
function getRepositoryRelativePath (line 74) | function getRepositoryRelativePath(path: string): string {
function getRepositoryPathFromBackupFile (line 95) | function getRepositoryPathFromBackupFile(backupFilePath: string): string {
FILE: packages/ui/src/features/backups/utils/sort.ts
function sortBackupsByTimeDesc (line 4) | function sortBackupsByTimeDesc(backups: Backup[] | undefined | null): Ba...
FILE: packages/ui/src/features/files/assets/file-items-thumbnails/index.tsx
type ThumbnailProps (line 24) | type ThumbnailProps = React.ImgHTMLAttributes<HTMLImageElement>
FILE: packages/ui/src/features/files/cmdk-search-provider.tsx
constant MAX_RESULTS (line 10) | const MAX_RESULTS = 10
FILE: packages/ui/src/features/files/components/cards/server-cards.tsx
function AddManuallyCard (line 3) | function AddManuallyCard({onClick, label}: {onClick?: () => void; label:...
function ServerCard (line 23) | function ServerCard({
FILE: packages/ui/src/features/files/components/dialogs/add-network-share-dialog/index.tsx
type Step (line 46) | enum Step {
type ManualStep (line 53) | enum ManualStep {
function AddNetworkShareDialog (line 61) | function AddNetworkShareDialog(props?: {
function DiscoverStep (line 428) | function DiscoverStep({
function CredentialsStep (line 495) | function CredentialsStep() {
function SelectShareStep (line 536) | function SelectShareStep({
FILE: packages/ui/src/features/files/components/dialogs/external-storage-unsupported-dialog/index.tsx
function ExternalStorageUnsupportedDialog (line 16) | function ExternalStorageUnsupportedDialog() {
FILE: packages/ui/src/features/files/components/dialogs/format-drive-dialog/index.tsx
function FilesystemCard (line 24) | function FilesystemCard({
function FormatDriveDialog (line 61) | function FormatDriveDialog() {
FILE: packages/ui/src/features/files/components/dialogs/permanently-delete-confirmation-dialog/index.tsx
function PermanentlyDeleteConfirmationDialog (line 20) | function PermanentlyDeleteConfirmationDialog() {
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/index.tsx
function ShareInfoDialog (line 30) | function ShareInfoDialog() {
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-instructions/index.tsx
type PlatformInstructionsProps (line 7) | interface PlatformInstructionsProps {
function PlatformInstructions (line 16) | function PlatformInstructions({
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-instructions/inline-copyable-field.tsx
function InlineCopyableField (line 10) | function InlineCopyableField({value, className}: {value: string; classNa...
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-instructions/instruction.tsx
function InstructionContainer (line 3) | function InstructionContainer({children}: {children: ReactNode}) {
function InstructionItem (line 6) | function InstructionItem({children}: {children: ReactNode}) {
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-instructions/ios-instructions.tsx
type IOSInstructionsProps (line 10) | interface IOSInstructionsProps {
function IOSInstructions (line 16) | function IOSInstructions({smbUrl, username, password}: IOSInstructionsPr...
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-instructions/macos-instructions.tsx
type MacOSInstructionsProps (line 13) | interface MacOSInstructionsProps {
function MacOSInstructions (line 20) | function MacOSInstructions({smbUrl, username, password, name}: MacOSInst...
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-instructions/umbrelos-instructions.tsx
type Props (line 15) | interface Props {
function UmbrelOSInstructions (line 21) | function UmbrelOSInstructions({username, password, sharename}: Props) {
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-instructions/windows-instructions.tsx
type WindowsInstructionsProps (line 10) | interface WindowsInstructionsProps {
function WindowsInstructions (line 16) | function WindowsInstructions({smbUrl, username, password}: WindowsInstru...
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/platform-selector.tsx
type Platform (line 11) | type Platform = {
type PlatformSelectorProps (line 24) | interface PlatformSelectorProps {
function PlatformSelector (line 29) | function PlatformSelector({selectedPlatform, onPlatformChange}: Platform...
FILE: packages/ui/src/features/files/components/dialogs/share-info-dialog/share-toggle.tsx
type ShareToggleProps (line 4) | interface ShareToggleProps {
function ShareToggle (line 11) | function ShareToggle({name, isShared, isLoading, onToggle}: ShareToggleP...
FILE: packages/ui/src/features/files/components/embedded/index.tsx
function EmbeddedFiles (line 18) | function EmbeddedFiles({
FILE: packages/ui/src/features/files/components/file-viewer/audio-viewer/index.tsx
type AudioViewerProps (line 6) | interface AudioViewerProps {
FILE: packages/ui/src/features/files/components/file-viewer/downloader/index.tsx
function DownloadDialog (line 9) | function DownloadDialog() {
FILE: packages/ui/src/features/files/components/file-viewer/image-viewer/index.tsx
type ImageViewerProps (line 4) | interface ImageViewerProps {
function ImageViewer (line 8) | function ImageViewer({item}: ImageViewerProps) {
FILE: packages/ui/src/features/files/components/file-viewer/pdf-viewer/index.tsx
type PdfViewerProps (line 7) | interface PdfViewerProps {
function PdfViewer (line 11) | function PdfViewer({item}: PdfViewerProps) {
FILE: packages/ui/src/features/files/components/file-viewer/video-viewer/index.tsx
type VideoViewerProps (line 8) | interface VideoViewerProps {
function VideoViewer (line 12) | function VideoViewer({item}: VideoViewerProps) {
FILE: packages/ui/src/features/files/components/file-viewer/viewer-wrapper.tsx
type ViewerWrapperProps (line 5) | interface ViewerWrapperProps {
FILE: packages/ui/src/features/files/components/files-dnd-wrapper/files-dnd-overlay.tsx
function FilesDndOverlay (line 8) | function FilesDndOverlay() {
FILE: packages/ui/src/features/files/components/files-dnd-wrapper/index.tsx
function FilesDndWrapper (line 32) | function FilesDndWrapper({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/features/files/components/floating-islands/audio-island/equalizer.tsx
constant RANGES (line 14) | const RANGES = [
type MusicEqualizerProps (line 40) | interface MusicEqualizerProps {
FILE: packages/ui/src/features/files/components/floating-islands/audio-island/expanded.tsx
type ExpandedContentProps (line 7) | interface ExpandedContentProps {
FILE: packages/ui/src/features/files/components/floating-islands/audio-island/index.tsx
type PlayerState (line 9) | interface PlayerState {
function AudioIsland (line 16) | function AudioIsland() {
FILE: packages/ui/src/features/files/components/floating-islands/audio-island/minimized.tsx
type MinimizedContentProps (line 7) | interface MinimizedContentProps {
FILE: packages/ui/src/features/files/components/floating-islands/formatting-island/expanded.tsx
type FormattingDevice (line 8) | type FormattingDevice = {
function ExpandedContent (line 14) | function ExpandedContent({devices}: {devices: FormattingDevice[]}) {
FILE: packages/ui/src/features/files/components/floating-islands/formatting-island/index.tsx
type FormattingDevice (line 7) | type FormattingDevice = {
function FormattingIsland (line 13) | function FormattingIsland() {
FILE: packages/ui/src/features/files/components/floating-islands/formatting-island/minimized.tsx
function MinimizedContent (line 5) | function MinimizedContent({count}: {count: number}) {
FILE: packages/ui/src/features/files/components/floating-islands/operations-island/expanded.tsx
function ExpandedContent (line 10) | function ExpandedContent({progress, count, speed}: {progress: number; co...
FILE: packages/ui/src/features/files/components/floating-islands/operations-island/index.tsx
function OperationsIsland (line 7) | function OperationsIsland() {
FILE: packages/ui/src/features/files/components/floating-islands/operations-island/minimized.tsx
function MinimizedContent (line 7) | function MinimizedContent({
FILE: packages/ui/src/features/files/components/floating-islands/uploading-island/expanded.tsx
function ExpandedContent (line 9) | function ExpandedContent() {
FILE: packages/ui/src/features/files/components/floating-islands/uploading-island/index.tsx
function UploadingIsland (line 5) | function UploadingIsland() {
FILE: packages/ui/src/features/files/components/floating-islands/uploading-island/minimized.tsx
function MinimizedContent (line 6) | function MinimizedContent() {
FILE: packages/ui/src/features/files/components/listing/actions-bar/actions-bar-context.tsx
type ActionsBarConfig (line 8) | interface ActionsBarConfig {
type ActionsBarContextValue (line 26) | interface ActionsBarContextValue {
function ActionsBarProvider (line 35) | function ActionsBarProvider({children}: {children: React.ReactNode}) {
function useActionsBarConfig (line 44) | function useActionsBarConfig() {
function useSetActionsBarConfig (line 53) | function useSetActionsBarConfig() {
FILE: packages/ui/src/features/files/components/listing/actions-bar/index.tsx
function ActionsBar (line 15) | function ActionsBar() {
FILE: packages/ui/src/features/files/components/listing/actions-bar/mobile-actions.tsx
function MobileActions (line 26) | function MobileActions({DropdownItems = null}: {DropdownItems?: React.Re...
FILE: packages/ui/src/features/files/components/listing/actions-bar/navigation-controls.tsx
function NavigationControls (line 17) | function NavigationControls() {
FILE: packages/ui/src/features/files/components/listing/actions-bar/path-bar/index.tsx
function PathBar (line 12) | function PathBar() {
FILE: packages/ui/src/features/files/components/listing/actions-bar/path-bar/path-bar-desktop.tsx
type PathSegment (line 20) | type PathSegment = {
function PathBarDesktop (line 27) | function PathBarDesktop({path}: {path: string}) {
type PathSegmentProps (line 199) | type PathSegmentProps = Omit<PathSegment, 'id'> & {
FILE: packages/ui/src/features/files/components/listing/actions-bar/path-bar/path-bar-mobile.tsx
type PathBarMobileProps (line 8) | interface PathBarMobileProps {
function PathBarMobile (line 12) | function PathBarMobile({path}: PathBarMobileProps) {
FILE: packages/ui/src/features/files/components/listing/actions-bar/path-bar/path-input.tsx
type PathInputProps (line 8) | interface PathInputProps {
function PathInput (line 13) | function PathInput({path, onClose}: PathInputProps) {
FILE: packages/ui/src/features/files/components/listing/actions-bar/search-input.tsx
function SearchInput (line 21) | function SearchInput() {
FILE: packages/ui/src/features/files/components/listing/actions-bar/sort-dropdown.tsx
function SortDropdown (line 10) | function SortDropdown() {
FILE: packages/ui/src/features/files/components/listing/actions-bar/view-toggle.tsx
function ViewToggle (line 9) | function ViewToggle() {
FILE: packages/ui/src/features/files/components/listing/apps-listing/index.tsx
function AppsListing (line 8) | function AppsListing() {
FILE: packages/ui/src/features/files/components/listing/directory-listing/empty-state.tsx
function EmptyStateDirectory (line 15) | function EmptyStateDirectory() {
function EmptyStateNetwork (line 56) | function EmptyStateNetwork() {
FILE: packages/ui/src/features/files/components/listing/directory-listing/index.tsx
function DirectoryListing (line 27) | function DirectoryListing({marqueeScale = 1}: {marqueeScale?: number} = ...
FILE: packages/ui/src/features/files/components/listing/file-item/circular-progress.tsx
type CircularProgressProps (line 5) | interface CircularProgressProps {
FILE: packages/ui/src/features/files/components/listing/file-item/editable-name.tsx
type EditableNameProps (line 18) | interface EditableNameProps {
FILE: packages/ui/src/features/files/components/listing/file-item/icons-view-file-item.tsx
type IconsViewFileItemProps (line 16) | interface IconsViewFileItemProps {
FILE: packages/ui/src/features/files/components/listing/file-item/index.tsx
type FileItemProps (line 15) | interface FileItemProps {
function whenTouchOrPen (line 21) | function whenTouchOrPen<E>(handler: React.PointerEventHandler<E>): React...
function isInInput (line 164) | function isInInput(event: KeyboardEvent) {
function handleKeyDown (line 170) | function handleKeyDown(event: KeyboardEvent) {
FILE: packages/ui/src/features/files/components/listing/file-item/list-view-file-item.tsx
type ListViewFileItemProps (line 19) | interface ListViewFileItemProps {
function ListViewFileItem (line 26) | function ListViewFileItem({item, isEditingName, onEditingNameComplete, f...
FILE: packages/ui/src/features/files/components/listing/file-item/truncated-filename.tsx
type TruncatedFilenameProps (line 4) | interface TruncatedFilenameProps {
function TruncatedFilename (line 10) | function TruncatedFilename({filename, className, view = 'list'}: Truncat...
FILE: packages/ui/src/features/files/components/listing/index.tsx
type ListingProps (line 22) | interface ListingProps {
function ListingContent (line 37) | function ListingContent({
function Listing (line 111) | function Listing({
function ErrorView (line 187) | function ErrorView({error}: {error: unknown}) {
function LoadingView (line 212) | function LoadingView() {
function EmptyView (line 220) | function EmptyView() {
FILE: packages/ui/src/features/files/components/listing/listing-and-file-item-context-menu.tsx
type ListingAndFileItemContextMenuProps (line 38) | interface ListingAndFileItemContextMenuProps {
function ListingAndFileItemContextMenu (line 43) | function ListingAndFileItemContextMenu({children, menuItems}: ListingAnd...
FILE: packages/ui/src/features/files/components/listing/listing-body.tsx
type ListingBodyProps (line 11) | interface ListingBodyProps {
FILE: packages/ui/src/features/files/components/listing/marquee-selection.tsx
class DOMVector (line 7) | class DOMVector {
method constructor (line 8) | constructor(
method getDiagonalLength (line 15) | getDiagonalLength(): number {
method toDOMRect (line 19) | toDOMRect(): DOMRect {
method toTerminalPoint (line 28) | toTerminalPoint(): DOMPoint {
method add (line 32) | add(vector: DOMVector): DOMVector {
method clamp (line 41) | clamp(rect: DOMRect): DOMVector {
function rectsIntersect (line 51) | function rectsIntersect(rect1: DOMRect, rect2: DOMRect): boolean {
type MarqueeSelectionProps (line 57) | interface MarqueeSelectionProps {
FILE: packages/ui/src/features/files/components/listing/recents-listing/index.tsx
function RecentsListing (line 7) | function RecentsListing() {
FILE: packages/ui/src/features/files/components/listing/search-listing/index.tsx
function SearchListing (line 17) | function SearchListing() {
function EmptySearchView (line 61) | function EmptySearchView({query}: {query: string}) {
FILE: packages/ui/src/features/files/components/listing/trash-listing/index.tsx
function TrashListing (line 15) | function TrashListing() {
FILE: packages/ui/src/features/files/components/listing/virtualized-list.tsx
constant LIST_OVERSCAN_AMOUNT (line 76) | const LIST_OVERSCAN_AMOUNT = 20
constant GRID_OVERSCAN_AMOUNT (line 77) | const GRID_OVERSCAN_AMOUNT = 2
constant INFINITE_LOADER_THRESHOLD (line 80) | const INFINITE_LOADER_THRESHOLD = 100
type VirtualizedListProps (line 82) | interface VirtualizedListProps {
type IndexRange (line 96) | interface IndexRange {
type InfiniteLoaderRenderProps (line 108) | interface InfiniteLoaderRenderProps {
type GridVisibleIndices (line 117) | interface GridVisibleIndices {
type GridItemData (line 128) | interface GridItemData {
FILE: packages/ui/src/features/files/components/mini-browser/index.tsx
type MiniBrowserProps (line 21) | type MiniBrowserProps = {
constant INDENT_PER_LEVEL (line 49) | const INDENT_PER_LEVEL = 16
constant MAX_INDENT_LEVELS (line 50) | const MAX_INDENT_LEVELS = 9
constant MOBILE_MAX_INDENT_LEVELS (line 51) | const MOBILE_MAX_INDENT_LEVELS = 6
constant PATH_ANCESTORS_TO_SHOW (line 53) | const PATH_ANCESTORS_TO_SHOW = 1
function formatCompactPath (line 57) | function formatCompactPath(path: string, ancestorsToShow: number) {
function MiniBrowser (line 66) | function MiniBrowser({
function Tree (line 227) | function Tree({
function Node (line 319) | function Node({
function Subtree (line 441) | function Subtree({
function NewFolderNode (line 530) | function NewFolderNode({
FILE: packages/ui/src/features/files/components/rewind/index.tsx
function SidebarRewind (line 39) | function SidebarRewind() {
function RewindOverlay (line 59) | function RewindOverlay() {
FILE: packages/ui/src/features/files/components/rewind/overlay-context.tsx
type RewindOverlayContextValue (line 3) | type RewindOverlayContextValue = {
function RewindOverlayProvider (line 12) | function RewindOverlayProvider({children}: {children: React.ReactNode}) {
function useRewindOverlay (line 18) | function useRewindOverlay() {
FILE: packages/ui/src/features/files/components/rewind/prerewind-dialog.tsx
function PreRewindDialog (line 18) | function PreRewindDialog({
FILE: packages/ui/src/features/files/components/rewind/restore-grouping.ts
function groupRestoreByDestination (line 25) | function groupRestoreByDestination(selectedItems: FileSystemItem[], moun...
FILE: packages/ui/src/features/files/components/rewind/restore-progress-dialog.tsx
function RestoreProgressDialog (line 15) | function RestoreProgressDialog({open, phase}: {open: boolean; phase: 'id...
function RestoringItems (line 86) | function RestoringItems({
FILE: packages/ui/src/features/files/components/rewind/snapshot-carousel.tsx
function SnapshotCarousel (line 15) | function SnapshotCarousel({
FILE: packages/ui/src/features/files/components/rewind/snapshot-date-label.ts
function getSnapshotDateLabel (line 5) | function getSnapshotDateLabel(
FILE: packages/ui/src/features/files/components/rewind/timeline-bar.tsx
function TimelineBar (line 7) | function TimelineBar({
FILE: packages/ui/src/features/files/components/rewind/tooltip.tsx
function TooltipProvider (line 8) | function TooltipProvider({delayDuration = 0, ...props}: React.ComponentP...
function Tooltip (line 12) | function Tooltip({...props}: React.ComponentProps<typeof TooltipPrimitiv...
function TooltipTrigger (line 20) | function TooltipTrigger({...props}: React.ComponentProps<typeof TooltipP...
function TooltipContent (line 24) | function TooltipContent({
FILE: packages/ui/src/features/files/components/shared/circular-progress.tsx
type CircularProgressProps (line 3) | interface CircularProgressProps {
FILE: packages/ui/src/features/files/components/shared/drag-and-drop.tsx
type DraggableProps (line 12) | interface DraggableProps {
type DroppableProps (line 20) | interface DroppableProps {
FILE: packages/ui/src/features/files/components/shared/file-item-icon/animated-folder-icon.tsx
type AnimatedFolderIconProps (line 3) | interface AnimatedFolderIconProps {
FILE: packages/ui/src/features/files/components/shared/file-item-icon/folder-icon.tsx
type FolderIconProps (line 3) | interface FolderIconProps {
FILE: packages/ui/src/features/files/components/shared/file-item-icon/index.tsx
type FileItemIcon (line 45) | interface FileItemIcon {
function useOnDemandThumbnail (line 241) | function useOnDemandThumbnail(item: FileSystemItem) {
function extractAppIdFromPath (line 371) | function extractAppIdFromPath(path: string): string {
FILE: packages/ui/src/features/files/components/shared/file-upload-drop-zone.tsx
type FileUploadDropZoneProps (line 9) | interface FileUploadDropZoneProps {
function FileUploadDropZone (line 13) | function FileUploadDropZone({children}: FileUploadDropZoneProps) {
type RippleProps (line 47) | interface RippleProps {
FILE: packages/ui/src/features/files/components/shared/upload-input.tsx
function UploadInput (line 4) | function UploadInput({ref}: {ref?: React.Ref<HTMLInputElement>}) {
FILE: packages/ui/src/features/files/components/sidebar/index.tsx
function Sidebar (line 25) | function Sidebar({className}: {className?: string}) {
FILE: packages/ui/src/features/files/components/sidebar/mobile-sidebar-wrapper.tsx
type MobileSidebarProps (line 4) | interface MobileSidebarProps {
function MobileSidebarWrapper (line 10) | function MobileSidebarWrapper({children, isOpen, onClose}: MobileSidebar...
FILE: packages/ui/src/features/files/components/sidebar/sidebar-apps.tsx
function SidebarApps (line 6) | function SidebarApps() {
FILE: packages/ui/src/features/files/components/sidebar/sidebar-external-storage-item.tsx
type SidebarExternalStorageItemProps (line 22) | interface SidebarExternalStorageItemProps {
function SidebarExternalStorageItem (line 26) | function SidebarExternalStorageItem({item}: SidebarExternalStorageItemPr...
FILE: packages/ui/src/features/files/components/sidebar/sidebar-external-storage.tsx
function SidebarExternalStorage (line 11) | function SidebarExternalStorage() {
FILE: packages/ui/src/features/files/components/sidebar/sidebar-favorites.tsx
function SidebarFavorites (line 10) | function SidebarFavorites({favorites}: {favorites: (string | null)[]}) {
FILE: packages/ui/src/features/files/components/sidebar/sidebar-home.tsx
function SidebarHome (line 12) | function SidebarHome() {
FILE: packages/ui/src/features/files/components/sidebar/sidebar-item.tsx
type SidebarItem (line 14) | type SidebarItem = {
type SidebarItemProps (line 20) | interface SidebarItemProps {
function SidebarItem (line 27) | function SidebarItem({item, isActive, onClick, disabled = false}: Sideba...
FILE: packages/ui/src/features/files/components/sidebar/sidebar-network-share-item.tsx
type SidebarNetworkShareItemProps (line 16) | interface SidebarNetworkShareItemProps {
function SidebarNetworkShareItem (line 23) | function SidebarNetworkShareItem({host, rootPath, onEject, disabled}: Si...
FILE: packages/ui/src/features/files/components/sidebar/sidebar-network-storage.tsx
function SidebarNetworkStorage (line 18) | function SidebarNetworkStorage() {
function NetworkRootItem (line 86) | function NetworkRootItem() {
FILE: packages/ui/src/features/files/components/sidebar/sidebar-recents.tsx
function SidebarRecents (line 8) | function SidebarRecents() {
FILE: packages/ui/src/features/files/components/sidebar/sidebar-shares.tsx
function SidebarShares (line 12) | function SidebarShares({shares}: {shares: (Share | null)[]}) {
FILE: packages/ui/src/features/files/components/sidebar/sidebar-trash.tsx
function SidebarTrash (line 16) | function SidebarTrash() {
FILE: packages/ui/src/features/files/constants.ts
constant BASE_ROUTE_PATH (line 27) | const BASE_ROUTE_PATH = '/files' as const
constant HOME_PATH (line 28) | const HOME_PATH = '/Home' as const
constant TRASH_PATH (line 29) | const TRASH_PATH = '/Trash' as const
constant APPS_PATH (line 30) | const APPS_PATH = '/Apps' as const
constant EXTERNAL_STORAGE_PATH (line 31) | const EXTERNAL_STORAGE_PATH = '/External' as const
constant NETWORK_STORAGE_PATH (line 32) | const NETWORK_STORAGE_PATH = '/Network' as const
constant BACKUPS_PATH (line 33) | const BACKUPS_PATH = '/Backups' as const
constant SEARCH_PATH (line 39) | const SEARCH_PATH = '/Search' as const
constant RECENTS_PATH (line 40) | const RECENTS_PATH = '/Recents' as const
constant USE_LIST_DIRECTORY_LOAD_ITEMS (line 43) | const USE_LIST_DIRECTORY_LOAD_ITEMS = {
constant SUPPORTED_ARCHIVE_EXTRACT_EXTENSIONS (line 49) | const SUPPORTED_ARCHIVE_EXTRACT_EXTENSIONS = [
constant SORT_BY_OPTIONS (line 60) | const SORT_BY_OPTIONS = [
constant IMAGE_EXTENSIONS_WITH_IMAGE_THUMBNAILS (line 69) | const IMAGE_EXTENSIONS_WITH_IMAGE_THUMBNAILS = new Set(['.jpg', '.jpeg',...
constant VIDEO_EXTENSIONS_WITH_IMAGE_THUMBNAILS (line 70) | const VIDEO_EXTENSIONS_WITH_IMAGE_THUMBNAILS = new Set(['.mov', '.mp4', ...
constant FILE_TYPE_MAP (line 72) | const FILE_TYPE_MAP = {
type FileType (line 190) | type FileType = keyof typeof FILE_TYPE_MAP
FILE: packages/ui/src/features/files/hooks/use-drag-and-drop.ts
function useDragAndDrop (line 9) | function useDragAndDrop() {
FILE: packages/ui/src/features/files/hooks/use-external-storage.ts
function useExternalStorage (line 18) | function useExternalStorage() {
FILE: packages/ui/src/features/files/hooks/use-favorites.ts
function useFavorites (line 13) | function useFavorites() {
FILE: packages/ui/src/features/files/hooks/use-files-keyboard-shortcuts.ts
function useFilesKeyboardShortcuts (line 19) | function useFilesKeyboardShortcuts({
FILE: packages/ui/src/features/files/hooks/use-files-operations.ts
type OperationAsyncFn (line 15) | type OperationAsyncFn<TArgs extends object, TResult = any> = (args: TArg...
type GetOperationArgsFn (line 17) | type GetOperationArgsFn<TArgs extends object> = (path: string) => TArgs
type ErrorToastFn (line 19) | type ErrorToastFn = (message: string) => void
function useFilesOperations (line 21) | function useFilesOperations() {
FILE: packages/ui/src/features/files/hooks/use-home-directory-name.ts
function useHomeDirectoryName (line 4) | function useHomeDirectoryName() {
FILE: packages/ui/src/features/files/hooks/use-is-touch-device.ts
function useIsTouchDevice (line 3) | function useIsTouchDevice() {
FILE: packages/ui/src/features/files/hooks/use-list-directory.ts
type UseListDirectoryOptions (line 11) | interface UseListDirectoryOptions {
function useListDirectory (line 16) | function useListDirectory(
FILE: packages/ui/src/features/files/hooks/use-list-recents.ts
function useListRecents (line 13) | function useListRecents() {
FILE: packages/ui/src/features/files/hooks/use-navigate.ts
function toFsPath (line 19) | function toFsPath(urlPath: string): string {
function encodePathSegments (line 24) | function encodePathSegments(fsPath: string): string {
FILE: packages/ui/src/features/files/hooks/use-network-device-type.ts
type NetworkDeviceType (line 7) | type NetworkDeviceType = 'umbrel' | 'nas'
function useNetworkDeviceType (line 28) | function useNetworkDeviceType(path: string) {
FILE: packages/ui/src/features/files/hooks/use-network-storage.ts
function useNetworkStorage (line 16) | function useNetworkStorage(options?: {suppressNavigateOnAdd?: boolean}) {
FILE: packages/ui/src/features/files/hooks/use-new-folder.ts
function useNewFolder (line 12) | function useNewFolder() {
function isNameAvailable (line 122) | function isNameAvailable(name: string, existingItems: FileSystemItem[]) {
FILE: packages/ui/src/features/files/hooks/use-preferences.ts
function usePreferences (line 9) | function usePreferences() {
FILE: packages/ui/src/features/files/hooks/use-rewind-action.ts
function useRewindAction (line 9) | function useRewindAction(selectedItems: FileSystemItem[]) {
FILE: packages/ui/src/features/files/hooks/use-rewind.ts
type ViewState (line 9) | type ViewState = 'preflight' | 'browsing' | 'switching-snapshot' | 'rest...
function useRewind (line 11) | function useRewind({overlayOpen, repoOpen}: {overlayOpen: boolean; repoO...
FILE: packages/ui/src/features/files/hooks/use-search-files.ts
type UseSearchFilesReturn (line 13) | interface UseSearchFilesReturn {
function useSearchFiles (line 20) | function useSearchFiles({
FILE: packages/ui/src/features/files/hooks/use-shares.ts
function useShares (line 15) | function useShares() {
FILE: packages/ui/src/features/files/index.tsx
function FilesLayout (line 31) | function FilesLayout() {
FILE: packages/ui/src/features/files/providers/files-capabilities-context.tsx
type FilesMode (line 9) | type FilesMode = 'full' | 'read-only'
type FilesCapabilities (line 11) | type FilesCapabilities = {
function FilesCapabilitiesProvider (line 39) | function FilesCapabilitiesProvider({
function useFilesCapabilities (line 57) | function useFilesCapabilities() {
function useIsFilesReadOnly (line 62) | function useIsFilesReadOnly() {
function useIsFilesEmbedded (line 67) | function useIsFilesEmbedded() {
FILE: packages/ui/src/features/files/store/slices/clipboard-slice.ts
type ClipboardMode (line 9) | type ClipboardMode = 'copy' | 'cut' | null
type ClipboardSlice (line 11) | interface ClipboardSlice {
FILE: packages/ui/src/features/files/store/slices/drag-and-drop-slice.ts
type DragAndDropSlice (line 9) | interface DragAndDropSlice {
FILE: packages/ui/src/features/files/store/slices/file-viewer-slice.ts
type FileViewerSlice (line 9) | interface FileViewerSlice {
FILE: packages/ui/src/features/files/store/slices/interaction-slice.ts
type InteractionSlice (line 10) | interface InteractionSlice {
FILE: packages/ui/src/features/files/store/slices/new-folder-slice.ts
type NewFolderSlice (line 9) | interface NewFolderSlice {
FILE: packages/ui/src/features/files/store/slices/rename-slice.ts
type RenameSlice (line 9) | interface RenameSlice {
FILE: packages/ui/src/features/files/store/slices/selection-slice.ts
type SelectionSlice (line 9) | interface SelectionSlice {
FILE: packages/ui/src/features/files/store/use-files-store.ts
type FilesStore (line 11) | type FilesStore = SelectionSlice &
FILE: packages/ui/src/features/files/types.ts
type UmbreldFileSystemItem (line 6) | type UmbreldFileSystemItem = RouterOutput['files']['list']['files'][number]
type Favorite (line 8) | type Favorite = RouterOutput['files']['favorites'][number]
type Share (line 10) | type Share = RouterOutput['files']['shares'][number]
type ExternalStorageDevice (line 12) | type ExternalStorageDevice = RouterOutput['files']['externalDevices'][nu...
type ViewPreferences (line 14) | type ViewPreferences = RouterOutput['files']['viewPreferences']
type FileSystemItem (line 18) | interface FileSystemItem extends UmbreldFileSystemItem {
type UploadStats (line 25) | interface UploadStats {
type PolymorphicPropsWithoutRef (line 33) | type PolymorphicPropsWithoutRef<T extends React.ElementType, P> = P &
type PolymorphicRef (line 39) | type PolymorphicRef<C extends React.ElementType> = React.ComponentPropsW...
type PolymorphicPropsWithRef (line 41) | type PolymorphicPropsWithRef<T extends React.ElementType, P> = Polymorph...
FILE: packages/ui/src/features/files/utils/error-messages.ts
function getFilesErrorMessage (line 5) | function getFilesErrorMessage(message: string): string {
FILE: packages/ui/src/features/files/utils/format-filesystem-date.ts
function capitalize (line 7) | function capitalize(str: string): string {
function formatFilesystemDate (line 16) | function formatFilesystemDate(date: number | undefined, languageCode: Su...
function formatFilesystemDateOnly (line 34) | function formatFilesystemDateOnly(date: number | undefined, languageCode...
FILE: packages/ui/src/features/files/utils/format-filesystem-name.ts
function formatItemName (line 6) | function formatItemName({name, maxLength = 30}: {name: FileSystemItem['n...
function splitFileName (line 23) | function splitFileName(fileName: string): {name: string; extension: stri...
function truncateName (line 47) | function truncateName(name: string, maxLength: number): string {
FILE: packages/ui/src/features/files/utils/format-filesystem-size.ts
function formatFilesystemSize (line 3) | function formatFilesystemSize(size: number | undefined | null): string {
FILE: packages/ui/src/features/files/utils/get-grid-column-count.ts
function getGridColumnCount (line 6) | function getGridColumnCount(width: number): number {
FILE: packages/ui/src/features/files/utils/get-item-key.ts
function getItemKey (line 10) | function getItemKey(item: FileSystemItem): string {
FILE: packages/ui/src/features/files/utils/path-alias.ts
type PathAliases (line 5) | type PathAliases = Record<string, string> | undefined
function replaceLeadingPrefix (line 9) | function replaceLeadingPrefix(path: string, prefix: string, replacement:...
function uiToVirtualPath (line 16) | function uiToVirtualPath(path: string, aliases: PathAliases): string {
function virtualToUiPath (line 27) | function virtualToUiPath(path: string, aliases: PathAliases): string {
FILE: packages/ui/src/features/files/utils/sort-filesystem-items.ts
function sortFilesystemItems (line 40) | function sortFilesystemItems(
FILE: packages/ui/src/features/files/widgets.tsx
type FilesListWidget (line 15) | type FilesListWidget = BaseWidget & {
type FilesGridWidget (line 22) | type FilesGridWidget = BaseWidget & {
type FilesListWidgetProps (line 69) | interface FilesListWidgetProps {
type FilesGridWidgetProps (line 76) | interface FilesGridWidgetProps {
function FilesListWidget (line 84) | function FilesListWidget({
function SkeletonListItem (line 130) | function SkeletonListItem() {
function ListItem (line 143) | function ListItem({item}: {item: FileSystemItem}) {
function FilesGridWidget (line 168) | function FilesGridWidget({
function SkeletonGridItem (line 239) | function SkeletonGridItem() {
function GridItem (line 250) | function GridItem({item, count}: {item: FileSystemItem; index: number; c...
FILE: packages/ui/src/features/storage/components/dialogs/add-to-raid-dialog.tsx
type InfoTextProps (line 41) | type InfoTextProps = {
function InfoText (line 53) | function InfoText({
type AddToRaidDialogProps (line 179) | type AddToRaidDialogProps = {
function AddToRaidDialog (line 190) | function AddToRaidDialog({
FILE: packages/ui/src/features/storage/components/dialogs/install-ssd-dialog.tsx
type InstallSsdDialogProps (line 19) | type InstallSsdDialogProps = {
function InstallSsdDialog (line 25) | function InstallSsdDialog({open, onOpenChange, isUmbrelPro}: InstallSsdD...
FILE: packages/ui/src/features/storage/components/dialogs/install-tips-collapsible.tsx
type InstallTipsCollapsibleProps (line 8) | type InstallTipsCollapsibleProps = {
function InstallTipsCollapsible (line 13) | function InstallTipsCollapsible({isOpen, onToggle}: InstallTipsCollapsib...
FILE: packages/ui/src/features/storage/components/dialogs/operation-in-progress-banner.tsx
type OperationInProgressBannerProps (line 8) | type OperationInProgressBannerProps = {
function OperationInProgressBanner (line 12) | function OperationInProgressBanner({variant}: OperationInProgressBannerP...
FILE: packages/ui/src/features/storage/components/dialogs/replace-failed-drive-dialog.tsx
type FailedRaidDevice (line 24) | type FailedRaidDevice = {
type ReplaceFailedDriveDialogProps (line 32) | type ReplaceFailedDriveDialogProps = {
function ReplaceFailedDriveDialog (line 44) | function ReplaceFailedDriveDialog({
FILE: packages/ui/src/features/storage/components/dialogs/shutdown-confirmation-dialog.tsx
type ShutdownConfirmationDialogProps (line 15) | type ShutdownConfirmationDialogProps = {
function ShutdownConfirmationDialog (line 20) | function ShutdownConfirmationDialog({open, onOpenChange}: ShutdownConfir...
FILE: packages/ui/src/features/storage/components/dialogs/ssd-health-dialog.tsx
type Warning (line 14) | type Warning = {
type SsdHealthDialogProps (line 19) | type SsdHealthDialogProps = {
function SsdHealthDialog (line 28) | function SsdHealthDialog({device, slotNumber, open, onOpenChange, raidDe...
function useSsdHealthDialog (line 290) | function useSsdHealthDialog() {
FILE: packages/ui/src/features/storage/components/dialogs/swap-dialog.tsx
type SwapDialogProps (line 27) | type SwapDialogProps = {
function SwapDialog (line 39) | function SwapDialog({
FILE: packages/ui/src/features/storage/components/floating-island/data-stream-icon.tsx
type DataStreamIconProps (line 11) | interface DataStreamIconProps {
function DataStreamIcon (line 16) | function DataStreamIcon({size = 32, isActive = true}: DataStreamIconProp...
type DataStreamIconMiniProps (line 156) | interface DataStreamIconMiniProps {
function DataStreamIconMini (line 161) | function DataStreamIconMini({size = 20, isActive = true}: DataStreamIcon...
FILE: packages/ui/src/features/storage/components/floating-island/expanded.tsx
function ExpandedContent (line 9) | function ExpandedContent({operation}: {operation: RaidProgress}) {
FILE: packages/ui/src/features/storage/components/floating-island/index.tsx
function RaidIsland (line 25) | function RaidIsland() {
FILE: packages/ui/src/features/storage/components/floating-island/minimized.tsx
function MinimizedContent (line 8) | function MinimizedContent({operation}: {operation: RaidProgress}) {
FILE: packages/ui/src/features/storage/components/ssd-shape.tsx
type SsdShapeProps (line 12) | type SsdShapeProps = {
function SsdShape (line 24) | function SsdShape({
FILE: packages/ui/src/features/storage/components/storage-donut-chart.tsx
type StorageDonutChartProps (line 7) | type StorageDonutChartProps = {
function StorageDonutChart (line 17) | function StorageDonutChart({
FILE: packages/ui/src/features/storage/components/storage-mode-display.tsx
type ModeOption (line 22) | type ModeOption = {
type StorageModeDisplayProps (line 50) | type StorageModeDisplayProps = {
function StorageModeDisplay (line 55) | function StorageModeDisplay({value, canEnableFailsafe}: StorageModeDispl...
FILE: packages/ui/src/features/storage/hooks/use-active-raid-operation.ts
function useActiveRaidOperation (line 9) | function useActiveRaidOperation(): RaidProgress | null {
FILE: packages/ui/src/features/storage/hooks/use-raid-progress.ts
type ExpansionStatus (line 8) | type ExpansionStatus = {
type RebuildStatus (line 13) | type RebuildStatus = {
type ReplaceStatus (line 18) | type ReplaceStatus = RebuildStatus
type FailsafeTransitionStatus (line 20) | type FailsafeTransitionStatus = {
type RaidOperationType (line 26) | type RaidOperationType = 'expansion' | 'rebuild' | 'replace' | 'failsafe...
type RaidProgress (line 28) | type RaidProgress = {
function useRaidProgress (line 36) | function useRaidProgress(): RaidProgress | null {
FILE: packages/ui/src/features/storage/hooks/use-storage.ts
type RaidStatus (line 4) | type RaidStatus = RouterOutput['hardware']['raid']['getStatus']
type StorageDevice (line 5) | type StorageDevice = RouterOutput['hardware']['internalStorage']['getDev...
type RaidType (line 6) | type RaidType = 'storage' | 'failsafe'
type RaidDeviceStatus (line 9) | type RaidDeviceStatus = 'ONLINE' | 'DEGRADED' | 'FAULTED' | 'OFFLINE' | ...
constant LIFETIME_WARNING_THRESHOLD (line 28) | const LIFETIME_WARNING_THRESHOLD = 80
function getDeviceHealth (line 31) | function getDeviceHealth(device: StorageDevice) {
type RaidDevice (line 65) | type RaidDevice = StorageDevice & {
type UseStorageOptions (line 74) | type UseStorageOptions = {
function useStorage (line 83) | function useStorage(options: UseStorageOptions = {}) {
FILE: packages/ui/src/features/storage/index.tsx
function StorageStats (line 40) | function StorageStats({
constant SLOT_INDICES (line 111) | const SLOT_INDICES = [0, 1, 2, 3] as const
function StorageManagerDialog (line 113) | function StorageManagerDialog() {
FILE: packages/ui/src/features/storage/providers/pending-operation-context.tsx
type PendingRaidOperationContextType (line 5) | type PendingRaidOperationContextType = {
function PendingRaidOperationProvider (line 13) | function PendingRaidOperationProvider({children}: {children: ReactNode}) {
function usePendingRaidOperation (line 25) | function usePendingRaidOperation() {
FILE: packages/ui/src/hooks/use-2fa.ts
function use2fa (line 5) | function use2fa(onEnableChange?: (enabled: boolean) => void) {
FILE: packages/ui/src/hooks/use-app-install.ts
function useUninstallAllApps (line 21) | function useUninstallAllApps() {
function useAppInstall (line 43) | function useAppInstall(id: string) {
FILE: packages/ui/src/hooks/use-apps-with-updates.ts
function useAppsWithUpdates (line 4) | function useAppsWithUpdates() {
FILE: packages/ui/src/hooks/use-auto-height-animation.tsx
function useAutoHeightAnimation (line 4) | function useAutoHeightAnimation(deps: any[]): [LegacyAnimationControls, ...
FILE: packages/ui/src/hooks/use-color-thief.ts
function useColorThief (line 8) | function useColorThief(ref: React.RefObject<HTMLImageElement | null>) {
function processColors (line 54) | function processColors(colors: RGBColor[] | null) {
function isNeutralBright (line 66) | function isNeutralBright(rgb: number[]) {
function isNeutralDark (line 74) | function isNeutralDark(rgb: number[]) {
function rgbToHsl (line 92) | function rgbToHsl(r: number, g: number, b: number) {
FILE: packages/ui/src/hooks/use-cpu-temperature.ts
function useCpuTemperature (line 3) | function useCpuTemperature() {
FILE: packages/ui/src/hooks/use-cpu.ts
function useCpu (line 7) | function useCpu(options: {poll?: boolean} = {}) {
function useCpuForUi (line 34) | function useCpuForUi(options: {poll?: boolean} = {}) {
FILE: packages/ui/src/hooks/use-debug-install-random-apps.ts
function useDebugInstallRandomApps (line 6) | function useDebugInstallRandomApps() {
FILE: packages/ui/src/hooks/use-device-info.ts
type UiHostInfo (line 4) | type UiHostInfo = {
type DeviceInfoT (line 9) | type DeviceInfoT =
function useDeviceInfo (line 27) | function useDeviceInfo(): DeviceInfoT {
type DeviceInfo (line 71) | type DeviceInfo = RouterOutput['system']['device']
function deviceInfoToHostEnvironment (line 73) | function deviceInfoToHostEnvironment(deviceInfo?: DeviceInfo): UmbrelHos...
FILE: packages/ui/src/hooks/use-disk.ts
constant ONE_SECOND (line 10) | const ONE_SECOND = 1000
function useDisk (line 12) | function useDisk(options: {poll?: boolean} = {}) {
function useDiskForUi (line 46) | function useDiskForUi(options: {poll?: boolean} = {}) {
function useSystemDisk (line 75) | function useSystemDisk(options: {poll?: boolean} = {}) {
function useSystemDiskForUi (line 95) | function useSystemDiskForUi(options: {poll?: boolean} = {}) {
FILE: packages/ui/src/hooks/use-is-externaldns.ts
function useIsExternalDns (line 5) | function useIsExternalDns({onSuccess}: {onSuccess?: (enabled: boolean) =...
FILE: packages/ui/src/hooks/use-is-home-or-pro.ts
function useIsHomeOrPro (line 10) | function useIsHomeOrPro() {
FILE: packages/ui/src/hooks/use-is-mobile.ts
function useIsMobile (line 4) | function useIsMobile() {
function useIsSmallMobile (line 11) | function useIsSmallMobile() {
FILE: packages/ui/src/hooks/use-is-umbrel-home.tsx
function useIsUmbrelHome (line 3) | function useIsUmbrelHome() {
FILE: packages/ui/src/hooks/use-is-umbrel-pro.ts
function useIsUmbrelPro (line 3) | function useIsUmbrelPro() {
FILE: packages/ui/src/hooks/use-language.ts
function useLanguage (line 18) | function useLanguage(): [SupportedLanguageCode, (code: SupportedLanguage...
FILE: packages/ui/src/hooks/use-launch-app.ts
function useLaunchApp (line 30) | function useLaunchApp() {
FILE: packages/ui/src/hooks/use-memory.ts
function useSystemMemory (line 10) | function useSystemMemory(options: {poll?: boolean} = {}) {
function useMemory (line 29) | function useMemory(options: {poll?: boolean} = {}) {
function useSystemMemoryForUi (line 58) | function useSystemMemoryForUi(options: {poll?: boolean} = {}) {
function useMemoryForUi (line 83) | function useMemoryForUi(options: {poll?: boolean} = {}) {
FILE: packages/ui/src/hooks/use-notifications.ts
function useNotifications (line 8) | function useNotifications() {
FILE: packages/ui/src/hooks/use-password.ts
function usePassword (line 7) | function usePassword({onSuccess}: {onSuccess: () => void}) {
FILE: packages/ui/src/hooks/use-prefixed-local-storage.ts
function usePrefixedLocalStorage (line 10) | function usePrefixedLocalStorage<TT>(key: string, defaultValue?: TT) {
FILE: packages/ui/src/hooks/use-query-params.ts
type QueryObject (line 4) | type QueryObject = {[key: string]: string}
function useQueryParams (line 7) | function useQueryParams<T extends QueryObject>() {
FILE: packages/ui/src/hooks/use-scroll-restoration.ts
type ScrollRestorationAction (line 6) | type ScrollRestorationAction = 'restore' | 'reset' | 'ignore'
type ScrollRestorationHandler (line 12) | type ScrollRestorationHandler = (
function useScrollRestoration (line 23) | function useScrollRestoration(
function getScrollPosition (line 72) | function getScrollPosition(key: string) {
function setScrollPosition (line 77) | function setScrollPosition(key: string, pos: number) {
function clearScrollPositions (line 85) | function clearScrollPositions() {
FILE: packages/ui/src/hooks/use-settings-notification-count.ts
function useMounted (line 11) | function useMounted() {
function useSettingsNotificationCount (line 18) | function useSettingsNotificationCount() {
FILE: packages/ui/src/hooks/use-software-update.ts
type UpdateState (line 7) | type UpdateState = 'initial' | 'checking' | 'at-latest' | 'update-availa...
function useSoftwareUpdate (line 9) | function useSoftwareUpdate() {
FILE: packages/ui/src/hooks/use-temperature-unit.ts
type TemperatureUnit (line 13) | type TemperatureUnit = (typeof temperatureDescriptions)[number]['id']
function useTemperatureUnit (line 17) | function useTemperatureUnit(
FILE: packages/ui/src/hooks/use-tor-enabled.ts
function useTorEnabled (line 5) | function useTorEnabled({onSuccess}: {onSuccess?: (enabled: boolean) => v...
FILE: packages/ui/src/hooks/use-update-all-apps.ts
function useUpdateAllApps (line 4) | function useUpdateAllApps() {
FILE: packages/ui/src/hooks/use-user-name.ts
function useUserName (line 7) | function useUserName({onSuccess}: {onSuccess: () => void}) {
FILE: packages/ui/src/hooks/use-version.ts
function useVersion (line 3) | function useVersion() {
FILE: packages/ui/src/hooks/use-widgets.ts
function useWidgets (line 9) | function useWidgets() {
function useEnableWidgets (line 110) | function useEnableWidgets() {
FILE: packages/ui/src/init.tsx
function init (line 29) | function init(element: React.ReactNode) {
FILE: packages/ui/src/layouts/app-store.tsx
function AppStoreLayout (line 24) | function AppStoreLayout() {
function SearchInput (line 71) | function SearchInput({
function CommunityAppsDropdown (line 107) | function CommunityAppsDropdown() {
function SearchResults (line 129) | function SearchResults({query}: {query: string}) {
FILE: packages/ui/src/layouts/bare/bare-page.tsx
function BarePage (line 4) | function BarePage({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/layouts/bare/bare.tsx
function BareLayout (line 6) | function BareLayout() {
FILE: packages/ui/src/layouts/bare/onboarding-page.tsx
function OnboardingPage (line 6) | function OnboardingPage({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/layouts/bare/onboarding.tsx
function OnboardingLayout (line 6) | function OnboardingLayout() {
FILE: packages/ui/src/layouts/bare/shared.tsx
function Title (line 12) | function Title({children}: {children: React.ReactNode}) {
function SubTitle (line 26) | function SubTitle({
function Layout (line 55) | function Layout({
FILE: packages/ui/src/layouts/desktop.tsx
function Desktop (line 12) | function Desktop() {
function InstallFirstAppPage (line 26) | function InstallFirstAppPage() {
function prefetchRouteChunks (line 35) | function prefetchRouteChunks() {
function DesktopPage (line 44) | function DesktopPage() {
FILE: packages/ui/src/layouts/sheet.tsx
function SheetLayout (line 38) | function SheetLayout() {
function SheetCloseButton (line 107) | function SheetCloseButton() {
FILE: packages/ui/src/lib/utils.ts
function cn (line 18) | function cn(...inputs: ClassValue[]) {
FILE: packages/ui/src/modules/app-store/app-page/app-content.tsx
function AppContent (line 16) | function AppContent({
FILE: packages/ui/src/modules/app-store/app-page/app-settings-dialog.tsx
function AppSettingsDialog (line 18) | function AppSettingsDialog() {
function areSelectionsEqual (line 42) | function areSelectionsEqual(a?: Record<string, string>, b?: Record<strin...
function AppSettingsDialogForApp (line 53) | function AppSettingsDialogForApp({
FILE: packages/ui/src/modules/app-store/app-page/default-credentials-dialog.tsx
function DefaultCredentialsDialog (line 18) | function DefaultCredentialsDialog() {
function ShowCredentialsBeforeOpenCheckbox (line 102) | function ShowCredentialsBeforeOpenCheckbox({appId}: {appId: string}) {
FILE: packages/ui/src/modules/app-store/app-page/get-recommendations.ts
function getRecommendationsFor (line 5) | function getRecommendationsFor(apps: RegistryApp[], appId: string) {
FILE: packages/ui/src/modules/app-store/app-page/info-section.tsx
function KV (line 54) | function KV({k, v}: {k: ReactNode; v: ReactNode}) {
function InfoSectionCompatibilityText (line 63) | function InfoSectionCompatibilityText({app}: {app: RegistryApp}) {
FILE: packages/ui/src/modules/app-store/app-page/recommendations-section.tsx
function AppWithDescriptionSmall (line 32) | function AppWithDescriptionSmall({
FILE: packages/ui/src/modules/app-store/app-page/settings-section.tsx
function SettingsSection (line 10) | function SettingsSection({userApp}: {userApp: UserApp}) {
function KV (line 35) | function KV({k, v}: {k: ReactNode; v: ReactNode}) {
FILE: packages/ui/src/modules/app-store/app-page/shared.tsx
function ReadMoreMarkdownSection (line 10) | function ReadMoreMarkdownSection({children}: {children: string}) {
FILE: packages/ui/src/modules/app-store/app-page/top-header.tsx
function BackButton (line 74) | function BackButton() {
FILE: packages/ui/src/modules/app-store/app-store-nav.tsx
function ConnectedAppStoreNav (line 13) | function ConnectedAppStoreNav() {
function AppStoreNav (line 34) | function AppStoreNav({activeId, allCategories}: {activeId: string; allCa...
function categoryIdToPath (line 78) | function categoryIdToPath(categoryId: string) {
FILE: packages/ui/src/modules/app-store/community-app-store-dialog.tsx
function CommunityAppStoreDialog (line 24) | function CommunityAppStoreDialog() {
FILE: packages/ui/src/modules/app-store/constants.ts
type Category (line 17) | type Category = (typeof categories)[number]
type Categoryish (line 19) | type Categoryish = Category | 'all' | 'discover'
constant UMBREL_APP_STORE_ID (line 42) | const UMBREL_APP_STORE_ID = 'umbrel-app-store'
FILE: packages/ui/src/modules/app-store/discover/apps-grid-section.tsx
function AppsGridSection (line 11) | function AppsGridSection({overline, title, apps}: {overline: string; tit...
function AppsGridFaintSection (line 26) | function AppsGridFaintSection({title, apps}: {title?: string; apps?: Reg...
function AppWithDescription (line 39) | function AppWithDescription({app, to}: {app: RegistryApp; to?: string}) {
FILE: packages/ui/src/modules/app-store/discover/apps-row-section.tsx
function App (line 23) | function App({app, index}: {app: RegistryApp; index: number}) {
FILE: packages/ui/src/modules/app-store/discover/apps-three-column-section.tsx
type AppsThreeColumnSectionProps (line 14) | type AppsThreeColumnSectionProps = {
function ColorApp (line 60) | function ColorApp({app, className}: {app: RegistryApp; className?: strin...
FILE: packages/ui/src/modules/app-store/os-update-required.tsx
function OSUpdateRequiredDialog (line 17) | function OSUpdateRequiredDialog({
FILE: packages/ui/src/modules/app-store/select-dependencies-dialog.tsx
function SelectDependenciesDialog (line 25) | function SelectDependenciesDialog({
function SelectDependencies (line 107) | function SelectDependencies({
function DependencyStateText (line 221) | function DependencyStateText({appId, appState, onClick}: {appId: string;...
function DependencyDropdown (line 244) | function DependencyDropdown({
FILE: packages/ui/src/modules/app-store/shared.tsx
function SectionTitle (line 18) | function SectionTitle({overline, title}: {overline: string; title: React...
function AppStoreSheetInner (line 27) | function AppStoreSheetInner({
function AppWithName (line 52) | function AppWithName({
FILE: packages/ui/src/modules/app-store/updates-button.tsx
function UpdatesButton (line 11) | function UpdatesButton() {
FILE: packages/ui/src/modules/app-store/updates-dialog.tsx
function UpdatesDialogConnected (line 21) | function UpdatesDialogConnected() {
function UpdatesDialog (line 48) | function UpdatesDialog({
function AppItem (line 86) | function AppItem({app}: {app: RegistryApp}) {
FILE: packages/ui/src/modules/app-store/utils.ts
function preloadFirstFewGalleryImages (line 8) | function preloadFirstFewGalleryImages(app: RegistryApp) {
function getCategoryLabel (line 14) | function getCategoryLabel(categoryId: string): string {
function getAllCategories (line 32) | function getAllCategories(appsGroupedByCategory: Record<string, any[]>) {
FILE: packages/ui/src/modules/auth/ensure-backend-available.tsx
function EnsureBackendAvailable (line 5) | function EnsureBackendAvailable({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/modules/auth/ensure-logged-in.tsx
function EnsureLoggedIn (line 7) | function EnsureLoggedIn({children}: {children?: React.ReactNode}) {
function EnsureLoggedOut (line 15) | function EnsureLoggedOut({children}: {children?: React.ReactNode}) {
function EnsureLoggedInState (line 24) | function EnsureLoggedInState({
FILE: packages/ui/src/modules/auth/ensure-no-raid-mount-failure.tsx
function EnsureNoRaidMountFailure (line 9) | function EnsureNoRaidMountFailure({children}: {children?: React.ReactNod...
FILE: packages/ui/src/modules/auth/ensure-pro-device.tsx
function EnsureProDevice (line 8) | function EnsureProDevice({children}: {children?: React.ReactNode}) {
FILE: packages/ui/src/modules/auth/ensure-user-exists.tsx
function EnsureUserDoesntExist (line 11) | function EnsureUserDoesntExist({children}: {children?: React.ReactNode}) {
function EnsureUserExists (line 20) | function EnsureUserExists({children}: {children?: React.ReactNode}) {
function EnsureUser (line 28) | function EnsureUser({
FILE: packages/ui/src/modules/auth/redirects.tsx
constant SLEEP_TIME (line 8) | const SLEEP_TIME = IS_DEV ? 600 : 0
type Page (line 10) | type Page = 'onboarding' | 'login' | 'home' | 'raid-error'
function RedirectOnboarding (line 25) | function RedirectOnboarding() {
function RedirectLogin (line 43) | function RedirectLogin() {
function RedirectHome (line 66) | function RedirectHome() {
function RedirectRaidError (line 84) | function RedirectRaidError() {
method createRedirectSearch (line 104) | createRedirectSearch() {
method getRedirectPath (line 107) | getRedirectPath() {
FILE: packages/ui/src/modules/auth/shared.ts
constant JWT_LOCAL_STORAGE_KEY (line 5) | const JWT_LOCAL_STORAGE_KEY = 'jwt'
constant JWT_REFRESH_LOCAL_STORAGE_KEY (line 6) | const JWT_REFRESH_LOCAL_STORAGE_KEY = 'jwt-last-refreshed'
function initTokenRenewal (line 8) | function initTokenRenewal() {
FILE: packages/ui/src/modules/auth/use-auth.tsx
function useJwt (line 11) | function useJwt() {
function useAuth (line 25) | function useAuth() {
FILE: packages/ui/src/modules/bare/alert.tsx
function Alert (line 5) | function Alert({children, className}: {children: React.ReactNode; classN...
FILE: packages/ui/src/modules/bare/failed-layout.tsx
function FailedLayout (line 10) | function FailedLayout({
FILE: packages/ui/src/modules/bare/progress-layout.tsx
function ProgressLayout (line 8) | function ProgressLayout({
FILE: packages/ui/src/modules/bare/progress.tsx
function Progress (line 7) | function Progress({value, children}: {value?: number; children?: ReactNo...
FILE: packages/ui/src/modules/bare/success-layout.tsx
function SuccessLayout (line 7) | function SuccessLayout({
FILE: packages/ui/src/modules/community-app-store/community-badge.tsx
function CommunityBadge (line 4) | function CommunityBadge({className}: {className?: string}) {
FILE: packages/ui/src/modules/desktop/app-grid/app-grid.tsx
function AppGrid (line 10) | function AppGrid({
function ArrowButtonWrapper (line 112) | function ArrowButtonWrapper({side, children}: {side: 'left' | 'right'; c...
function PageInner (line 126) | function PageInner({children, innerRef}: {children?: ReactNode; innerRef...
FILE: packages/ui/src/modules/desktop/app-grid/app-pagination-utils.tsx
type PageT (line 7) | type PageT = {
function usePager (line 23) | function usePager({apps, widgets, forceBreakpoint}: PageT & {forceBreakp...
FILE: packages/ui/src/modules/desktop/app-grid/paginator.tsx
constant DATA_INDEX_ATTR (line 7) | const DATA_INDEX_ATTR = 'data-index'
function usePaginator (line 9) | function usePaginator(pageCount: number) {
function Page (line 66) | function Page({index, children, className}: {index: number; children: Re...
function ArrowButton (line 75) | function ArrowButton({
function PaginatorPill (line 97) | function PaginatorPill({active, onClick}: {active?: boolean; onClick: ()...
function PaginatorPills (line 115) | function PaginatorPills({
FILE: packages/ui/src/modules/desktop/app-icon.tsx
constant APP_ICON_PLACEHOLDER_SRC (line 24) | const APP_ICON_PLACEHOLDER_SRC = '/assets/app-icon-placeholder.svg'
function AppIcon (line 26) | function AppIcon({
function AppLabel (line 119) | function AppLabel({state, label = ''}: {state: AppStateOrLoading; label?...
function AppIconConnected (line 149) | function AppIconConnected({appId}: {appId: string}) {
function ContextMenuItemLinkToAppStore (line 300) | function ContextMenuItemLinkToAppStore({appId}: {appId: string}) {
FILE: packages/ui/src/modules/desktop/desktop-content.tsx
function DesktopContent (line 17) | function DesktopContent({onSearchClick}: {onSearchClick?: () => void}) {
FILE: packages/ui/src/modules/desktop/desktop-context-menu.tsx
function DesktopContextMenu (line 13) | function DesktopContextMenu({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/modules/desktop/desktop-misc.tsx
function Search (line 8) | function Search({onClick}: {onClick?: () => void}) {
function AppGridGradientMasking (line 21) | function AppGridGradientMasking() {
function GradientMaskSide (line 36) | function GradientMaskSide({side}: {side: 'left' | 'right'}) {
FILE: packages/ui/src/modules/desktop/desktop-preview.tsx
function DesktopPreviewConnected (line 36) | function DesktopPreviewConnected() {
function DesktopPreviewContent (line 104) | function DesktopPreviewContent() {
function DesktopPreviewFrame (line 139) | function DesktopPreviewFrame({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/modules/desktop/dock-item.tsx
type HTMLDivProps (line 8) | type HTMLDivProps = HTMLMotionProps<'div'>
type DockItemProps (line 9) | type DockItemProps = {
constant BOUNCE_DURATION (line 22) | const BOUNCE_DURATION = 0.4
function DockItem (line 24) | function DockItem({
function OpenPill (line 125) | function OpenPill() {
FILE: packages/ui/src/modules/desktop/dock.tsx
constant DOCK_BOTTOM_PADDING_PX (line 20) | const DOCK_BOTTOM_PADDING_PX = 10
constant DOCK_DIMENSIONS_PX (line 22) | const DOCK_DIMENSIONS_PX = {
type DockDimensionsPx (line 40) | type DockDimensionsPx = {
function useDockDimensions (line 47) | function useDockDimensions(options?: {isPreview?: boolean}): DockDimensi...
function Dock (line 60) | function Dock() {
function DockPreview (line 161) | function DockPreview() {
function DockSpacer (line 214) | function DockSpacer({className}: {className?: string}) {
function DockBottomPositioner (line 219) | function DockBottomPositioner({children}: {children: React.ReactNode}) {
FILE: packages/ui/src/modules/desktop/greeting-message.ts
function greetingMessage (line 4) | function greetingMessage(name: string) {
function getPartofDay (line 16) | function getPartofDay() {
FILE: packages/ui/src/modules/desktop/header.tsx
function Header (line 5) | function Header({userName}: {userName: string}) {
FILE: packages/ui/src/modules/desktop/install-first-app.tsx
function InstallFirstApp (line 14) | function InstallFirstApp() {
function Cards (line 46) | function Cards() {
function CardsSkeleton (line 80) | function CardsSkeleton() {
function SkeletonApps (line 99) | function SkeletonApps() {
function SkeletonApp (line 110) | function SkeletonApp() {
function AppApp (line 114) | function AppApp({app}: {app: RegistryApp}) {
function App (line 119) | function App({
FILE: packages/ui/src/modules/desktop/logout-dialog.tsx
function LogoutDialog (line 16) | function LogoutDialog() {
FILE: packages/ui/src/modules/desktop/uninstall-confirmation-dialog.tsx
function UninstallConfirmationDialog (line 14) | function UninstallConfirmationDialog({
FILE: packages/ui/src/modules/desktop/uninstall-these-first-dialog.tsx
function UninstallTheseFirstDialog (line 10) | function UninstallTheseFirstDialog({
function AppWithName (line 61) | function AppWithName({icon, appName}: {icon: string; appName: ReactNode}) {
FILE: packages/ui/src/modules/floating-island/bare-island.tsx
type IslandProps (line 26) | interface IslandProps {
type IslandChildProps (line 35) | interface IslandChildProps {
FILE: packages/ui/src/modules/floating-island/container.tsx
function FloatingIslandContainer (line 22) | function FloatingIslandContainer() {
FILE: packages/ui/src/modules/immersive-picker/index.tsx
function ImmersivePickerDialogContentInit (line 25) | function ImmersivePickerDialogContentInit({title, children}: {title: str...
function ImmersivePickerItem (line 34) | function ImmersivePickerItem({
function BackLink (line 69) | function BackLink({to, children}: {to: string; children: React.ReactNode...
function ImmersivePickerDialogContent (line 81) | function ImmersivePickerDialogContent({children}: {children: React.React...
function AppDropdown (line 89) | function AppDropdown({
FILE: packages/ui/src/modules/migrate/migrate-image.tsx
constant FROM_RASPBERRY_PI_URL (line 6) | const FROM_RASPBERRY_PI_URL = '/assets/migrate-raspberrypi-umbrel-home.png'
constant FROM_UMBREL_URL (line 7) | const FROM_UMBREL_URL = '/assets/migrate-umbrel-home-umbrel-home.png'
function MigrateImage (line 9) | function MigrateImage() {
FILE: packages/ui/src/modules/migrate/migrate-inner.tsx
function MigrateInner (line 8) | function MigrateInner({
FILE: packages/ui/src/modules/sheet-top-fixed.tsx
constant SHEET_FIXED_ID (line 4) | const SHEET_FIXED_ID = 'sheet-fixed-id'
function SheetFixedTarget (line 6) | function SheetFixedTarget() {
function SheetFixedContent (line 9) | function SheetFixedContent({children}: {children: ReactNode}) {
FILE: packages/ui/src/modules/widgets/four-stats-widget.tsx
function FourStatsWidget (line 7) | function FourStatsWidget({
function Item (line 34) | function Item(item?: FourStatsItem) {
FILE: packages/ui/src/modules/widgets/index.tsx
function Widget (line 31) | function Widget({appId, config: manifestConfig}: {appId: string; config:...
function SystemThreeUpWidget (line 115) | function SystemThreeUpWidget({items, ...props}: ComponentPropsWithRef<ty...
function ExampleWidget (line 132) | function ExampleWidget<T extends WidgetType = WidgetType>({
function LoadingWidget (line 185) | function LoadingWidget<T extends WidgetType = WidgetType>({type, onClick...
function ErrorWidget (line 218) | function ErrorWidget({error}: {error: string}) {
FILE: packages/ui/src/modules/widgets/list-emoji-widget.tsx
function ListEmojiWidget (line 6) | function ListEmojiWidget({
function ListEmojiItem (line 40) | function ListEmojiItem(item?: ListEmojiItem) {
function limitToOneEmoji (line 51) | function limitToOneEmoji(str: string) {
FILE: packages/ui/src/modules/widgets/list-widget.tsx
function ListWidget (line 8) | function ListWidget({
function ListItem (line 42) | function ListItem(item?: ListWidgetItem) {
FILE: packages/ui/src/modules/widgets/shared/backdrop-blur-context.tsx
type Variant (line 3) | type Variant = 'with-backdrop-blur' | 'default'
FILE: packages/ui/src/modules/widgets/shared/constants.ts
constant DEFAULT_REFRESH_MS (line 5) | const DEFAULT_REFRESH_MS = 1000 * 60 * 5
type BaseWidget (line 7) | type BaseWidget = {
type WidgetType (line 24) | type WidgetType = (typeof widgetTypes)[number]
type Link (line 32) | type Link = string
type FourStatsItem (line 34) | type FourStatsItem = BaseWidget & {
type FourStatsWidget (line 39) | type FourStatsWidget = BaseWidget & {
type FourStatsWidgetProps (line 44) | type FourStatsWidgetProps = Omit<FourStatsWidget, 'type'>
type ThreeStatsItem (line 46) | type ThreeStatsItem = {
type ThreeStatsWidget (line 51) | type ThreeStatsWidget = BaseWidget & {
type ThreeStatsWidgetProps (line 56) | type ThreeStatsWidgetProps = Omit<ThreeStatsWidget, 'type'>
type TwoStatsWithProgressItem (line 59) | type TwoStatsWithProgressItem = {
type TwoStatsWithProgressWidget (line 66) | type TwoStatsWithProgressWidget = BaseWidget & {
type TwoStatsWithProgressWidgetProps (line 71) | type TwoStatsWithProgressWidgetProps = Omit<TwoStatsWithProgressWidget, ...
type TextWithProgressWidget (line 73) | type TextWithProgressWidget = BaseWidget & {
type TextWithProgressWidgetProps (line 83) | type TextWithProgressWidgetProps = Omit<TextWithProgressWidget, 'type'>
type TextWithButtonsWidget (line 85) | type TextWithButtonsWidget = BaseWidget & {
type TextWithButtonsWidgetProps (line 96) | type TextWithButtonsWidgetProps = Omit<TextWithButtonsWidget, 'type'>
type ListWidgetItem (line 98) | type ListWidgetItem = {
type ListWidget (line 102) | type ListWidget = BaseWidget & {
type ListWidgetProps (line 108) | type ListWidgetProps = Omit<ListWidget, 'type'>
type ListEmojiItem (line 110) | type ListEmojiItem = {
type ListEmojiWidget (line 114) | type ListEmojiWidget = BaseWidget & {
type ListEmojiWidgetProps (line 120) | type ListEmojiWidgetProps = Omit<ListEmojiWidget, 'type'>
type AnyWidgetConfig (line 122) | type AnyWidgetConfig =
type WidgetConfig (line 135) | type WidgetConfig<T extends WidgetType = WidgetType> = Extract<AnyWidget...
type ExampleWidgetConfig (line 139) | type ExampleWidgetConfig<T extends WidgetType = WidgetType> = T extends ...
type RegistryWidget (line 146) | type RegistryWidget<T extends WidgetType = WidgetType> = {
constant MAX_WIDGETS (line 156) | const MAX_WIDGETS = 3
FILE: packages/ui/src/modules/widgets/shared/shared.tsx
type WidgetContainerButtonProps (line 40) | type WidgetContainerButtonProps = React.ComponentPropsWithoutRef<'button'>
type WidgetContainerDivProps (line 41) | type WidgetContainerDivProps = React.ComponentPropsWithoutRef<'div'>
type WidgetContainerProps (line 42) | type WidgetContainerProps = WidgetContainerButtonProps | WidgetContainer...
FILE: packages/ui/src/modules/widgets/shared/stat-text.tsx
function StatText (line 5) | function StatText({title, value, valueSub}: {title?: string; value?: str...
FILE: packages/ui/src/modules/widgets/shared/tabler-icon.tsx
function sanitizeIconName (line 5) | function sanitizeIconName(input: string) {
function TablerIcon (line 11) | function TablerIcon({iconName, className, ...props}: {iconName: string} ...
FILE: packages/ui/src/modules/widgets/shared/widget-wrapper.tsx
function WidgetWrapper (line 5) | function WidgetWrapper({label, children}: {label: string; children?: Rea...
FILE: packages/ui/src/modules/widgets/text-with-buttons-widget.tsx
function TextWithButtonsWidget (line 11) | function TextWithButtonsWidget({
function WidgetButton (line 43) | function WidgetButton({onClick, children}: {onClick: () => void; childre...
FILE: packages/ui/src/modules/widgets/text-with-progress-widget.tsx
function TextWithProgressWidget (line 9) | function TextWithProgressWidget({
FILE: packages/ui/src/modules/widgets/three-stats-widget.tsx
function ThreeStatsWidget (line 8) | function ThreeStatsWidget({
function Item (line 34) | function Item(item?: ThreeStatsItem) {
FILE: packages/ui/src/modules/widgets/two-stats-with-guage-widget.tsx
function TwoStatsWidget (line 8) | function TwoStatsWidget({
function Item (line 31) | function Item(item?: TwoStatsWithProgressItem) {
FILE: packages/ui/src/modules/wifi/desktop-wifi-button-connected.tsx
function DesktopWifiButtonConnected (line 8) | function DesktopWifiButtonConnected({className}: {className?: string}) {
FILE: packages/ui/src/modules/wifi/icon.tsx
function WifiIcon (line 6) | function WifiIcon({bars = 4, className}: {bars: number; className?: stri...
function WifiIcon2 (line 27) | function WifiIcon2({bars = 4, ...props}: {bars: number} & SVGProps<SVGSV...
function WifiIcon2Circled (line 70) | function WifiIcon2Circled({bars = 4, isConnected}: {bars: number; isConn...
function LockIcon (line 89) | function LockIcon() {
FILE: packages/ui/src/modules/wifi/wifi-drawer-or-dialog.tsx
type NetworkStatus (line 31) | type NetworkStatus = RouterOutput['wifi']['connected']
function WifiDrawerOrDialogContent (line 33) | function WifiDrawerOrDialogContent() {
function WifiDrawerOrDialog (line 121) | function WifiDrawerOrDialog(props: React.ComponentProps<typeof DrawerPri...
function DrawerOrDialogContent (line 128) | function DrawerOrDialogContent({header, children}: {header?: ReactNode; ...
function EnabledContent (line 152) | function EnabledContent({
function Network (line 215) | function Network({
function AnimateHeight (line 297) | function AnimateHeight({children}: {children: ReactNode}) {
type ConnectData (line 335) | type ConnectData = {ssid: string; password?: string}
type ConnectProps (line 336) | type ConnectProps = {
function ConnectWithConfirmation (line 344) | function ConnectWithConfirmation({onConnect, ...rest}: ConnectProps) {
function Connect (line 382) | function Connect({network, status, onConnect, error, passwordInputRef}: ...
function Message (line 434) | function Message({children}: {children?: React.ReactNode}) {
FILE: packages/ui/src/modules/wifi/wifi-item-content.tsx
function WifiListItemContent (line 10) | function WifiListItemContent({
FILE: packages/ui/src/modules/wifi/wifi-list-row-connected-description.tsx
function WifiListRowConnectedDescription (line 7) | function WifiListRowConnectedDescription({network}: {network: Partial<Wi...
FILE: packages/ui/src/providers/apps.tsx
type AppT (line 7) | type AppT = {
type AppsContextT (line 74) | type AppsContextT = {
function AppsProvider (line 86) | function AppsProvider({children}: {children: React.ReactNode}) {
function useApps (line 114) | function useApps() {
function useUserApp (line 121) | function useUserApp(id?: string | null) {
FILE: packages/ui/src/providers/auth-bootstrap.tsx
function AuthBootstrap (line 9) | function AuthBootstrap() {
FILE: packages/ui/src/providers/available-apps.tsx
type AppsContextT (line 8) | type AppsContextT =
function AvailableAppsProvider (line 22) | function AvailableAppsProvider({children}: {children: React.ReactNode}) {
function useAvailableApps (line 45) | function useAvailableApps(registryId: string = UMBREL_APP_STORE_ID) {
function useAllAvailableApps (line 63) | function useAllAvailableApps() {
function useAvailableApp (line 80) | function useAvailableApp(id?: string | null, registryId: string = UMBREL...
FILE: packages/ui/src/providers/confirmation/confirmation-provider.tsx
type ConfirmationProviderProps (line 12) | interface ConfirmationProviderProps {
FILE: packages/ui/src/providers/confirmation/generic-confirmation-dialog.tsx
type GenericConfirmationDialogProps (line 16) | interface GenericConfirmationDialogProps {
FILE: packages/ui/src/providers/confirmation/types.ts
type ConfirmationAction (line 4) | type ConfirmationAction = {
type ConfirmationOptions (line 11) | type ConfirmationOptions = {
type ConfirmationResult (line 19) | type ConfirmationResult = {
type ResolveFunction (line 25) | type ResolveFunction = (value: ConfirmationResult | PromiseLike<Confirma...
type RejectFunction (line 27) | type RejectFunction = (reason?: any) => void
type ConfirmationContextType (line 30) | type ConfirmationContextType = {
FILE: packages/ui/src/providers/global-files.tsx
type AudioState (line 16) | interface AudioState {
type UploadStats (line 21) | interface UploadStats {
type UploadStatus (line 30) | type UploadStatus = 'uploading' | 'collided' | 'retrying' | 'error' | 'c...
type UploadingFileSystemItem (line 31) | interface UploadingFileSystemItem extends FileSystemItem {
type OperationProgress (line 39) | type OperationProgress = RouterOutput['files']['operationProgress'][number]
type OperationsInProgress (line 40) | type OperationsInProgress = OperationProgress[]
type GlobalFilesContextValue (line 42) | interface GlobalFilesContextValue {
function GlobalFilesProvider (line 109) | function GlobalFilesProvider({children}: {children: React.ReactNode}) {
function useGlobalFiles (line 584) | function useGlobalFiles() {
FILE: packages/ui/src/providers/global-system-state/index.tsx
type SystemStatus (line 23) | type SystemStatus = RouterOutput['system']['status']
function GlobalSystemStateProvider (line 38) | function GlobalSystemStateProvider({children}: {children: ReactNode}) {
function useGlobalSystemState (line 310) | function useGlobalSystemState() {
FILE: packages/ui/src/providers/global-system-state/migrate.tsx
function useMigrate (line 7) | function useMigrate({onMutate, onSuccess}: {onMutate?: () => void; onSuc...
function MigratingCover (line 18) | function MigratingCover({onRetry}: {onRetry: () => void}) {
function useSoftwareUpdate (line 57) | function useSoftwareUpdate({
FILE: packages/ui/src/providers/global-system-state/reset.tsx
function useReset (line 6) | function useReset({onMutate, onError}: {onMutate?: () => void; onError?:...
function ResettingCover (line 19) | function ResettingCover() {
FILE: packages/ui/src/providers/global-system-state/restart.tsx
function useRestart (line 6) | function useRestart({onMutate, onSuccess}: {onMutate?: () => void; onSuc...
function RestartingCover (line 16) | function RestartingCover() {
FILE: packages/ui/src/providers/global-system-state/restore.tsx
function RestoreCover (line 8) | function RestoreCover() {
FILE: packages/ui/src/providers/global-system-state/shutdown.tsx
function useShutdown (line 6) | function useShutdown({onMutate, onSuccess}: {onMutate?: () => void; onSu...
function ShuttingDownCover (line 16) | function ShuttingDownCover() {
FILE: packages/ui/src/providers/global-system-state/update.tsx
function useUpdate (line 7) | function useUpdate({onMutate, onSuccess}: {onMutate?: () => void; onSucc...
function UpdatingCover (line 18) | function UpdatingCover({onRetry}: {onRetry: () => void}) {
FILE: packages/ui/src/providers/immersive-dialog.tsx
type ImmersiveDialogContextValue (line 12) | interface ImmersiveDialogContextValue {
function ImmersiveDialogProvider (line 20) | function ImmersiveDialogProvider({children}: {children: ReactNode}) {
function useImmersiveDialogOpen (line 28) | function useImmersiveDialogOpen() {
function useImmersiveDialogCounter (line 34) | function useImmersiveDialogCounter() {
FILE: packages/ui/src/providers/language.tsx
function RemoteLanguageInjector (line 7) | function RemoteLanguageInjector() {
FILE: packages/ui/src/providers/prefetch.tsx
function Prefetcher (line 10) | function Prefetcher() {
FILE: packages/ui/src/providers/sheet-sticky-header.tsx
constant SCROLL_THRESHOLD (line 9) | const SCROLL_THRESHOLD = 110
constant SHEET_HEADER_ID (line 10) | const SHEET_HEADER_ID = 'sheet-header-root-id'
type ContextT (line 12) | type ContextT = {
function SheetStickyHeaderProvider (line 20) | function SheetStickyHeaderProvider({
function useSheetStickyHeader (line 50) | function useSheetStickyHeader() {
function SheetStickyHeader (line 59) | function SheetStickyHeader(props: ComponentPropsWithoutRef<'div'>) {
function SheetStickyHeaderTarget (line 70) | function SheetStickyHeaderTarget() {
FILE: packages/ui/src/providers/wallpaper.tsx
type WallpaperBase (line 11) | type WallpaperBase = {
function getWallpaperThumbUrl (line 130) | function getWallpaperThumbUrl(wallpaper: WallpaperBase) {
type Wallpaper (line 134) | type Wallpaper = (typeof wallpapers)[number]
type WallpaperId (line 135) | type WallpaperId = (typeof wallpapers)[number]['id']
type WallpaperType (line 147) | type WallpaperType = {
function WallpaperProviderConnected (line 170) | function WallpaperProviderConnected({children}: {children: ReactNode}) {
function WallpaperProvider (line 188) | function WallpaperProvider({
function useWallpaperCssVars (line 231) | function useWallpaperCssVars(wallpaperId?: WallpaperId) {
function Wallpaper (line 251) | function Wallpaper({
function useRemoteWallpaper (line 312) | function useRemoteWallpaper(onSuccess?: (id: WallpaperId) => void) {
function RemoteWallpaperInjector (line 349) | function RemoteWallpaperInjector() {
constant LIGHTEN_AMOUNT (line 364) | const LIGHTEN_AMOUNT = 8
function brandHslLighterByAmount (line 365) | function brandHslLighterByAmount(hsl: string, amount: number) {
function brandHslLighter (line 374) | function brandHslLighter(hsl: string) {
function brandHslLightest (line 377) | function brandHslLightest(hsl: string) {
FILE: packages/ui/src/routes/app-store/app-page/index.tsx
function AppPage (line 17) | function AppPage() {
FILE: packages/ui/src/routes/app-store/category-page.tsx
function CategoryPage (line 11) | function CategoryPage() {
function CategoryContent (line 22) | function CategoryContent() {
FILE: packages/ui/src/routes/app-store/discover.tsx
function DiscoverUnavailable (line 28) | function DiscoverUnavailable() {
function Discover (line 37) | function Discover() {
function DiscoverContent (line 48) | function DiscoverContent() {
FILE: packages/ui/src/routes/app-store/use-discover-query.tsx
type Banner (line 5) | type Banner = {
type Section (line 10) | type Section = {
type DiscoverData (line 20) | type DiscoverData = {
function useDiscoverQuery (line 25) | function useDiscoverQuery() {
FILE: packages/ui/src/routes/community-app-store/app-page/index.tsx
function CommunityAppPage (line 14) | function CommunityAppPage() {
FILE: packages/ui/src/routes/community-app-store/index.tsx
function CommunityAppStoreHome (line 14) | function CommunityAppStoreHome() {
FILE: packages/ui/src/routes/edit-widgets/index.tsx
function EditWidgetsPage (line 10) | function EditWidgetsPage() {
FILE: packages/ui/src/routes/edit-widgets/widget-selector.tsx
function WidgetSelector (line 19) | function WidgetSelector({open, onOpenChange}: {open: boolean; onOpenChan...
function WidgetSheet (line 114) | function WidgetSheet({
function WidgetSection (line 157) | function WidgetSection({iconSrc, title, children}: {iconSrc: string; tit...
function PlusIcon (line 170) | function PlusIcon({className}: {className?: string}) {
function MinusIcon (line 178) | function MinusIcon({className}: {className?: string}) {
function WidgetChecker (line 186) | function WidgetChecker({
FILE: packages/ui/src/routes/factory-reset/_components/confirm-with-password.tsx
function ConfirmWithPassword (line 11) | function ConfirmWithPassword({
FILE: packages/ui/src/routes/factory-reset/_components/review-data.tsx
function ReviewData (line 13) | function ReviewData() {
FILE: packages/ui/src/routes/factory-reset/index.tsx
function FactoryReset (line 13) | function FactoryReset() {
function SplitDialog (line 52) | function SplitDialog({children}: {children: React.ReactNode}) {
function SplitLeftContent (line 61) | function SplitLeftContent() {
FILE: packages/ui/src/routes/live-usage.tsx
function LiveUsageDialog (line 31) | function LiveUsageDialog() {
type SelectedTab (line 50) | type SelectedTab = 'storage' | 'memory' | 'cpu'
function LiveUsageContent (line 52) | function LiveUsageContent() {
function StorageSection (line 155) | function StorageSection() {
function MemorySection (line 182) | function MemorySection() {
function CpuSection (line 202) | function CpuSection() {
function UsageCard (line 216) | function UsageCard({
function ErrorMessage (line 308) | function ErrorMessage({children}: {children?: ReactNode}) {
function AppList (line 319) | function AppList({apps, formatValue}: {apps?: {id: string; used: number}...
function AppListSkeleton (line 344) | function AppListSkeleton({systemApps}: {systemApps?: Array<AppT>}) {
function AppListRow (line 359) | function AppListRow({icon, title, value, disabled}: {icon?: string; titl...
FILE: packages/ui/src/routes/login.tsx
type Step (line 11) | type Step = 'password' | '2fa'
function Login (line 13) | function Login() {
FILE: packages/ui/src/routes/not-found.tsx
function NotFound (line 20) | function NotFound() {
FILE: packages/ui/src/routes/notifications.tsx
function NotificationContent (line 24) | function NotificationContent({children}: {children: string}) {
type NotificationContent (line 78) | type NotificationContent = {
function parseBackupNotificationId (line 91) | function parseBackupNotificationId(notification: string): {repoId: strin...
function getBackupFailingContent (line 102) | function getBackupFailingContent(
function getMigratedBackThatMacUpContent (line 155) | function getMigratedBackThatMacUpContent(): NotificationContent {
function getDefaultNotificationContent (line 166) | function getDefaultNotificationContent(notification: string): Notificati...
function Notifications (line 173) | function Notifications() {
FILE: packages/ui/src/routes/onboarding/account-created.tsx
function AccountCreated (line 12) | function AccountCreated() {
FILE: packages/ui/src/routes/onboarding/create-account.tsx
type AccountCredentials (line 17) | type AccountCredentials = {
function CreateAccount (line 23) | function CreateAccount() {
FILE: packages/ui/src/routes/onboarding/index.tsx
function useAutoDetectLanguage (line 12) | function useAutoDetectLanguage() {
function OnboardingStart (line 39) | function OnboardingStart() {
FILE: packages/ui/src/routes/onboarding/onboarding-footer.tsx
type OnboardingAction (line 14) | enum OnboardingAction {
type OnboardingFooterProps (line 19) | interface OnboardingFooterProps {
function OnboardingFooter (line 26) | function OnboardingFooter({action}: OnboardingFooterProps) {
function OnboardingLanguageDropdownTrigger (line 60) | function OnboardingLanguageDropdownTrigger() {
FILE: packages/ui/src/routes/onboarding/raid/index.tsx
constant MIN_SCAN_DISPLAY_TIME (line 12) | const MIN_SCAN_DISPLAY_TIME = 3000
function Raid (line 16) | function Raid() {
FILE: packages/ui/src/routes/onboarding/raid/raid-error.tsx
type RaidErrorProps (line 8) | type RaidErrorProps = {
function RaidError (line 18) | function RaidError({title, instructions, image}: RaidErrorProps) {
FILE: packages/ui/src/routes/onboarding/raid/setup.tsx
function getHealthWarningMessage (line 47) | function getHealthWarningMessage(device: StorageDevice): string | null {
function FailSafeInfo (line 66) | function FailSafeInfo({
function RaidSetup (line 128) | function RaidSetup() {
FILE: packages/ui/src/routes/onboarding/raid/ssd-health-dialog.tsx
type Warning (line 13) | type Warning = {
type SsdHealthDialogProps (line 18) | type SsdHealthDialogProps = {
function SsdHealthDialog (line 25) | function SsdHealthDialog({device, slotNumber, open, onOpenChange}: SsdHe...
function useSsdHealthDialog (line 263) | function useSsdHealthDialog() {
FILE: packages/ui/src/routes/onboarding/raid/ssd-tray.tsx
type SsdSlot (line 8) | type SsdSlot = {
type SsdTrayProps (line 13) | type SsdTrayProps = {
function SsdTray (line 32) | function SsdTray({slots, failsafeSlot = -1, onHealthClick}: SsdTrayProps) {
FILE: packages/ui/src/routes/onboarding/raid/use-raid-setup.ts
type StorageDevice (line 6) | type StorageDevice = RouterOutput['hardware']['internalStorage']['getDev...
type RaidType (line 9) | type RaidType = 'storage' | 'failsafe'
constant LIFETIME_WARNING_THRESHOLD (line 26) | const LIFETIME_WARNING_THRESHOLD = 80
constant FAILSAFE_COLOR (line 29) | const FAILSAFE_COLOR = '#FFFFFF'
constant WASTED_COLOR (line 32) | const WASTED_COLOR = '#FF2F63'
function getDeviceHealth (line 36) | function getDeviceHealth(device: StorageDevice) {
function useDetectStorageDevices (line 70) | function useDetectStorageDevices() {
FILE: packages/ui/src/routes/onboarding/restore.tsx
function BackupsRestoreOnboarding (line 31) | function BackupsRestoreOnboarding() {
function UmbrelProRestoreInstructions (line 61) | function UmbrelProRestoreInstructions() {
function RegularRestoreFlow (line 106) | function RegularRestoreFlow() {
function BackupSnapshot (line 391) | function BackupSnapshot({
FILE: packages/ui/src/routes/onboarding/use-onboarding-device.ts
type OnboardingDevice (line 4) | type OnboardingDevice = {
constant DEFAULT (line 33) | const DEFAULT: OnboardingDevice = {
function useOnboardingDevice (line 40) | function useOnboardingDevice(): OnboardingDevice {
FILE: packages/ui/src/routes/raid-error/index.tsx
function TroubleshootingStep (line 31) | function TroubleshootingStep({
function RaidErrorScreen (line 62) | function RaidErrorScreen() {
FILE: packages/ui/src/routes/settings/2fa-disable.tsx
function TwoFactorDisableDialog (line 10) | function TwoFactorDisableDialog() {
function Inner (line 43) | function Inner({onCodeCheck}: {onCodeCheck: (code: string) => Promise<bo...
FILE: packages/ui/src/routes/settings/2fa-enable.tsx
function TwoFactorEnableDialog (line 24) | function TwoFactorEnableDialog() {
function Inner (line 73) | function Inner({
FILE: packages/ui/src/routes/settings/2fa.tsx
function TwoFactorDialog (line 7) | function TwoFactorDialog() {
FILE: packages/ui/src/routes/settings/_components/app-store-preferences-content.tsx
function AppStorePreferencesContent (line 16) | function AppStorePreferencesContent() {
FILE: packages/ui/src/routes/settings/_components/cpu-card-content.tsx
function CpuCardContent (line 6) | function CpuCardContent() {
FILE: packages/ui/src/routes/settings/_components/cpu-temperature-card-content.tsx
function CpuTemperatureCardContent (line 17) | function CpuTemperatureCardContent({
FILE: packages/ui/src/routes/settings/_components/device-info-content.tsx
function DeviceInfoContent (line 13) | function DeviceInfoContent({
FILE: packages/ui/src/routes/settings/_components/device-info-umbrel-pro.tsx
constant CANVAS_SCALE (line 7) | const CANVAS_SCALE = 2
constant ENGRAVE_DISPLAY_HEIGHT (line 9) | const ENGRAVE_DISPLAY_HEIGHT = 150
constant ENGRAVE_CANVAS_WIDTH (line 10) | const ENGRAVE_CANVAS_WIDTH = 400 * CANVAS_SCALE
constant ENGRAVE_CANVAS_HEIGHT (line 11) | const ENGRAVE_CANVAS_HEIGHT = ENGRAVE_DISPLAY_HEIGHT * CANVAS_SCALE
constant COVER_SLIDE_DELAY (line 12) | const COVER_SLIDE_DELAY = 1.0 // seconds to wait after scale-up before c...
FILE: packages/ui/src/routes/settings/_components/language-dropdown.tsx
function LanguageDropdown (line 15) | function LanguageDropdown() {
function LanguageDropdownTrigger (line 24) | function LanguageDropdownTrigger() {
function LanguageDropdownContent (line 37) | function LanguageDropdownContent() {
FILE: packages/ui/src/routes/settings/_components/laser-engraving.tsx
type LaserEngravingProps (line 3) | interface LaserEngravingProps {
type Point (line 15) | interface Point {
type AnimationPhase (line 20) | type AnimationPhase = 'scanning' | 'engraving' | 'touching-up' | 'final-...
class SmokeParticle (line 22) | class SmokeParticle {
method constructor (line 33) | constructor(x: number, y: number) {
method update (line 45) | update(): boolean {
method draw (line 55) | draw(ctx: CanvasRenderingContext2D): void {
method drawLaser (line 136) | drawLaser(ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContex...
method createSmoke (line 158) | createSmoke(x: number, y: number): SmokeParticle[] {
method drawLaserLine (line 169) | drawLaserLine(
function animate (line 365) | function animate(currentTime: number): void {
FILE: packages/ui/src/routes/settings/_components/list-row.tsx
function ListRow (line 7) | function ListRow({
function ListRowMobile (line 59) | function ListRowMobile({
FILE: packages/ui/src/routes/settings/_components/memory-card-content.tsx
function MemoryCardContent (line 7) | function MemoryCardContent() {
FILE: packages/ui/src/routes/settings/_components/no-forgot-password-message.tsx
function NoForgotPasswordMessage (line 1) | function NoForgotPasswordMessage() {
FILE: packages/ui/src/routes/settings/_components/progress-card-content.tsx
function ProgressStatCardContent (line 6) | function ProgressStatCardContent({
FILE: packages/ui/src/routes/settings/_components/settings-content-mobile.tsx
function SettingsContentMobile (line 43) | function SettingsContentMobile() {
FILE: packages/ui/src/routes/settings/_components/settings-content.tsx
function SettingsContent (line 46) | function SettingsContent() {
function WifiSupportedListRow (line 309) | function WifiSupportedListRow() {
FILE: packages/ui/src/routes/settings/_components/settings-summary.tsx
function SettingsSummary (line 10) | function SettingsSummary() {
FILE: packages/ui/src/routes/settings/_components/shared.tsx
function ContactSupportLink (line 21) | function ContactSupportLink({className}: {className?: string}) {
function ChangePasswordWarning (line 34) | function ChangePasswordWarning() {
function useSettingsDialogProps (line 38) | function useSettingsDialogProps() {
FILE: packages/ui/src/routes/settings/_components/software-update-list-row.tsx
function SoftwareUpdateListRow (line 18) | function SoftwareUpdateListRow({isActive}: {isActive: boolean}) {
FILE: packages/ui/src/routes/settings/_components/storage-card-content.tsx
function StorageCardContent (line 7) | function StorageCardContent() {
FILE: packages/ui/src/routes/settings/_components/wallpaper-picker.tsx
constant ITEM_W (line 6) | const ITEM_W = 40
constant GAP (line 7) | const GAP = 4
constant ACTIVE_SCALE (line 8) | const ACTIVE_SCALE = 1.4
function WallpaperItem (line 10) | function WallpaperItem({
function WallpaperPicker (line 46) | function WallpaperPicker({maxW}: {maxW?: number}) {
FILE: packages/ui/src/routes/settings/advanced.tsx
function AdvancedSettingsDrawerOrDialog (line 23) | function AdvancedSettingsDrawerOrDialog() {
function useIsBetaChannel (line 183) | function useIsBetaChannel() {
function CardText (line 208) | function CardText({title, description, trailingIcon}: {title: string; de...
FILE: packages/ui/src/routes/settings/app-store-preferences.tsx
function AppStorePreferencesDialog (line 7) | function AppStorePreferencesDialog() {
FILE: packages/ui/src/routes/settings/change-name.tsx
function ChangeNameDialog (line 8) | function ChangeNameDialog() {
FILE: packages/ui/src/routes/settings/change-password.tsx
function ChangePasswordDialog (line 8) | function ChangePasswordDialog() {
FILE: packages/ui/src/routes/settings/device-info.tsx
function DeviceInfoDialog (line 8) | function DeviceInfoDialog() {
FILE: packages/ui/src/routes/settings/file-sharing.tsx
function FileSharingDrawerOrDialog (line 28) | function FileSharingDrawerOrDialog() {
FILE: packages/ui/src/routes/settings/index.tsx
constant SETTINGS_FULLSCREEN_PATHS (line 22) | const SETTINGS_FULLSCREEN_PATHS = ['/settings/storage'] as const
function isFullscreenSettingsPath (line 25) | function isFullscreenSettingsPath(pathname: string) {
type SettingsDialogKey (line 90) | type SettingsDialogKey = keyof typeof routeToDialogDesktop
function QueryStringDialog (line 98) | function QueryStringDialog() {
function Settings (line 112) | function Settings() {
function CoverTest (line 168) | function CoverTest() {
FILE: packages/ui/src/routes/settings/migration-assistant.tsx
function MigrationAssistantDialog (line 32) | function MigrationAssistantDialog() {
type MigrationState (line 69) | type MigrationState = 'prep' | 'check' | 'error' | 'ready'
function MigrateContent (line 71) | function MigrateContent({deviceName}: {deviceName: string}) {
function MigrationAssistantPrep (line 143) | function MigrationAssistantPrep({
function MigrationAssistantError (line 191) | function MigrationAssistantError({
function WarningMessage (line 245) | function WarningMessage({title, description}: {title: string; descriptio...
function MigrationAssistantReady (line 258) | function MigrationAssistantReady({onNext, deviceName}: {onNext: () => vo...
FILE: packages/ui/src/routes/settings/mobile/account.tsx
function AccountDrawer (line 21) | function AccountDrawer() {
function ChangeName (line 53) | function ChangeName({closeDialog}: {closeDialog: () => void}) {
function ChangePassword (line 80) | function ChangePassword({closeDialog}: {closeDialog: () => void}) {
FILE: packages/ui/src/routes/settings/mobile/app-store-preferences.tsx
function AppStorePreferencesDrawer (line 14) | function AppStorePreferencesDrawer() {
FILE: packages/ui/src/routes/settings/mobile/backups-mobile-drawer.tsx
function BackupsMobileDrawer (line 16) | function BackupsMobileDrawer() {
FILE: packages/ui/src/routes/settings/mobile/device-info.tsx
function DeviceInfoDrawer (line 15) | function DeviceInfoDrawer() {
FILE: packages/ui/src/routes/settings/mobile/language.tsx
function LanguageDrawer (line 18) | function LanguageDrawer() {
FILE: packages/ui/src/routes/settings/mobile/software-update.tsx
function SoftwareUpdateDrawer (line 18) | function SoftwareUpdateDrawer() {
FILE: packages/ui/src/routes/settings/mobile/start-migration-drawer-or-dialog.tsx
function StartMigrationDrawerOrDialog (line 12) | function StartMigrationDrawerOrDialog() {
FILE: packages/ui/src/routes/settings/mobile/tor.tsx
function TorDrawer (line 11) | function TorDrawer() {
FILE: packages/ui/src/routes/settings/mobile/wallpaper.tsx
function WallpaperDrawer (line 18) | function WallpaperDrawer() {
function WallpaperItem (line 56) | function WallpaperItem({
FILE: packages/ui/src/routes/settings/restart.tsx
function RestartDialog (line 17) | function RestartDialog() {
FILE: packages/ui/src/routes/settings/shutdown.tsx
function ShutdownDialog (line 16) | function ShutdownDialog() {
FILE: packages/ui/src/routes/settings/software-update-confirm.tsx
function SoftwareUpdateConfirmDialog (line 10) | function SoftwareUpdateConfirmDialog() {
FILE: packages/ui/src/routes/settings/terminal/_shared.tsx
function TerminalTitleBackLink (line 15) | function TerminalTitleBackLink() {
constant MIN_COLS (line 20) | const MIN_COLS = 80
FILE: packages/ui/src/routes/settings/terminal/app.tsx
function App (line 9) | function App() {
FILE: packages/ui/src/routes/settings/terminal/index.tsx
function TerminalDialog (line 14) | function TerminalDialog() {
function PickerDialogContent (line 33) | function PickerDialogContent() {
FILE: packages/ui/src/routes/settings/terminal/umbrelos.tsx
function UmbrelOs (line 5) | function UmbrelOs() {
FILE: packages/ui/src/routes/settings/troubleshoot/_shared.tsx
type SystemLogType (line 11) | type SystemLogType = RouterInput['system']['logs']['type']
function TroubleshootTitleBackLink (line 13) | function TroubleshootTitleBackLink() {
function useScrollToBottom (line 29) | function useScrollToBottom(ref: React.RefObject<HTMLDivElement | null>, ...
function LogResults (line 38) | function LogResults({children}: {children: string}) {
FILE: packages/ui/src/routes/settings/troubleshoot/app.tsx
function TroubleshootApp (line 14) | function TroubleshootApp() {
function useAppLogs (line 44) | function useAppLogs(appId: string) {
FILE: packages/ui/src/routes/settings/troubleshoot/index.tsx
function TroubleshootDialog (line 13) | function TroubleshootDialog() {
function PickerDialogContent (line 32) | function PickerDialogContent() {
FILE: packages/ui/src/routes/settings/troubleshoot/umbrelos.tsx
function TroubleshootUmbrelOs (line 8) | function TroubleshootUmbrelOs() {
function useSystemLogs (line 26) | function useSystemLogs(type: SystemLogType) {
FILE: packages/ui/src/routes/settings/wifi-unsupported.tsx
function WifiUnsupported (line 15) | function WifiUnsupported() {
function Icon (line 38) | function Icon() {
FILE: packages/ui/src/routes/settings/wifi.tsx
function Wifi (line 4) | function Wifi() {
FILE: packages/ui/src/routes/whats-new-modal.tsx
constant VERSION (line 20) | const VERSION = 'umbrelOS 1.5'
constant FEATURES (line 22) | const FEATURES = [
function DotIndicators (line 56) | function DotIndicators({
function WhatsNewModal (line 100) | function WhatsNewModal() {
FILE: packages/ui/src/trpc/loading-indicator.tsx
function LoadingIndicator (line 5) | function LoadingIndicator() {
FILE: packages/ui/src/trpc/trpc.ts
type RouterInput (line 94) | type RouterInput = inferRouterInputs<AppRouter>
type RouterOutput (line 95) | type RouterOutput = inferRouterOutputs<AppRouter>
type RouterError (line 96) | type RouterError = TRPCClientErrorLike<AppRouter>
type AppState (line 100) | type AppState = RouterOutput['apps']['state']['state']
type InstallState (line 115) | type InstallState = (typeof installStates)[number]
type InstalledState (line 117) | type InstalledState = (typeof installedStates)[number]
type AppStateOrLoading (line 133) | type AppStateOrLoading = 'loading' | AppState
type WifiNetwork (line 137) | type WifiNetwork = Omit<RouterOutput['wifi']['networks'][number], 'active'>
type WifiStatus (line 138) | type WifiStatus = Exclude<RouterOutput['wifi']['connected'], undefined>[...
type WifiStatusUi (line 140) | type WifiStatusUi = WifiStatus | 'loading'
type RegistryApp (line 147) | type RegistryApp = RouterOutput['appStore']['registry'][number]['apps'][...
type UserApp (line 152) | type UserApp = Exclude<RouterOutput['apps']['list'][number], {error: str...
type UserAppError (line 157) | type UserAppError = Extract<RouterOutput['apps']['list'][number], {error...
FILE: packages/ui/src/types.d.ts
type RGBColor (line 6) | type RGBColor = [number, number, number]
class ColorThief (line 7) | class ColorThief {
FILE: packages/ui/src/utils/call-every-interval.ts
constant REFRESH_INTERVAL (line 3) | const REFRESH_INTERVAL: number = MS_PER_HOUR
function callEveryInterval (line 6) | function callEveryInterval(
FILE: packages/ui/src/utils/date-time.ts
constant MS_PER_SECOND (line 7) | const MS_PER_SECOND: number = 1000
constant MS_PER_MINUTE (line 8) | const MS_PER_MINUTE: number = MS_PER_SECOND * 60
constant MS_PER_HOUR (line 9) | const MS_PER_HOUR: number = MS_PER_MINUTE * 60
function duration (line 26) | function duration(seconds: number | undefined, languageCode: SupportedLa...
FILE: packages/ui/src/utils/dialog.ts
constant EXIT_DURATION_MS (line 9) | const EXIT_DURATION_MS = 100
type GlobalDialogKey (line 11) | type GlobalDialogKey = 'logout' | 'live-usage' | 'whats-new'
type AppStoreDialogKey (line 12) | type AppStoreDialogKey = 'updates' | 'add-community-store' | 'default-cr...
type FilesDialogKey (line 13) | type FilesDialogKey =
type DialogKey (line 21) | type DialogKey = GlobalDialogKey | AppStoreDialogKey | SettingsDialogKey...
function afterDelayedClose (line 28) | function afterDelayedClose(cb?: () => void) {
function useAfterDelayedClose (line 32) | function useAfterDelayedClose(open: boolean, cb: () => void) {
function useDialogOpenProps (line 44) | function useDialogOpenProps(dialogKey: DialogKey) {
function useLinkToDialog (line 81) | function useLinkToDialog() {
FILE: packages/ui/src/utils/language.ts
type SupportedLanguageCode (line 20) | type SupportedLanguageCode = (typeof supportedLanguageCodes)[number]
FILE: packages/ui/src/utils/logs.ts
function pushLog (line 6) | function pushLog(log: any) {
function monkeyPatchConsoleLog (line 18) | function monkeyPatchConsoleLog() {
function downloadLogs (line 51) | function downloadLogs() {
FILE: packages/ui/src/utils/misc.ts
function firstNameFromFullName (line 5) | function firstNameFromFullName(name: string) {
function sleep (line 9) | function sleep(milliseconds: number) {
function isNormalNumber (line 13) | function isNormalNumber(value: number | null | undefined): value is numb...
function assertUnreachable (line 19) | function assertUnreachable(x: never): never {
function keyBy (line 26) | function keyBy<T, U extends keyof T>(array: ReadonlyArray<T>, key: U): R...
function urlJoin (line 32) | function urlJoin(base: string, path: string) {
function pathJoin (line 37) | function pathJoin(base: string, path: string) {
function appToUrl (line 42) | function appToUrl(app: UserApp) {
function appToUrlWithAppPath (line 48) | function appToUrlWithAppPath(app: UserApp) {
function isOnionPage (line 52) | function isOnionPage() {
function preloadImage (line 56) | function preloadImage(url: string): Promise<void> {
function isWindows (line 70) | function isWindows() {
function isLinux (line 74) | function isLinux() {
function isMac (line 78) | function isMac() {
function platform (line 82) | function platform() {
constant IS_ANDROID (line 90) | const IS_ANDROID = /Android/i.test(navigator.userAgent)
constant IS_DEV (line 92) | const IS_DEV = localStorage.getItem('debug') === 'true'
function cmdOrCtrl (line 94) | function cmdOrCtrl() {
FILE: packages/ui/src/utils/number.ts
function formatNumberI18n (line 3) | function formatNumberI18n({n, showDecimals = true}: {n: number; showDeci...
FILE: packages/ui/src/utils/pretty-bytes.ts
function maybePrettyBytes (line 6) | function maybePrettyBytes(n: number | undefined | null) {
FILE: packages/ui/src/utils/search.ts
type SearchKey (line 20) | type SearchKey = {
function createSearch (line 25) | function createSearch<T>(items: T[], keys: SearchKey[]) {
FILE: packages/ui/src/utils/seconds-to-eta.ts
function secondsToEta (line 1) | function secondsToEta(seconds: number | null | undefined): string {
FILE: packages/ui/src/utils/system.ts
function trpcDiskToLocal (line 3) | function trpcDiskToLocal(
function trpcMemoryToLocal (line 19) | function trpcMemoryToLocal(
function isTrpcDiskFull (line 34) | function isTrpcDiskFull(data?: RouterOutput['system']['systemDiskUsage']) {
function isTrpcDiskLow (line 38) | function isTrpcDiskLow(data?: RouterOutput['system']['systemDiskUsage']) {
function isTrpcMemoryLow (line 42) | function isTrpcMemoryLow(data?: RouterOutput['system']['systemMemoryUsag...
function isDiskLow (line 48) | function isDiskLow(remaining?: number) {
function isDiskFull (line 56) | function isDiskFull(remaining?: number) {
function isCpuTooHot (line 62) | function isCpuTooHot(warning?: string) {
function isMemoryLow (line 67) | function isMemoryLow({size, used}: {size?: number; used?: number}) {
FILE: packages/ui/src/utils/temperature.ts
function celciusToFahrenheit (line 4) | function celciusToFahrenheit(temperatureInCelcius?: number) {
function formatTemperature (line 10) | function formatTemperature(tempCelcius: number | undefined, unit: 'c' | ...
function temperatureWarningToColor (line 17) | function temperatureWarningToColor(warning?: string) {
function temperatureWarningToMessage (line 29) | function temperatureWarningToMessage(warning?: string) {
FILE: packages/ui/src/utils/wifi.ts
function signalToBars (line 1) | function signalToBars(signal: number) {
FILE: packages/ui/update-translations.js
function getBaseEnglishContent (line 43) | function getBaseEnglishContent(baseBranch) {
function getModifiedKeys (line 57) | function getModifiedKeys(currentContent, baseBranch) {
function getLastCommitForKey (line 96) | function getLastCommitForKey(filePath, key, baseBranch) {
function shouldRegenerateKey (line 125) | function shouldRegenerateKey(key, localeFile, baseBranch) {
function getKeysNeedingRegeneration (line 163) | function getKeysNeedingRegeneration(modifiedEnKeys, localeFile, baseBran...
function generateTranslation (line 176) | async function generateTranslation(englishReferenceContent, textToTransl...
function removeUnusedTranslations (line 205) | async function removeUnusedTranslations(englishReferenceContent) {
function generateAndWriteTranslations (line 248) | async function generateAndWriteTranslations(
function checkAndGenerateTranslations (line 321) | async function checkAndGenerateTranslations(englishReferenceContent, mod...
function start (line 329) | async function start() {
FILE: packages/umbreld/source/cli.ts
function cleanShutdown (line 64) | async function cleanShutdown(signal: string) {
FILE: packages/umbreld/source/constants.ts
constant UMBREL_APP_STORE_REPO (line 2) | const UMBREL_APP_STORE_REPO = 'https://github.com/getumbrel/umbrel-apps....
constant BACKUP_RESTORE_FIRST_START_FLAG (line 5) | const BACKUP_RESTORE_FIRST_START_FLAG = '.is-backups-restore-first-start'
FILE: packages/umbreld/source/index.ts
type StoreSchema (line 26) | type StoreSchema = {
type UmbreldOptions (line 87) | type UmbreldOptions = {
class Umbreld (line 94) | class Umbreld {
method constructor (line 116) | constructor({
method start (line 141) | async start() {
method setBackupRestoreFirstStartFlag (line 212) | private async setBackupRestoreFirstStartFlag() {
method stop (line 225) | async stop() {
FILE: packages/umbreld/source/modules/apps/app-repository.ts
function readYaml (line 16) | async function readYaml(path: string) {
function isValidUrl (line 23) | function isValidUrl(url: string) {
class AppRepository (line 32) | class AppRepository {
method constructor (line 38) | constructor(umbreld: Umbreld, url: string) {
method cleanUrl (line 48) | cleanUrl() {
method atomicClone (line 71) | async atomicClone() {
method getCurrentCommit (line 94) | async getCurrentCommit() {
method checkLatestCommit (line 100) | async checkLatestCommit() {
method isUpdated (line 107) | async isUpdated() {
method update (line 119) | async update() {
method readRegistry (line 134) | async readRegistry() {
FILE: packages/umbreld/source/modules/apps/app-store.ts
class AppStore (line 7) | class AppStore {
method constructor (line 15) | constructor(umbreld: Umbreld, {defaultAppStoreRepo}: {defaultAppStoreR...
method start (line 22) | async start() {
method stop (line 60) | async stop() {
method getRepositories (line 64) | async getRepositories() {
method getDefaultRepository (line 71) | async getDefaultRepository() {
method update (line 76) | async update() {
method registry (line 88) | async registry() {
method addRepository (line 103) | async addRepository(url: string) {
method removeRepository (line 127) | async removeRepository(url: string) {
method getAppTemplateFilePath (line 151) | async getAppTemplateFilePath(appId: string) {
FILE: packages/umbreld/source/modules/apps/app.ts
function readYaml (line 20) | async function readYaml(path: string) {
function writeYaml (line 24) | async function writeYaml(path: string, data: any) {
function readManifestInDirectory (line 28) | async function readManifestInDirectory(dataDirectory: string) {
type AppState (line 33) | type AppState =
class App (line 50) | class App {
method constructor (line 59) | constructor(umbreld: Umbreld, appId: string) {
method readManifest (line 71) | readManifest() {
method readCompose (line 75) | readCompose() {
method readHiddenService (line 79) | async readHiddenService() {
method deriveDeterministicPassword (line 88) | async deriveDeterministicPassword() {
method writeCompose (line 96) | writeCompose(compose: Compose) {
method patchComposeFile (line 100) | async patchComposeFile() {
method pull (line 140) | async pull() {
method install (line 155) | async install() {
method update (line 177) | async update() {
method start (line 213) | async start() {
method stop (line 236) | async stop({persistState = false}: {persistState?: boolean} = {}) {
method restart (line 257) | async restart() {
method uninstall (line 269) | async uninstall() {
method getPids (line 302) | async getPids() {
method getDiskUsage (line 324) | async getDiskUsage() {
method getLogs (line 337) | async getLogs() {
method getContainerIp (line 343) | async getContainerIp(service: string) {
method getBackupIgnoredFilePaths (line 360) | async getBackupIgnoredFilePaths() {
method getWidgetMetadata (line 392) | async getWidgetMetadata(widgetName: string) {
method getWidgetData (line 403) | async getWidgetData(widgetId: string) {
method getDependencies (line 429) | async getDependencies() {
method getSelectedDependencies (line 438) | async getSelectedDependencies() {
method setSelectedDependencies (line 447) | async setSelectedDependencies(selectedDependencies: Record<string, str...
method isBackupIgnored (line 460) | async isBackupIgnored() {
method setBackupIgnored (line 465) | async setBackupIgnored(backupIgnore: boolean) {
method setAutoStart (line 470) | async setAutoStart(autoStart: boolean) {
method shouldAutoStart (line 475) | async shouldAutoStart() {
FILE: packages/umbreld/source/modules/apps/apps.ts
class Apps (line 17) | class Apps {
method constructor (line 23) | constructor(umbreld: Umbreld) {
method cleanDockerState (line 36) | async cleanDockerState() {
method start (line 55) | async start() {
method reinstallMissingAppsAfterRestore (line 207) | private async reinstallMissingAppsAfterRestore(appIds: string[]) {
method stop (line 247) | async stop() {
method isInstalled (line 270) | async isInstalled(appId: string) {
method getApp (line 274) | getApp(appId: string) {
method install (line 281) | async install(appId: string, alternatives?: AppSettings['dependencies'...
method uninstall (line 342) | async uninstall(appId: string) {
method restart (line 358) | async restart(appId: string) {
method update (line 364) | async update(appId: string) {
method trackOpen (line 370) | async trackOpen(appId: string) {
method recentlyOpened (line 392) | async recentlyOpened() {
method setTorEnabled (line 396) | async setTorEnabled(torEnabled: boolean) {
method getTorEnabled (line 422) | async getTorEnabled() {
method setSelectedDependencies (line 426) | async setSelectedDependencies(appId: string, dependencies: Record<stri...
method getDependents (line 431) | async getDependents(appId: string) {
method setHideCredentialsBeforeOpen (line 442) | async setHideCredentialsBeforeOpen(appId: string, value: boolean) {
FILE: packages/umbreld/source/modules/apps/legacy-compat/app-environment.ts
function appEnvironment (line 8) | async function appEnvironment(umbreld: Umbreld, command: string) {
FILE: packages/umbreld/source/modules/apps/legacy-compat/app-script.ts
function appScript (line 8) | async function appScript(umbreld: Umbreld, command: string, arg: string,...
FILE: packages/umbreld/source/modules/apps/schema.ts
type ProgressStatus (line 5) | type ProgressStatus = {
type AppRepositoryMeta (line 18) | type AppRepositoryMeta = z.infer<typeof AppRepositoryMetaSchema>
type AppManifest (line 65) | type AppManifest = z.infer<typeof AppManifestSchema>
function isRecord (line 67) | function isRecord(value: unknown): value is Record<string, any> {
function tryNormalizeVersion (line 71) | function tryNormalizeVersion(version: number | string) {
function validateManifest (line 85) | function validateManifest(parsed: unknown): AppManifest {
type AppSettings (line 107) | type AppSettings = z.infer<typeof AppSettingsSchema>
FILE: packages/umbreld/source/modules/backups/backups.integration.test.ts
function createBackupShare (line 25) | async function createBackupShare(umbreld: Awaited<ReturnType<typeof crea...
FILE: packages/umbreld/source/modules/backups/backups.ts
type Backup (line 21) | type Backup = {
type BackupProgress (line 28) | type BackupProgress = {
type RestoreStatus (line 35) | type RestoreStatus = ProgressStatus & {
type BackupsInProgress (line 41) | type BackupsInProgress = BackupProgress[]
class Backups (line 43) | class Backups {
method constructor (line 64) | constructor(umbreld: Umbreld) {
method start (line 72) | async start() {
method stop (line 86) | async stop() {
method backupOnInterval (line 116) | async backupOnInterval() {
method getRepositories (line 161) | async getRepositories() {
method getRepository (line 166) | async getRepository(id: string) {
method kopia (line 176) | async kopia(
method createRepository (line 216) | async createRepository(virtualPath: string, password: string) {
method connectToExistingRepository (line 222) | async connectToExistingRepository(virtualPath: string, password: strin...
method addRepository (line 229) | async addRepository(virtualPath: string, password: string, createNew =...
method forgetRepository (line 305) | async forgetRepository(repositoryId: string) {
method restoreBackup (line 322) | async restoreBackup(backupId: string) {
method connect (line 417) | private async connect(repositoryId: string) {
method repository (line 442) | async repository(
method getRepositorySize (line 458) | async getRepositorySize(repositoryId: string) {
method backup (line 474) | async backup(repositoryId: string) {
method getIgnoredPaths (line 553) | async getIgnoredPaths() {
method addIgnoredPath (line 558) | async addIgnoredPath(path: string) {
method removeIgnoredPath (line 572) | async removeIgnoredPath(path: string) {
method createIgnoreFile (line 586) | async createIgnoreFile() {
method listBackups (line 655) | async listBackups(repositoryId: string) {
method listAllBackups (line 680) | async listAllBackups() {
method parseBackupId (line 699) | parseBackupId(backupId: string) {
method getBackup (line 705) | async getBackup(backupId: string) {
method listBackupFiles (line 715) | async listBackupFiles(backupId: string, path = '/') {
method mountBackup (line 722) | async mountBackup(backupId: string) {
method unmountBackup (line 777) | async unmountBackup(directoryName: string) {
method unmountAll (line 806) | async unmountAll(): Promise<void> {
FILE: packages/umbreld/source/modules/blacklist-uas/blacklist-uas.ts
function blacklistUASDriver (line 15) | async function blacklistUASDriver() {
FILE: packages/umbreld/source/modules/cli-client.ts
function signJwt (line 14) | async function signJwt() {
function parseValue (line 39) | function parseValue(value: string): any {
function parseArgs (line 59) | function parseArgs(args: string[]): any {
type CliClientOptions (line 74) | type CliClientOptions = {
FILE: packages/umbreld/source/modules/dbus/dbus.ts
class Dbus (line 8) | class Dbus {
method constructor (line 13) | constructor(umbreld: Umbreld) {
method start (line 19) | async start() {
method addDiskEventListeners (line 27) | async addDiskEventListeners() {
method stop (line 67) | async stop() {
FILE: packages/umbreld/source/modules/development.ts
function overrideDevelopmentHostname (line 7) | async function overrideDevelopmentHostname(umbreld: Umbreld, hostname: s...
FILE: packages/umbreld/source/modules/event-bus/event-bus.ts
type MissingInEvents (line 10) | type MissingInEvents = Exclude<keyof EventTypes, (typeof events)[number]>
type _AssertEveryKeyIsListed (line 11) | type _AssertEveryKeyIsListed = MissingInEvents extends never ? true : [`...
type EventTypes (line 29) | type EventTypes = {
class EventBus (line 57) | class EventBus {
method constructor (line 68) | constructor(umbreld: Umbreld) {
method stream (line 75) | stream(event: keyof EventTypes, {signal}: {signal?: AbortSignal} = {}) {
FILE: packages/umbreld/source/modules/files/api.download.integration.test.ts
function extractZipBuffer (line 24) | function extractZipBuffer(buffer: Buffer): Record<string, string> {
FILE: packages/umbreld/source/modules/files/api.ts
function api (line 9) | function api({publicApi, privateApi, umbreld}: ApiOptions) {
FILE: packages/umbreld/source/modules/files/archive.integration.test.ts
function extractZipBuffer (line 27) | function extractZipBuffer(buffer: Buffer): Record<string, string> {
FILE: packages/umbreld/source/modules/files/archive.ts
type ZipEntryData (line 12) | type ZipEntryData = archiver.EntryData & {store?: boolean}
class Archive (line 14) | class Archive {
method constructor (line 18) | constructor(umbreld: Umbreld) {
method start (line 25) | async start() {}
method stop (line 26) | async stop() {}
method zipName (line 29) | zipName(files: string[], {defaultName = 'Archive.zip'} = {}) {
method #shouldSkipCompression (line 38) | #shouldSkipCompression(filePath: string): boolean {
method createZipStream (line 45) | async createZipStream(systemPaths: string[]) {
method createZipFile (line 83) | async createZipFile(virtualPaths: string[]) {
method archive (line 104) | async archive(virtualPaths: string[]) {
method isUnarchiveable (line 109) | isUnarchiveable(path: string) {
method unarchive (line 115) | async unarchive(virtualPath: string) {
FILE: packages/umbreld/source/modules/files/external-storage.integration.test.ts
constant LSBLK_NO_EXTERNAL_DISK (line 462) | const LSBLK_NO_EXTERNAL_DISK = {
constant LSBLK_EXTERNAL_DISK_ATTACHED (line 821) | const LSBLK_EXTERNAL_DISK_ATTACHED = {
constant LSBLK_EXTERNAL_DISK_MOUNTED (line 1380) | const LSBLK_EXTERNAL_DISK_MOUNTED = {
FILE: packages/umbreld/source/modules/files/external-storage.ts
type BlockDevice (line 14) | type BlockDevice = {
function getBlockDevices (line 33) | async function getBlockDevices() {
class ExternalStorage (line 89) | class ExternalStorage {
method constructor (line 96) | constructor(umbreld: Umbreld) {
method supported (line 105) | async supported() {
method start (line 111) | async start() {
method stop (line 134) | async stop() {
method #mountExternalDevices (line 156) | async #mountExternalDevices() {
method unmountExternalDevice (line 226) | async unmountExternalDevice(deviceId: string, {remove = true} = {}) {
method formatExternalDevice (line 270) | async formatExternalDevice({
method #getExternalDevices (line 334) | async #getExternalDevices() {
method getExternalDevicesWithVirtualMountPoints (line 344) | async getExternalDevicesWithVirtualMountPoints() {
method getMountedExternalDevices (line 370) | async getMountedExternalDevices() {
method #unmountAllMountedExternalDevices (line 387) | async #unmountAllMountedExternalDevices() {
method #cleanLeftOverMountPoints (line 401) | async #cleanLeftOverMountPoints() {
method isExternalDeviceConnectedOnUnsupportedDevice (line 427) | async isExternalDeviceConnectedOnUnsupportedDevice() {
FILE: packages/umbreld/source/modules/files/favorites.ts
class Favorites (line 5) | class Favorites {
method constructor (line 10) | constructor(umbreld: Umbreld) {
method start (line 17) | async start() {
method #get (line 28) | async #get() {
method #handleFileChange (line 37) | async #handleFileChange(event: FileChangeEvent) {
method listFavorites (line 46) | async listFavorites() {
method addFavorite (line 65) | async addFavorite(virtualPath: string) {
method removeFavorite (line 83) | async removeFavorite(virtualPath: string) {
method stop (line 95) | async stop() {
FILE: packages/umbreld/source/modules/files/files.ts
constant ALL_OPERATIONS (line 43) | const ALL_OPERATIONS = [
type FileOperation (line 56) | type FileOperation = (typeof ALL_OPERATIONS)[number]
type File (line 58) | type File = {
type DirectoryListing (line 68) | type DirectoryListing = File & {
type Trashmeta (line 73) | type Trashmeta = {
type BaseDirectory (line 77) | type BaseDirectory = '/Home' | '/Trash' | '/Apps' | '/External' | '/Back...
type ViewPreferences (line 79) | type ViewPreferences = {
constant DEFAULT_VIEW_PREFERENCES (line 85) | const DEFAULT_VIEW_PREFERENCES: ViewPreferences = {
type OperationProgress (line 91) | type OperationProgress = {
type OperationsInProgress (line 100) | type OperationsInProgress = OperationProgress[]
class Files (line 102) | class Files {
method constructor (line 123) | constructor(umbreld: Umbreld) {
method start (line 151) | async start() {
method firstRun (line 182) | async firstRun() {
method stop (line 199) | async stop() {
method getBaseDirectory (line 213) | getBaseDirectory(virtualPath: BaseDirectory) {
method createDirectory (line 221) | async createDirectory(virtualPath: string) {
method chownSystemPath (line 249) | async chownSystemPath(systemPath: string) {
method status (line 260) | async status(systemPath: string): Promise<File> {
method isHidden (line 309) | isHidden(filename: string) {
method #listRoot (line 317) | async #listRoot() {
method list (line 333) | async list(virtualPath: string): Promise<DirectoryListing> {
method streamContents (line 384) | async *streamContents(virtualPath: string) {
method #copyWithProgress (line 391) | async #copyWithProgress(sourceSystemPath: string, destinationSystemPat...
method copy (line 437) | async copy(sourceVirtualPath: string, destinationVirtualDirectory: str...
method move (line 491) | async move(sourceVirtualPath: string, destinationVirtualDirectory: str...
method rename (line 546) | async rename(sourceVirtualPath: string, newName: string): Promise<stri...
method trash (line 573) | async trash(virtualPath: string) {
method restore (line 618) | async restore(trashVirtualPath: string, {collision = 'error'} = {}) {
method emptyTrash (line 658) | async emptyTrash() {
method delete (line 682) | async delete(virtualPath: string) {
method getAllowedOperations (line 701) | async getAllowedOperations(virtualPath: string): Promise<FileOperation...
method splitExtension (line 805) | splitExtension(path: string) {
method getUniqueName (line 829) | async getUniqueName(systemPath: string, {maxIndex = 100} = {}) {
method virtualToSystemPathUnsafe (line 848) | virtualToSystemPathUnsafe(virtualPath: string) {
method virtualToSystemPath (line 875) | async virtualToSystemPath(virtualPath: string) {
method systemToVirtualPath (line 904) | systemToVirtualPath(systemPath: string) {
method getViewPreferences (line 922) | async getViewPreferences(): Promise<ViewPreferences> {
method updateViewPreferences (line 928) | async updateViewPreferences(newViewPreferences: Partial<ViewPreference...
function match (line 943) | function match(path: string, patterns: string[]) {
function normalizePath (line 949) | function normalizePath(path: string) {
function getDeepestExistingPath (line 959) | async function getDeepestExistingPath(path: string) {
function move (line 979) | async function move(sourceSystemPath: string, targetSystemPath: string, ...
FILE: packages/umbreld/source/modules/files/network-storage.integration.test.ts
function createNetworkShare (line 18) | async function createNetworkShare(umbreld: Awaited<ReturnType<typeof cre...
FILE: packages/umbreld/source/modules/files/network-storage.ts
type NetworkShare (line 12) | type NetworkShare = {
class NetworkStorage (line 20) | class NetworkStorage {
method constructor (line 28) | constructor(umbreld: Umbreld) {
method start (line 35) | async start() {
method stop (line 42) | async stop() {
method getShares (line 70) | async getShares() {
method getShareInfo (line 75) | async getShareInfo() {
method #watchAndMountShares (line 86) | async #watchAndMountShares() {
method #isMounted (line 114) | async #isMounted(share: NetworkShare): Promise<boolean> {
method #mountShare (line 126) | async #mountShare(share: NetworkShare): Promise<void> {
method #unmountShare (line 153) | async #unmountShare(share: NetworkShare): Promise<void> {
method #unmountAllShares (line 179) | async #unmountAllShares(): Promise<void> {
method addShare (line 185) | async addShare(newShare: Omit<NetworkShare, 'mountPath'>) {
method getShare (line 214) | async getShare(mountPath: string) {
method removeShare (line 222) | async removeShare(sharePath: string) {
method discoverServers (line 240) | async discoverServers() {
method discoverSharesOnServer (line 260) | async discoverSharesOnServer(host: string, username: string, password:...
method isServerAnUmbrelDevice (line 282) | async isServerAnUmbrelDevice(address: string) {
FILE: packages/umbreld/source/modules/files/recents.ts
class Recents (line 11) | class Recents {
method constructor (line 22) | constructor(umbreld: Umbreld, {paths}: {paths: string[]}) {
method start (line 30) | async start() {
method get (line 52) | async get() {
method #directWrite (line 67) | async #directWrite() {
method #handleFileChange (line 72) | async #handleFileChange(event: FileChangeEvent) {
method stop (line 113) | async stop() {
FILE: packages/umbreld/source/modules/files/samba.integration.test.ts
function createSmbClient (line 370) | async function createSmbClient(share: string) {
FILE: packages/umbreld/source/modules/files/samba.ts
constant SMB_CONFIG (line 12) | const SMB_CONFIG = `# Generated by umbreld
class Samba (line 74) | class Samba {
method constructor (line 79) | constructor(umbreld: Umbreld) {
method start (line 86) | async start() {
method stop (line 107) | async stop() {
method getSharePassword (line 118) | async getSharePassword() {
method applySharePassword (line 133) | async applySharePassword() {
method applyShares (line 141) | async applyShares() {
method #computeSharename (line 175) | async #computeSharename(name: string, path: string) {
method #get (line 188) | async #get() {
method #handleFileChange (line 197) | async #handleFileChange(event: FileChangeEvent) {
method listShares (line 206) | async listShares() {
method addShare (line 232) | async addShare(virtualPath: string) {
method removeShare (line 270) | async removeShare(virtualPath: string) {
FILE: packages/umbreld/source/modules/files/search.ts
class Search (line 7) | class Search {
method constructor (line 13) | constructor(umbreld: Umbreld) {
method start (line 20) | async start() {}
method stop (line 21) | async stop() {}
method search (line 26) | async search(query: string, maxResults = 250) {
FILE: packages/umbreld/source/modules/files/thumbnails.integration.test.ts
function copyFixtureFile (line 33) | async function copyFixtureFile(
function pollUntil (line 55) | async function pollUntil(
FILE: packages/umbreld/source/modules/files/thumbnails.ts
constant SUPPORTED_THUMBNAIL_EXTENSIONS (line 16) | const SUPPORTED_THUMBNAIL_EXTENSIONS = [
class Thumbnails (line 32) | class Thumbnails {
method constructor (line 61) | constructor(umbreld: Umbreld) {
method start (line 68) | async start() {
method #debouncedGenerateThumbnail (line 98) | #debouncedGenerateThumbnail(systemPath: string): void {
method #handleFileChange (line 125) | async #handleFileChange(event: FileChangeEvent) {
method #isValidFileForThumbnail (line 142) | async #isValidFileForThumbnail(systemPath: string): Promise<boolean> {
method getFilesystemUuid (line 155) | async getFilesystemUuid(systemPath: string, deviceId: number): Promise...
method getThumbnailHash (line 178) | async getThumbnailHash(systemPath: string): Promise<string> {
method hashToThumbnailSystemPath (line 202) | hashToThumbnailSystemPath(hash: string): string {
method #generateThumbnail (line 211) | async #generateThumbnail(systemPath: string, {background = true}: {bac...
method getThumbnailOnDemand (line 243) | async getThumbnailOnDemand(virtualPath: string): Promise<string> {
method getExistingThumbnail (line 260) | async getExistingThumbnail(systemPath: string): Promise<string | undef...
method #pruneOldestThumbnails (line 282) | async #pruneOldestThumbnails(): Promise<void> {
method stop (line 352) | async stop() {
FILE: packages/umbreld/source/modules/files/watcher.ts
type FileChangeEvent (line 8) | type FileChangeEvent = watcher.Event
class Watcher (line 10) | class Watcher {
method constructor (line 17) | constructor(umbreld: Umbreld, {paths}: {paths: string[]}) {
method start (line 25) | async start() {
method watch (line 49) | async watch(virtualPath: string) {
method stop (line 66) | async stop() {
FILE: packages/umbreld/source/modules/hardware/hardware.ts
class Hardware (line 7) | class Hardware {
method constructor (line 14) | constructor(umbreld: Umbreld) {
method start (line 24) | async start() {
method stop (line 35) | async stop() {
FILE: packages/umbreld/source/modules/hardware/internal-storage.ts
function kelvinToCelsius (line 9) | function kelvinToCelsius(kelvin: number): number {
type NvmeDevice (line 13) | type NvmeDevice = {
type NvmeSmartData (line 30) | type NvmeSmartData = {
function getNvmeSmartData (line 39) | async function getNvmeSmartData(devicePath: string): Promise<NvmeSmartDa...
function getDeviceId (line 90) | async function getDeviceId(deviceName: string): Promise<string | undefin...
function getDevicePciSlotNumber (line 141) | async function getDevicePciSlotNumber(deviceName: string): Promise<numbe...
function getNvmeDevices (line 169) | async function getNvmeDevices(): Promise<NvmeDevice[]> {
class InternalStorage (line 218) | class InternalStorage {
method constructor (line 222) | constructor(umbreld: Umbreld) {
method start (line 228) | async start() {
method stop (line 232) | async stop() {
method getDevices (line 237) | async getDevices(): Promise<NvmeDevice[]> {
FILE: packages/umbreld/source/modules/hardware/raid.ts
function getDeviceSize (line 15) | async function getDeviceSize(device: string): Promise<number> {
function getRoundedDeviceSize (line 22) | function getRoundedDeviceSize(sizeInBytes: number): number {
type RaidType (line 31) | type RaidType = 'storage' | 'failsafe'
type ExpansionStatus (line 33) | type ExpansionStatus = {
type FailsafeTransitionStatus (line 38) | type FailsafeTransitionStatus = {
type RebuildStatus (line 44) | type RebuildStatus = {
type ReplaceStatus (line 49) | type ReplaceStatus = {
type State (line 55) | type State = 'ONLINE' | 'DEGRADED' | 'FAULTED' | 'OFFLINE' | 'UNAVAIL' |...
type Vdev (line 56) | type Vdev = {
type ScanStats (line 74) | type ScanStats = {
type RaidzExpandStats (line 91) | type RaidzExpandStats = {
type Pool (line 101) | type Pool = {
type ZpoolStatusOutput (line 117) | type ZpoolStatusOutput = {
type ConfigStore (line 126) | type ConfigStore = {
class Raid (line 141) | class Raid {
method constructor (line 159) | constructor(umbreld: Umbreld) {
method hasConfigStore (line 189) | async hasConfigStore() {
method generatePoolName (line 195) | generatePoolName(): string {
method start (line 200) | async start() {
method stop (line 225) | async stop() {
method #startPoolMonitor (line 230) | #startPoolMonitor() {
method getStatus (line 263) | async getStatus() {
method getPoolStatus (line 281) | async getPoolStatus(poolName: string): Promise<{
method triggerInitialRaidSetupBootFlow (line 468) | async triggerInitialRaidSetupBootFlow(
method handlePostBootRaidSetupProcess (line 487) | async handlePostBootRaidSetupProcess() {
method checkInitialRaidSetupStatus (line 510) | async checkInitialRaidSetupStatus(): Promise<boolean> {
method checkRaidMountFailure (line 530) | async checkRaidMountFailure(): Promise<boolean> {
method checkRaidMountFailureDevices (line 535) | async checkRaidMountFailureDevices(): Promise<Array<{name: string; isO...
method #partitionDevice (line 547) | async #partitionDevice(device: string): Promise<{statePartition: strin...
method #createPool (line 611) | async #createPool(poolName: string, dataPartitions: string[], raidType...
method #createDataset (line 625) | async #createDataset(poolName: string): Promise<void> {
method setup (line 654) | async setup(deviceIds: string[], raidType: RaidType): Promise<boolean> {
method addDevice (line 693) | async addDevice(deviceId: string): Promise<boolean> {
method replaceDevice (line 735) | async replaceDevice(oldDeviceId: string, newDeviceId: string): Promise...
method transitionToFailsafe (line 841) | async transitionToFailsafe(newDeviceId: string): Promise<boolean> {
method #completeFailsafeTransition (line 1005) | async #completeFailsafeTransition(): Promise<void> {
FILE: packages/umbreld/source/modules/hardware/umbrel-pro.ts
constant EC_STATUS_COMMAND_PORT_ADDRESS (line 26) | const EC_STATUS_COMMAND_PORT_ADDRESS = 0x66
constant EC_DATA_PORT_ADDRESS (line 27) | const EC_DATA_PORT_ADDRESS = 0x62
constant EC_INPUT_BUFFER_FULL_VALUE (line 30) | const EC_INPUT_BUFFER_FULL_VALUE = 0x02
function readPort (line 33) | async function readPort(port: number): Promise<number> {
function writePort (line 45) | async function writePort(port: number, value: number): Promise<void> {
function waitForEcReady (line 56) | async function waitForEcReady(): Promise<void> {
function writeEcRegister (line 66) | async function writeEcRegister(register: number, value: number): Promise...
function readEcRegister (line 77) | async function readEcRegister(register: number): Promise<number> {
class UmbrelPro (line 87) | class UmbrelPro {
method constructor (line 94) | constructor(umbreld: Umbreld) {
method isUmbrelPro (line 101) | async isUmbrelPro(): Promise<boolean> {
method start (line 106) | async start() {
method stop (line 125) | async stop() {
method #writeEcRegister (line 133) | async #writeEcRegister(register: number, value: number): Promise<void> {
method #readEcRegister (line 139) | async #readEcRegister(register: number): Promise<number> {
method #manageFanSpeed (line 171) | async #manageFanSpeed(): Promise<void> {
method setMinFanSpeed (line 221) | async setMinFanSpeed(percent: number): Promise<void> {
method setFanManagementEnabled (line 237) | setFanManagementEnabled(enabled: boolean) {
method setLedOff (line 257) | async setLedOff(): Promise<void> {
method setLedStatic (line 266) | async setLedStatic(): Promise<void> {
method setLedColor (line 272) | async setLedColor({red, green, blue}: {red: number; green: number; blu...
method setLedWhite (line 288) | async setLedWhite(): Promise<void> {
method setLedDefault (line 295) | async setLedDefault(): Promise<void> {
method setLedBlinking (line 301) | async setLedBlinking(): Promise<void> {
method setLedBreathe (line 308) | async setLedBreathe(duration: number = 14): Promise<void> {
method wasBootedViaResetButton (line 328) | async wasBootedViaResetButton(): Promise<boolean> {
method clearResetBootFlag (line 334) | async clearResetBootFlag(): Promise<void> {
method getSsdSlotFromPciSlotNumber (line 352) | getSsdSlotFromPciSlotNumber(pciSlotNumber: number | undefined): number...
FILE: packages/umbreld/source/modules/is-umbrel-home.ts
function isUmbrelHome (line 4) | async function isUmbrelHome() {
FILE: packages/umbreld/source/modules/jwt.ts
constant ONE_MINUTE (line 3) | const ONE_MINUTE = 60
constant ONE_HOUR (line 4) | const ONE_HOUR = 60 * ONE_MINUTE
constant ONE_DAY (line 5) | const ONE_DAY = 24 * ONE_HOUR
constant ONE_WEEK (line 6) | const ONE_WEEK = 7 * ONE_DAY
constant JWT_ALGORITHM (line 8) | const JWT_ALGORITHM = 'HS256'
type jwtPayload (line 10) | type jwtPayload = {
function sign (line 23) | async function sign(secret: string) {
function verify (line 31) | async function verify(token: string, secret: string) {
function signProxyToken (line 47) | async function signProxyToken(secret: string) {
function verifyProxyToken (line 55) | async function verifyProxyToken(token: string, secret: string) {
FILE: packages/umbreld/source/modules/migration/migration.ts
function updateMigrationStatus (line 27) | function updateMigrationStatus(properties: Partial<ProgressStatus>) {
function getMigrationStatus (line 33) | function getMigrationStatus() {
function bytesToGB (line 38) | function bytesToGB(bytes: number) {
function getDirectorySize (line 43) | async function getDirectorySize(directoryPath: string) {
function findExternalUmbrelInstall (line 65) | async function findExternalUmbrelInstall() {
function unmountExternalDrives (line 107) | async function unmountExternalDrives() {
function runPreMigrationChecks (line 131) | async function runPreMigrationChecks(
function migrateData (line 190) | async function migrateData(currentInstall: string, externalUmbrelInstall...
FILE: packages/umbreld/source/modules/notifications/notifications.ts
class Notifications (line 3) | class Notifications {
method constructor (line 7) | constructor(umbreld: Umbreld) {
method get (line 14) | async get() {
method add (line 18) | async add(notification: string) {
method clear (line 38) | async clear(notification: string) {
FILE: packages/umbreld/source/modules/server/index.ts
type ServerOptions (line 27) | type ServerOptions = {umbreld: Umbreld}
type ApiOptions (line 29) | type ApiOptions = {
class Server (line 56) | class Server {
method constructor (line 64) | constructor({umbreld}: ServerOptions) {
method getJwtSecret (line 70) | async getJwtSecret() {
method signToken (line 75) | async signToken() {
method signProxyToken (line 79) | async signProxyToken() {
method verifyToken (line 83) | async verifyToken(token: string) {
method verifyProxyToken (line 87) | async verifyProxyToken(token: string) {
method mountWebSocketServer (line 93) | mountWebSocketServer(path: string, setupHandler: (wss: WebSocketServer...
method start (line 104) | async start() {
FILE: packages/umbreld/source/modules/server/terminal-socket.ts
constant DEFAULT_SHELL_CONTAINERS (line 10) | const DEFAULT_SHELL_CONTAINERS: Record<string, string> = {
function createTerminalWebSocketHandler (line 22) | function createTerminalWebSocketHandler({
FILE: packages/umbreld/source/modules/server/trpc/context.ts
type Simplify (line 40) | type Simplify<T> = {[K in keyof T]: T[K]}
type Merge (line 47) | type Merge<A, B> = Simplify<
type ContextWss (line 56) | type ContextWss = ReturnType<typeof createContextWss>
type ContextExpress (line 57) | type ContextExpress = ReturnType<typeof createContextExpress>
type Context (line 58) | type Context = Merge<ContextWss, ContextExpress>
FILE: packages/umbreld/source/modules/server/trpc/index.ts
type AppRouter (line 36) | type AppRouter = typeof appRouter
method onError (line 41) | onError({error, ctx}) {
method onError (line 59) | onError({error, ctx, path}) {
FILE: packages/umbreld/source/modules/server/trpc/is-authenticated.ts
type MiddlewareOptions (line 5) | type MiddlewareOptions = {
FILE: packages/umbreld/source/modules/server/trpc/trpc.ts
method errorFormatter (line 11) | errorFormatter(options) {
FILE: packages/umbreld/source/modules/server/trpc/websocket-logger.ts
type MiddlewareOptions (line 5) | type MiddlewareOptions = {
FILE: packages/umbreld/source/modules/startup-migrations/index.ts
function readYaml (line 10) | async function readYaml(path: string) {
function writeYaml (line 14) | async function writeYaml(path: string, data: any) {
class Migration (line 18) | class Migration {
method constructor (line 22) | constructor(umbreld: Umbreld) {
method finalizeMenderToRugixStateMigration (line 32) | async finalizeMenderToRugixStateMigration() {
method migrateLegacyLinuxData (line 61) | async migrateLegacyLinuxData() {
method activateImportedDataDirectory (line 89) | async activateImportedDataDirectory() {
method migrateLegacyData (line 105) | async migrateLegacyData() {
method migrateBackThatMacUpPort (line 149) | async migrateBackThatMacUpPort() {
method migrateDownloadsDirectory (line 171) | async migrateDownloadsDirectory() {
method start (line 184) | async start() {
FILE: packages/umbreld/source/modules/startup-migrations/startup-migrations.integration.test.ts
function readYaml (line 7) | async function readYaml(path: string) {
FILE: packages/umbreld/source/modules/system/factory-reset.ts
constant BACKUP_PREFIX (line 8) | const BACKUP_PREFIX = 'umbrel-factory-reset'
function performReset (line 14) | async function performReset() {
function cleanupFactoryResetBackups (line 20) | async function cleanupFactoryResetBackups(umbreld: Umbreld) {
FILE: packages/umbreld/source/modules/system/routes.ts
type SystemStatus (line 28) | type SystemStatus = 'running' | 'updating' | 'shutting-down' | 'restarti...
function setSystemStatus (line 32) | function setSystemStatus(status: SystemStatus) {
FILE: packages/umbreld/source/modules/system/system.integration.test.ts
method error (line 9) | error() {}
FILE: packages/umbreld/source/modules/system/system.ts
function getCpuTemperature (line 14) | async function getCpuTemperature(): Promise<{
type DiskUsage (line 40) | type DiskUsage = {
function getDiskUsageByPath (line 45) | async function getDiskUsageByPath(path: string): Promise<{size: number; ...
function getSystemDiskUsage (line 55) | async function getSystemDiskUsage(
function getDiskUsage (line 72) | async function getDiskUsage(
function getProcessesMemory (line 108) | async function getProcessesMemory() {
type MemoryUsage (line 129) | type MemoryUsage = {
function getSystemMemoryUsage (line 134) | async function getSystemMemoryUsage(): Promise<{
function getMemoryUsage (line 153) | async function getMemoryUsage(umbreld: Umbreld): Promise<{
function getProcessesCpu (line 197) | async function getProcessesCpu() {
type CpuUsage (line 224) | type CpuUsage = {
function getCpuUsage (line 229) | async function getCpuUsage(umbreld: Umbreld): Promise<{
function shutdown (line 281) | async function shutdown(): Promise<boolean> {
function reboot (line 287) | async function reboot(): Promise<boolean> {
function commitOsPartition (line 293) | async function commitOsPartition(umbreld: Umbreld): Promise<boolean> {
function detectDevice (line 305) | async function detectDevice() {
function isRaspberryPi (line 356) | async function isRaspberryPi() {
function isUmbrelOS (line 361) | async function isUmbrelOS() {
function setCpuGovernor (line 365) | async function setCpuGovernor(governor: string) {
function setupPiCpuGovernor (line 369) | async function setupPiCpuGovernor(umbreld: Umbreld): Promise<void> {
function hasWifi (line 380) | async function hasWifi() {
function getWifiNetworks (line 387) | async function getWifiNetworks() {
function deleteWifiConnections (line 419) | async function deleteWifiConnections({inactiveOnly = false}: {inactiveOn...
function connectToWiFiNetwork (line 430) | async function connectToWiFiNetwork({ssid, password}: {ssid: string; pas...
function restoreWiFi (line 468) | async function restoreWiFi(umbreld: Umbreld): Promise<void> {
function getIpAddresses (line 486) | function getIpAddresses(): string[] {
function syncDns (line 530) | async function syncDns() {
function waitForSystemTime (line 547) | async function waitForSystemTime(umbreld: Umbreld, timeout: number): Pro...
function getHostname (line 571) | async function getHostname() {
FILE: packages/umbreld/source/modules/system/update.ts
type UpdateStatus (line 6) | type UpdateStatus = ProgressStatus
function resetUpdateStatus (line 11) | function resetUpdateStatus() {
function setUpdateStatus (line 20) | function setUpdateStatus(properties: Partial<UpdateStatus>) {
function getUpdateStatus (line 24) | function getUpdateStatus() {
function getLatestRelease (line 28) | async function getLatestRelease(umbreld: Umbreld) {
function performUpdate (line 71) | async function performUpdate(umbreld: Umbreld) {
FILE: packages/umbreld/source/modules/test-utilities/create-test-umbreld.ts
function createTestHelpers (line 30) | function createTestHelpers(port: number) {
function createTestUmbreld (line 145) | async function createTestUmbreld({autoLogin = false, autoStart = true} =...
function createTestVm (line 200) | async function createTestVm() {
FILE: packages/umbreld/source/modules/test-utilities/run-git-server.ts
function runGitServer (line 13) | async function runGitServer() {
FILE: packages/umbreld/source/modules/user/routes.ts
constant ONE_SECOND (line 8) | const ONE_SECOND = 1000
constant ONE_MINUTE (line 9) | const ONE_MINUTE = 60 * ONE_SECOND
constant ONE_HOUR (line 10) | const ONE_HOUR = 60 * ONE_MINUTE
constant ONE_DAY (line 11) | const ONE_DAY = 24 * ONE_HOUR
constant ONE_WEEK (line 12) | const ONE_WEEK = 7 * ONE_DAY
function getDefaultWallpaper (line 16) | async function getDefaultWallpaper(): Promise<string> {
FILE: packages/umbreld/source/modules/user/user.ts
class User (line 9) | class User {
method constructor (line 13) | constructor(umbreld: Umbreld) {
method start (line 20) | async start() {
method stop (line 24) | async stop() {
method get (line 29) | async get() {
method exists (line 34) | async exists() {
method setName (line 40) | async setName(name: string) {
method setWallpaper (line 48) | async setWallpaper(wallpaper: string) {
method setPassword (line 53) | async setPassword(password: string) {
method syncSystemPassword (line 70) | async syncSystemPassword() {
method setHashedPassword (line 103) | async setHashedPassword(hashedPassword: string) {
method register (line 111) | async register(name: string, password: string, language: string) {
method validatePassword (line 126) | async validatePassword(password: string) {
method is2faEnabled (line 137) | async is2faEnabled() {
method validate2faToken (line 142) | async validate2faToken(token: string) {
method enable2fa (line 148) | async enable2fa(totpUri: string) {
method disable2fa (line 153) | async disable2fa() {
method setLanguage (line 158) | async setLanguage(language: string) {
method setTemperatureUnit (line 166) | async setTemperatureUnit(temperatureUnit: string) {
FILE: packages/umbreld/source/modules/utilities/copy-with-progress.ts
function copyWithProgress (line 4) | async function copyWithProgress(
FILE: packages/umbreld/source/modules/utilities/docker-pull.ts
constant DOWNLOADING_PERCENT (line 5) | const DOWNLOADING_PERCENT = 0.75
constant EXTRACTING_PERCENT (line 6) | const EXTRACTING_PERCENT = 0.25
function pull (line 8) | async function pull(
function pullAll (line 55) | async function pullAll(images: string[], updateProgress: (progress: numb...
FILE: packages/umbreld/source/modules/utilities/file-store.integration.test.ts
type LooseSchema (line 18) | type LooseSchema = Record<string, any>
type LooseSchema (line 180) | type LooseSchema = Record<string, any>
type LooseSchema (line 199) | type LooseSchema = Record<string, any>
type LooseSchema (line 216) | type LooseSchema = Record<string, any>
type LooseSchema (line 232) | type LooseSchema = Record<string, any>
type LooseSchema (line 253) | type LooseSchema = Record<string, any>
type LooseSchema (line 275) | type LooseSchema = Record<string, any>
FILE: packages/umbreld/source/modules/utilities/file-store.ts
type DotProp (line 8) | type DotProp<T, P extends string> = P extends `${infer K}.${infer R}`
type StorePath (line 16) | type StorePath<T, P extends string> = DotProp<T, P> extends never ? 'The...
type Primitive (line 18) | type Primitive = number | string | boolean | null | undefined
type Serializable (line 19) | type Serializable = {
class FileStore (line 23) | class FileStore<T extends Serializable> {
method constructor (line 32) | constructor({
method #read (line 54) | async #read() {
method #write (line 74) | async #write(store: T): Promise<boolean> {
method #set (line 94) | async #set<P extends string>(property: StorePath<T, P>, value: DotProp...
method #delete (line 101) | async #delete<P extends string>(property: StorePath<T, P>): Promise<bo...
method get (line 108) | async get<P extends string>(property?: StorePath<T, P>, defaultValue?:...
method set (line 114) | async set<P extends string>(property: StorePath<T, P>, value: DotProp<...
method delete (line 123) | async delete<P extends string>(property: StorePath<T, P>): Promise<boo...
method getWriteLock (line 130) | async getWriteLock(
FILE: packages/umbreld/source/modules/utilities/get-directory-size.ts
function getDirectorySize (line 4) | async function getDirectorySize(directoryPath: string) {
FILE: packages/umbreld/source/modules/utilities/get-or-create-file.ts
function getOrCreateFile (line 3) | async function getOrCreateFile(filePath: string, defaultValue: string) {
FILE: packages/umbreld/source/modules/utilities/logger.ts
type LogLevel (line 5) | type LogLevel = (typeof logLevels)[number]
function value (line 7) | function value(logLevel: LogLevel) {
type LogOptions (line 13) | type LogOptions = {
function createLogger (line 18) | function createLogger(scope: string, globalLogLevel: LogLevel = 'normal') {
FILE: packages/umbreld/source/modules/utilities/package-directory.ts
function findPackageDirectory (line 7) | async function findPackageDirectory(startPath: string): Promise<string> {
FILE: packages/umbreld/source/modules/utilities/random-token.ts
function randomToken (line 3) | function randomToken(bitLength: number) {
FILE: packages/umbreld/source/modules/utilities/regexp.ts
function escapeSpecialRegExpLiterals (line 2) | function escapeSpecialRegExpLiterals(string: string) {
FILE: packages/umbreld/source/modules/utilities/run-every.ts
function runEvery (line 6) | function runEvery(interval: string, job: () => Promise<void>, options?: ...
FILE: packages/umbreld/source/modules/utilities/temporary-directory.ts
function temporaryDirectory (line 8) | function temporaryDirectory({parentDirectory}: {parentDirectory?: string...
FILE: packages/umbreld/source/modules/utilities/totp.ts
function generateUri (line 8) | function generateUri(label: string, issuer: string) {
function verify (line 16) | function verify(uri: string, token: string) {
function generateToken (line 26) | function generateToken(uri: string) {
FILE: packages/umbreld/source/modules/widgets/routes.ts
constant MAX_ALLOWED_WIDGETS (line 8) | const MAX_ALLOWED_WIDGETS = 3
function splitWidgetId (line 14) | function splitWidgetId(widgetId: string) {
Condensed preview — 901 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,517K chars).
[
{
"path": ".gitattributes",
"chars": 370,
"preview": "# On Windows, Git defaults to checkout Windows-style and commit Unix-style.\r\n# As such, line endings of bash scripts are"
},
{
"path": ".github/workflows/ci.yml",
"chars": 13092,
"preview": "name: CI\n\non: push\n\njobs:\n # Detect if only UI files changed to skip heavy tests\n detect-changes:\n name: Detect cha"
},
{
"path": ".github/workflows/update-translations-in-pr.yml",
"chars": 1788,
"preview": "name: Update Translations in PR\n\non:\n pull_request:\n types: [opened, synchronize, reopened]\n paths:\n - 'pack"
},
{
"path": ".gitignore",
"chars": 669,
"preview": "# Ignore node_modules anywhere they may be\n\nnode_modules\n\n# Ignore all the bash stuff\n\n.bash_history\n.bash_logout\n.bashr"
},
{
"path": ".prettierrc.js",
"chars": 223,
"preview": "/**\n * @type {import('prettier').Config}\n */\nexport default {\n \"printWidth\": 120,\n \"semi\": false,\n \"useTabs\": true,\n "
},
{
"path": ".umbrel",
"chars": 0,
"preview": ""
},
{
"path": "CONTRIBUTING.md",
"chars": 3934,
"preview": "Contributing to Umbrel\n======================\n\nUmbrel is an open project and we love to receive contributions from our c"
},
{
"path": "LICENSE.md",
"chars": 4798,
"preview": "> Umbrel is licensed under the PolyForm Noncommercial License 1.0.0. Please refer to our [License FAQ](https://github.co"
},
{
"path": "README.md",
"chars": 4557,
"preview": "[](https://umbrel.com/umbrel"
},
{
"path": "containers/app-auth/.dockerignore",
"chars": 52,
"preview": "Dockerfile\nnode_modules\n.git\n.github\ntest\ndist\n*.log"
},
{
"path": "containers/app-auth/.gitignore",
"chars": 230,
"preview": ".DS_Store\nnode_modules\n/dist\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files"
},
{
"path": "containers/app-auth/Dockerfile",
"chars": 1137,
"preview": "# UI build stage\nFROM node:18.19.1-buster-slim AS umbrel-app-auth-ui-builder\n\n# Set the working directory\nWORKDIR /app\n\n"
},
{
"path": "containers/app-auth/README.md",
"chars": 1666,
"preview": "[](https://github.com/getumbrel"
},
{
"path": "containers/app-auth/bin/www",
"chars": 691,
"preview": "#!/usr/bin/env node\n\nconst cookieParser = require(\"cookie-parser\");\nconst express = require('express');\nconst { StatusCo"
},
{
"path": "containers/app-auth/middleware/handle_error.js",
"chars": 230,
"preview": "function handleError(error, req, res, next) {\n var statusCode = error.statusCode || 500;\n var route = req.url || '';\n "
},
{
"path": "containers/app-auth/middleware/validate_token.js",
"chars": 1647,
"preview": "const url = require('url');\n\nconst hmacUtils = require('../utils/hmac.js');\nconst tokenUtils = require('../utils/token.j"
},
{
"path": "containers/app-auth/package.json",
"chars": 1688,
"preview": "{\n \"name\": \"app-auth\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"scripts\": {\n \"lint\": \"eslint\",\n \"start\": \"node"
},
{
"path": "containers/app-auth/routes/auth.js",
"chars": 3262,
"preview": "const express = require(\"express\");\nconst axios = require(\"axios\");\nconst { StatusCodes } = require(\"http-status-codes\")"
},
{
"path": "containers/app-auth/test/docker-compose.yml",
"chars": 581,
"preview": "version: '3.7'\n\nservices:\n auth:\n image: getumbrel/app-auth1\n user: \"1000:1000\"\n build:\n "
},
{
"path": "containers/app-auth/test/fixtures/app-data/mempool/umbrel-app.yml",
"chars": 950,
"preview": "id: mempool\ncategory: Explorers\nname: mempool\nversion: 2.3.1\ntagline: A self-hosted explorer for the Bitcoin community\nd"
},
{
"path": "containers/app-auth/test/global.js",
"chars": 237,
"preview": "const chai = require('chai');\nconst chaiHttp = require('chai-http');\n\nchai.use(chaiHttp);\nchai.should();\n\nglobal.expect "
},
{
"path": "containers/app-auth/test/test.sh",
"chars": 62,
"preview": "#!/bin/bash\n\nexport MANAGER_IP=\"10.21.21.4\"\n\ndocker-compose up"
},
{
"path": "containers/app-auth/test/utils/hmac.js",
"chars": 921,
"preview": "const hmac = require(\"../../utils/hmac.js\");\n\ndescribe('hmac', () => {\n it('should sign the message', () => {\n asser"
},
{
"path": "containers/app-auth/utils/app.js",
"chars": 690,
"preview": "const fs = require('fs').promises;\nconst path = require('path');\nconst yaml = require('js-yaml');\n\nconst CONSTANTS = req"
},
{
"path": "containers/app-auth/utils/const.js",
"chars": 837,
"preview": "function readFromEnvOrTerminate(key) {\n\tconst value = process.env[key];\n\n\tif(typeof(value) !== \"string\" || value.trim()."
},
{
"path": "containers/app-auth/utils/dashboard.js",
"chars": 522,
"preview": "const axios = require('axios');\nconst package = require('../package.json');\n\nconst CONSTANTS = require('./const.js');\n\nc"
},
{
"path": "containers/app-auth/utils/express.js",
"chars": 229,
"preview": "function getQueryParam(req, key) {\n\tconst value = req.query[key];\n\n\tif(typeof(value) !== \"string\" || value.trim().length"
},
{
"path": "containers/app-auth/utils/hmac.js",
"chars": 461,
"preview": "const crypto = require('crypto');\n\nfunction sign(input, secret) {\n\treturn crypto\n\t\t.createHmac('sha256', secret)\n\t\t.upda"
},
{
"path": "containers/app-auth/utils/host_resolution.js",
"chars": 964,
"preview": "const fs = require('fs').promises;\nconst path = require('path');\nconst yaml = require('js-yaml');\n\nconst CONSTANTS = req"
},
{
"path": "containers/app-auth/utils/manager.js",
"chars": 671,
"preview": "const axios = require('axios');\nconst package = require('../package.json');\n\nconst CONSTANTS = require('./const.js');\n\nc"
},
{
"path": "containers/app-auth/utils/safe_handler.js",
"chars": 426,
"preview": "// this safe handler is used to wrap our api methods\n// so that we always fallback and return an exception if there is a"
},
{
"path": "containers/app-auth/utils/token.js",
"chars": 410,
"preview": "const jwt = require(\"jsonwebtoken\");\n\nconst JWT_ALGORITHM = \"HS256\";\n\nconst secret = process.env.JWT_SECRET;\n\nfunction v"
},
{
"path": "containers/app-auth/views/pages/redirect.ejs",
"chars": 385,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n\t<meta charset=\"utf-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale="
},
{
"path": "containers/app-proxy/.dockerignore",
"chars": 41,
"preview": "Dockerfile\nnode_modules\n.git\n.github\ntest"
},
{
"path": "containers/app-proxy/.gitignore",
"chars": 230,
"preview": ".DS_Store\nnode_modules\n/dist\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files"
},
{
"path": "containers/app-proxy/Dockerfile",
"chars": 549,
"preview": "# Build Stage\nFROM node:16-buster-slim AS umbrel-app-proxy-builder\n\n# Create app directory\nWORKDIR /app\n\n# Copy 'yarn.lo"
},
{
"path": "containers/app-proxy/Dockerfile.dev",
"chars": 146,
"preview": "FROM node:16-buster-slim\n\n# make the 'app' folder the current working directory\nWORKDIR /app\n\nENTRYPOINT [\"bash\"]\nCMD [\""
},
{
"path": "containers/app-proxy/README.md",
"chars": 3969,
"preview": "[](https://github.com/getumbr"
},
{
"path": "containers/app-proxy/bin/www",
"chars": 1548,
"preview": "#!/usr/bin/env node\n\nconst cookieParser = require('cookie-parser');\nconst express = require('express');\nconst waitPort ="
},
{
"path": "containers/app-proxy/middleware/handle_error.js",
"chars": 230,
"preview": "function handleError(error, req, res, next) {\n var statusCode = error.statusCode || 500;\n var route = req.url || '';\n "
},
{
"path": "containers/app-proxy/package.json",
"chars": 932,
"preview": "{\n \"name\": \"app-proxy\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"scripts\": {\n \"lint\": \"eslint\",\n \"start\": \"nod"
},
{
"path": "containers/app-proxy/routes/umbrel.js",
"chars": 1869,
"preview": "const { StatusCodes } = require(\"http-status-codes\");\nconst bodyParser = require(\"body-parser\");\nconst express = require"
},
{
"path": "containers/app-proxy/test/.gitignore",
"chars": 5,
"preview": "apps/"
},
{
"path": "containers/app-proxy/test/docker-compose.app1.yml",
"chars": 258,
"preview": "version: '3.7'\n\nservices:\n app_proxy:\n environment:\n APP_HOST: nginxdemo\n APP_PORT: 80\n "
},
{
"path": "containers/app-proxy/test/docker-compose.app2.yml",
"chars": 221,
"preview": "version: '3.7'\n\nservices:\n app_proxy:\n environment:\n APP_HOST: frontend\n APP_PORT: 8888\n"
},
{
"path": "containers/app-proxy/test/docker-compose.bleskomat.yml",
"chars": 1780,
"preview": "version: \"3.7\"\n\nservices:\n app_proxy:\n environment:\n APP_HOST: bleskomat_web\n APP_PORT: 3333\n PROXY_A"
},
{
"path": "containers/app-proxy/test/docker-compose.error.yml",
"chars": 197,
"preview": "version: '3.7'\n\nservices:\n app_proxy:\n environment:\n APP_HOST: app_wrong\n APP_PORT: 80\n "
},
{
"path": "containers/app-proxy/test/docker-compose.mempool.yml",
"chars": 1585,
"preview": "version: \"3.7\"\n\nservices:\n app_proxy:\n environment:\n APP_HOST: web_mempool\n APP_PORT: 3006\n PROXY_AUT"
},
{
"path": "containers/app-proxy/test/docker-compose.nextcloud.yml",
"chars": 2038,
"preview": "version: \"3.7\"\n\nservices:\n app_proxy:\n environment:\n - APP_HOST=web\n - APP_PORT=80\n\n db:\n image: maria"
},
{
"path": "containers/app-proxy/test/docker-compose.proxy.yml",
"chars": 455,
"preview": "version: '3.7'\n\nservices:\n caddy:\n image: caddy:2.5.1\n command: caddy reverse-proxy --from :4007 --to a"
},
{
"path": "containers/app-proxy/test/docker-compose.proxyhttps.yaml",
"chars": 462,
"preview": "version: '3.7'\n\nservices:\n caddy:\n image: caddy:2.5.1\n volumes:\n - \"./test/Caddyfile-https:/"
},
{
"path": "containers/app-proxy/test/docker-compose.sse.yml",
"chars": 247,
"preview": "version: '3.7'\n\nservices:\n app_proxy:\n environment:\n APP_HOST: sse_server\n APP_PORT: 80\n"
},
{
"path": "containers/app-proxy/test/docker-compose.suredbits.yml",
"chars": 2121,
"preview": "version: \"3.7\"\n\nservices:\n app_proxy:\n environment:\n APP_HOST: web\n APP_PORT: 3002\n\n web:\n image: bitc"
},
{
"path": "containers/app-proxy/test/docker-compose.ws.yml",
"chars": 174,
"preview": "version: '3.7'\n\nservices:\n app_proxy:\n environment:\n APP_HOST: ws_server\n APP_PORT: 8010"
},
{
"path": "containers/app-proxy/test/docker-compose.yml",
"chars": 1098,
"preview": "version: '3.7'\n\nservices:\n app_proxy:\n image: getumbrel/app-proxy\n build:\n context: ..\n "
},
{
"path": "containers/app-proxy/test/fixtures/mempool-umbrel-app.yml",
"chars": 950,
"preview": "id: mempool\ncategory: Explorers\nname: mempool\nversion: 2.3.1\ntagline: A self-hosted explorer for the Bitcoin community\nd"
},
{
"path": "containers/app-proxy/test/global.js",
"chars": 237,
"preview": "const chai = require('chai');\nconst chaiHttp = require('chai-http');\n\nchai.use(chaiHttp);\nchai.should();\n\nglobal.expect "
},
{
"path": "containers/app-proxy/test/sse-test-server/.dockerignore",
"chars": 37,
"preview": "Dockerfile\nnode_modules\n.git\n.github\n"
},
{
"path": "containers/app-proxy/test/sse-test-server/Dockerfile",
"chars": 567,
"preview": "# Build Stage\nFROM node:16-buster-slim AS umbrel-sse-test-server-builder\n\n# Create app directory\nWORKDIR /app\n\n# Copy 'y"
},
{
"path": "containers/app-proxy/test/sse-test-server/bin/www",
"chars": 1607,
"preview": "#!/usr/bin/env node\n\nconst express = require('express');\n\nconst PORT = process.env.PORT || 80;\n\nconst app = express();\n\n"
},
{
"path": "containers/app-proxy/test/sse-test-server/package.json",
"chars": 180,
"preview": "{\n \"name\": \"umbrel-sse-test-server\",\n \"version\": \"0.0.1\",\n \"private\": true,\n \"scripts\": {\n \"start\": \"node ./bin/w"
},
{
"path": "containers/app-proxy/test/test/Caddyfile-https",
"chars": 77,
"preview": "https://umbrel-dev.local:4007 {\n\treverse_proxy app_proxy:4000\n\ttls internal\n}"
},
{
"path": "containers/app-proxy/test/test.sh",
"chars": 1108,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nUMBREL_ENV_FILE=\"$(readlink -f $(dirname \"${BASH_SOURCE[0]}\")/../../../.env)\"\n\nCO"
},
{
"path": "containers/app-proxy/test/utils/express.js",
"chars": 2224,
"preview": "const httpMocks = require('node-mocks-http');\n\nconst express = require(\"../../utils/express.js\");\n\ndescribe('express', ("
},
{
"path": "containers/app-proxy/test/utils/tor.js",
"chars": 218,
"preview": "const tor = require(\"../../utils/tor.js\");\n\ndescribe('tor', () => {\n it('should return the auth HS url', async () => {\n"
},
{
"path": "containers/app-proxy/utils/const.js",
"chars": 2158,
"preview": "const yaml = require('js-yaml');\nconst fs = require('fs');\n\nconst APP_MANIFEST_FILE = process.env.APP_MANIFEST_FILE ||"
},
{
"path": "containers/app-proxy/utils/express.js",
"chars": 528,
"preview": "function removeCookie(req, cookieName) {\n\tconst allCookies = req.headers.cookie || \"\";\n\n\t// Split on '; ' (where space i"
},
{
"path": "containers/app-proxy/utils/hmac.js",
"chars": 461,
"preview": "const crypto = require('crypto');\n\nfunction sign(input, secret) {\n\treturn crypto\n\t\t.createHmac('sha256', secret)\n\t\t.upda"
},
{
"path": "containers/app-proxy/utils/manager.js",
"chars": 578,
"preview": "const axios = require('axios');\nconst package = require('../package.json');\n\nconst CONSTANTS = require('./const.js');\n\nc"
},
{
"path": "containers/app-proxy/utils/proxy.js",
"chars": 5057,
"preview": "const { createProxyMiddleware } = require(\"http-proxy-middleware\");\nconst { StatusCodes } = require(\"http-status-codes\")"
},
{
"path": "containers/app-proxy/utils/safe_handler.js",
"chars": 426,
"preview": "// this safe handler is used to wrap our api methods\n// so that we always fallback and return an exception if there is a"
},
{
"path": "containers/app-proxy/utils/token.js",
"chars": 298,
"preview": "const jwt = require(\"jsonwebtoken\");\n\nconst JWT_ALGORITHM = \"HS256\";\n\nconst secret = process.env.JWT_SECRET;\n\nfunction v"
},
{
"path": "containers/app-proxy/utils/tor.js",
"chars": 405,
"preview": "const fs = require(\"fs\").promises;\n\nconst CONSTANTS = require(\"./const.js\");\n\nasync function authHsUrl() {\n // Here is "
},
{
"path": "containers/app-proxy/views/pages/error.ejs",
"chars": 4270,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-sc"
},
{
"path": "containers/tor/Dockerfile",
"chars": 1411,
"preview": "# Based on https://github.com/lncm/docker-tor/tree/927ebac9fb43ba4d09249ee27688a4612b7a1707\n\nFROM debian:11-slim AS buil"
},
{
"path": "containers/tor/README.md",
"chars": 707,
"preview": "[](https://github.com/getumbrel/umbrel-to"
},
{
"path": "containers/tor/test/.gitignore",
"chars": 4,
"preview": "data"
},
{
"path": "containers/tor/test/docker-compose.entrypoint.yml",
"chars": 502,
"preview": "version: '3.7'\n\nservices:\n web:\n image: mendhak/http-https-echo\n environment:\n HTTP_PORT: 88"
},
{
"path": "containers/tor/test/docker-compose.yml",
"chars": 328,
"preview": "version: '3.7'\n\nservices:\n web:\n image: mendhak/http-https-echo\n environment:\n HTTP_PORT: 88"
},
{
"path": "containers/tor/test/entrypoint.sh",
"chars": 204,
"preview": "#!/bin/bash\n\nTORRC_PATH=\"/tmp/torrc\"\n\necho \"HiddenServiceDir /data/${HS_DIR}\" > \"${TORRC_PATH}\"\necho \"HiddenServicePort "
},
{
"path": "containers/tor/test/test-entrypoint.sh",
"chars": 189,
"preview": "#!/bin/bash\n\ndocker-compose -f docker-compose.entrypoint.yml up --detach web\ndocker-compose -f docker-compose.entrypoint"
},
{
"path": "containers/tor/test/test.sh",
"chars": 121,
"preview": "#!/bin/bash\n\ndocker-compose up --detach web\ndocker-compose up --detach tor\n\necho\necho \"Hostname:\"\ncat ./data/web/hostnam"
},
{
"path": "containers/tor/test/torrc",
"chars": 56,
"preview": "HiddenServiceDir /data/web\nHiddenServicePort 80 web:8888"
},
{
"path": "info.json",
"chars": 1715,
"preview": "{\n \"NOTE\": \"We must keep this file here forever to allow old umbrelOS 0.5.x Umbrel Homes to find the 1.0.0 update and b"
},
{
"path": "package.json",
"chars": 366,
"preview": "{\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"./scripts/umbrel-dev\",\n \"dev:help\": \"npm run dev help\",\n \"test:vm"
},
{
"path": "packages/os/.gitignore",
"chars": 15,
"preview": "build\nvm-state\n"
},
{
"path": "packages/os/README.md",
"chars": 5088,
"preview": "# umbrelOS\n\n\n## Build Process\n\nThe following diagram visualizes the build process and various build artifacts:\n\n```merma"
},
{
"path": "packages/os/build-steps/initialize.sh",
"chars": 1807,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nSNAPSHOT_DATE=${1:-}\n\n# Disable the resume from SWAP functionality. This takes significa"
},
{
"path": "packages/os/build-steps/setup-raspberrypi/cmdline.txt",
"chars": 201,
"preview": "console=serial0,115200 console=tty1 rootfstype=ext4 fsck.repair=yes rootwait cgroup_enable=memory swapaccount=1 loglevel"
},
{
"path": "packages/os/build-steps/setup-raspberrypi/config.txt",
"chars": 672,
"preview": "# Enable DRM VC4 V3D driver.\n#\n# MX: This has been enabled by default and is required for 3D graphics\n# hardware acceler"
},
{
"path": "packages/os/build-steps/setup-raspberrypi/raspberrypi.gpg.key",
"chars": 1718,
"preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1.4.12 (GNU/Linux)\n\nmQENBE/d7o8BCACrwqQacGJfn3tnMzGui6mv2lLxYbsOuy/"
},
{
"path": "packages/os/build-steps/setup-raspberrypi/raspberrypi.list",
"chars": 188,
"preview": "deb http://archive.raspberrypi.com/debian/ RELEASE main\n# Uncomment line below then 'apt-get update' to enable 'apt-get "
},
{
"path": "packages/os/build-steps/setup-raspberrypi.sh",
"chars": 1112,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\n# Install GPG (required for dearmoring the key).\napt-get install -y gpg\n\n\n# Remove any e"
},
{
"path": "packages/os/build.sh",
"chars": 9032,
"preview": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Pin the Rugix Docker image.\nexport RUGIX_BAKERY_IMAGE=\"ghcr.io/silitics/rugix-"
},
{
"path": "packages/os/builder.Dockerfile",
"chars": 496,
"preview": "FROM debian:bullseye\n\nRUN apt-get -y update\n\n# Install os image builder deps\nRUN apt-get -y install fdisk gdisk qemu-uti"
},
{
"path": "packages/os/mender.cfg",
"chars": 3338,
"preview": "DEPLOY_IMAGE_NAME=\"umbrelos\"\nMENDER_DEVICE_TYPE=\"amd64\"\n\n# This gives us:\n# - 200Mb EFI\n# - 2x ~10GB OS partitions\n# - 5"
},
{
"path": "packages/os/overlay-amd64/umbrelOS",
"chars": 0,
"preview": ""
},
{
"path": "packages/os/overlay-arm64/etc/systemd/system/umbrel-external-storage.service",
"chars": 467,
"preview": "# Umbrel External Storage Mounter\n# Installed at /etc/systemd/system/umbrel-external-storage.service\n\n[Unit]\nDescription"
},
{
"path": "packages/os/overlay-arm64/opt/umbrel-external-storage/umbrel-external-storage",
"chars": 8716,
"preview": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# This script will:\n# - Look for external storage devices\n# - Check if they cont"
},
{
"path": "packages/os/overlay-common/etc/NetworkManager/NetworkManager.conf",
"chars": 58,
"preview": "[main]\nplugins=ifupdown,keyfile\n\n[ifupdown]\nmanaged=false\n"
},
{
"path": "packages/os/overlay-common/etc/NetworkManager/conf.d/10-cloudflaredns.conf",
"chars": 440,
"preview": "# This is important, we use Cloudflare for DNS because some users have routers that provide\r\n# unreliable DNS that resul"
},
{
"path": "packages/os/overlay-common/etc/acpi/events/power-button",
"chars": 94,
"preview": "event=button/power.*PBTN\naction=systemd-cat -t umbrel-power-button /etc/acpi/power-button.sh &"
},
{
"path": "packages/os/overlay-common/etc/acpi/power-button.sh",
"chars": 2748,
"preview": "#!/bin/bash\n\n# Configuration\nRECOVERY_SEQUENCE_COUNT=10\nLISTEN_TIME=1\nSTATE_FILE=\"/tmp/power_button_state\"\nPASSWORD_RESE"
},
{
"path": "packages/os/overlay-common/etc/fstab",
"chars": 753,
"preview": "# <device> <dir> <type> <options> <dump> <fsck>\n/ "
},
{
"path": "packages/os/overlay-common/etc/hostname",
"chars": 7,
"preview": "umbrel\n"
},
{
"path": "packages/os/overlay-common/etc/hosts",
"chars": 49,
"preview": "127.0.0.1 umbrel\n127.0.0.1 localhost\n"
},
{
"path": "packages/os/overlay-common/etc/issue",
"chars": 0,
"preview": ""
},
{
"path": "packages/os/overlay-common/etc/locale.conf",
"chars": 61,
"preview": "# Fixes locale warnings when logging in via SSH\nLANG=C.UTF-8\n"
},
{
"path": "packages/os/overlay-common/etc/motd",
"chars": 856,
"preview": "\n\n ,;###GGGGGGGGGGl#Sp\n ,##GGGlW\"\"^' '`\"\"%GGGG#S,\n ,#GGG\" \"lGG#o\n "
},
{
"path": "packages/os/overlay-common/etc/sudoers.d/umbrel",
"chars": 388,
"preview": "# Remove the silly outdated warning from sudo the first time it's used:\n#\n# We trust you have received the usual lecture"
},
{
"path": "packages/os/overlay-common/etc/sudoers.lecture",
"chars": 0,
"preview": ""
},
{
"path": "packages/os/overlay-common/etc/sysctl.d/99-vm-zram-parameters.conf",
"chars": 1562,
"preview": "# Optimise swap for zram\n\n# Our swap is super fast in-memory via zram so we can afford to be more aggressive with swappi"
},
{
"path": "packages/os/overlay-common/etc/systemd/logind.conf.d/lid-switch.conf",
"chars": 502,
"preview": "# Ignore lid switch events to allow users running umbrelOS on laptops to keep the device on when the lid is closed.\n# Th"
},
{
"path": "packages/os/overlay-common/etc/systemd/logind.conf.d/power-button.conf",
"chars": 162,
"preview": "# We want logind to ignore the power button press events.\n# We will register custom acpi event handlers to handle this\n#"
},
{
"path": "packages/os/overlay-common/etc/systemd/system/umbrel-dns-sync.service",
"chars": 232,
"preview": "[Unit]\r\nDescription=Synchronize DNS configuration before starting NetworkManager\r\nBefore=NetworkManager.service\r\n\r\n[Serv"
},
{
"path": "packages/os/overlay-common/etc/systemd/system/umbrel-ssh-host-key-hydration.service",
"chars": 196,
"preview": "[Unit]\nDescription=Hydrate SSH Host Keys\nBefore=ssh.service\n\n[Service]\nType=oneshot\nExecStart=/opt/umbrel-ssh-host-key-h"
},
{
"path": "packages/os/overlay-common/etc/systemd/system/umbrel-tty-message.service",
"chars": 195,
"preview": "[Unit]\nDescription=Display Umbrel access information on TTY\nAfter=umbrel.service\n\n[Service]\nExecStart=/opt/umbrel-tty-me"
},
{
"path": "packages/os/overlay-common/etc/systemd/system/umbrel.service",
"chars": 331,
"preview": "[Unit]\nDescription=Umbrel daemon\nAfter=network-online.target docker.service\n\n[Service]\nTimeoutStopSec=15min\nExecStart=um"
},
{
"path": "packages/os/overlay-common/etc/systemd/timesyncd.conf.d/cloudflare.conf",
"chars": 213,
"preview": "# We default to Cloudflare for NTP because some users have issues\n# connecting to the default Debian ntp pool. If Cloudf"
},
{
"path": "packages/os/overlay-common/etc/systemd/zram-generator.conf",
"chars": 504,
"preview": "# We create a virtual in-memory swap device that is 75% of the RAM and compress it\n# with zstd which generally gives a ~"
},
{
"path": "packages/os/overlay-common/opt/umbrel-data/umbrel-data-mount",
"chars": 5188,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nCONFIG_PARTITION=${CONFIG_PARTITION:-\"/run/rugix/mounts/config\"}\nCONFIG_FILE=\"$CONFIG_PA"
},
{
"path": "packages/os/overlay-common/opt/umbrel-dns-sync/umbrel-dns-sync",
"chars": 874,
"preview": "#!/bin/bash\n\nUMBREL_YAML=/home/umbrel/umbrel/umbrel.yaml\n\nCLOUDFLARE_CONF=/etc/NetworkManager/conf.d/10-cloudflaredns.co"
},
{
"path": "packages/os/overlay-common/opt/umbrel-ssh-host-key-hydration/umbrel-ssh-host-key-hydration",
"chars": 406,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nSSH_STATE_DIR=${SSH_STATE_DIR:-\"/data/ssh\"}\n\nif [ ! -f \"${SSH_STATE_DIR}\"/ssh_host_rsa_k"
},
{
"path": "packages/os/overlay-common/opt/umbrel-tty-message/umbrel-tty-message",
"chars": 969,
"preview": "#!/bin/bash\n\nget_addresses() {\n # Get all active wifi and ethernet interfaces\n local active_interfaces=$(nmcli --t"
},
{
"path": "packages/os/overlay-common/umbrelOS",
"chars": 0,
"preview": ""
},
{
"path": "packages/os/package.json",
"chars": 559,
"preview": "{\n \"scripts\": {\n \"build\": \"./build.sh\",\n \"build:amd64\": \"SKIP_ARM64=true npm run build\",\n \"build:amd64:rugix\":"
},
{
"path": "packages/os/rugix/.gitignore",
"chars": 15,
"preview": "/.rugix\n/build\n"
},
{
"path": "packages/os/rugix/fix-umbrelos-pi-mbr.sh",
"chars": 242,
"preview": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nLAST_USED_SECTOR=$(sfdisk -l /data/build/umbrelos-pi-mbr/system.img -o end | tai"
},
{
"path": "packages/os/rugix/layers/umbrelos-amd64.toml",
"chars": 352,
"preview": "parent = \"umbrelos-root-amd64\"\n\nrecipes = [\n # Prepare umbrelOS base image for Rugix.\n \"umbrelos-prepare\",\n # C"
},
{
"path": "packages/os/rugix/layers/umbrelos-mender-amd64.toml",
"chars": 452,
"preview": "parent = \"umbrelos-root-amd64\"\n\nrecipes = [\n # Prepare umbrelOS base image for Rugix.\n \"umbrelos-prepare\",\n # C"
},
{
"path": "packages/os/rugix/layers/umbrelos-pi.toml",
"chars": 352,
"preview": "parent = \"umbrelos-root-arm64\"\n\nrecipes = [\n # Prepare umbrelOS base image for Rugix.\n \"umbrelos-prepare\",\n # C"
},
{
"path": "packages/os/rugix/layers/umbrelos-pi4.toml",
"chars": 181,
"preview": "parent = \"umbrelos-pi\"\n\nrecipes = [\n # Include the firmware update for Raspberry Pi 4.\n \"core/rpi-include-firmware"
},
{
"path": "packages/os/rugix/layers/umbrelos-root-amd64.toml",
"chars": 58,
"preview": "url=\"file:///build/umbrelos-root/umbrelos-root-amd64.tar\"\n"
},
{
"path": "packages/os/rugix/layers/umbrelos-root-arm64.toml",
"chars": 58,
"preview": "url=\"file:///build/umbrelos-root/umbrelos-root-arm64.tar\"\n"
},
{
"path": "packages/os/rugix/recipes/fix-overlay/files/.gitignore",
"chars": 14,
"preview": "hostname\nhosts"
},
{
"path": "packages/os/rugix/recipes/fix-overlay/files/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "packages/os/rugix/recipes/fix-overlay/recipe.toml",
"chars": 53,
"preview": "description = \"fix `/etc/hostname` and `/etc/hosts`\"\n"
},
{
"path": "packages/os/rugix/recipes/fix-overlay/steps/00-install.sh",
"chars": 136,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\ninstall -m 644 \"${RECIPE_DIR}/files/hostname\" \"/etc/\"\ninstall -m 644 \"${RECIPE_DIR}/file"
},
{
"path": "packages/os/rugix/recipes/setup-rugix/files/bootstrapping-amd64.toml",
"chars": 831,
"preview": "[layout]\ntype = \"gpt\"\npartitions = [\n { name = \"EFI\", size = \"256M\", type = \"C12A7328-F81F-11D2-BA4B-00A0C93EC93B\" },"
},
{
"path": "packages/os/rugix/recipes/setup-rugix/files/bootstrapping-arm64.toml",
"chars": 923,
"preview": "[layout]\ntype = \"gpt\"\npartitions = [\n { name = \"EFI\", size = \"256M\", type = \"C12A7328-F81F-11D2-BA4B-00A0C93EC93B\" },"
},
{
"path": "packages/os/rugix/recipes/setup-rugix/files/hooks/state-reset/prepare.sh",
"chars": 2934,
"preview": "#!/bin/bash\n\n# Rugix `state-reset/prepare` hook to reset the RAID and main disk data partition. By\n# default Rugix reset"
},
{
"path": "packages/os/rugix/recipes/setup-rugix/files/state-data.toml",
"chars": 32,
"preview": "[[persist]]\ndirectory = \"/data\"\n"
},
{
"path": "packages/os/rugix/recipes/setup-rugix/files/system.toml",
"chars": 192,
"preview": "[data-partition]\n# This is safe to enable on all systems as it mounts the default\n# data partition if no external RAID h"
},
{
"path": "packages/os/rugix/recipes/setup-rugix/recipe.toml",
"chars": 77,
"preview": "description = \"setup Rugix for umbrelOS\"\n\ndependencies = [\"core/rugix-ctrl\"]\n"
},
{
"path": "packages/os/rugix/recipes/setup-rugix/steps/00-install.sh",
"chars": 542,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\napt-get install -y fdisk parted\n\ninstall -D -m 644 \\\n \"${RECIPE_DIR}/files/bootstrapp"
},
{
"path": "packages/os/rugix/recipes/setup-rugix-mender/files/init",
"chars": 372,
"preview": "#!/bin/sh\n\nDATA_DIR=\"/run/rugix/mounts/data\"\n\n# If the data directory exists, Rugix Ctrl initialized the system and we c"
},
{
"path": "packages/os/rugix/recipes/setup-rugix-mender/files/migrate-state.sh",
"chars": 1289,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\n# We check whether the old `umbrel-os` directory still exists. If it does not, then the\n"
},
{
"path": "packages/os/rugix/recipes/setup-rugix-mender/files/system.toml",
"chars": 584,
"preview": "#:schema https://raw.githubusercontent.com/silitics/rugix/refs/tags/v0.8.0/schemas/rugix-ctrl-system.schema.json\n\n[data-"
},
{
"path": "packages/os/rugix/recipes/setup-rugix-mender/recipe.toml",
"chars": 126,
"preview": "description = \"configure system for compatibility with legacy Mender systems\"\npriority = -100\n\ndependencies = [\"setup-ru"
},
{
"path": "packages/os/rugix/recipes/setup-rugix-mender/steps/00-install.sh",
"chars": 729,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\n# Install custom Rugix system configuration for Mender-compatibility.\ninstall -D -m 644 "
},
{
"path": "packages/os/rugix/recipes/umbrelos-boot/files/grub.cfg",
"chars": 129,
"preview": "load_env -f /boot.grubenv\nlinux /vmlinuz console=ttyS0 console=tty1 loglevel=3 panic=5 ${rugpi_bootargs}\ninitrd /initrd."
},
{
"path": "packages/os/rugix/recipes/umbrelos-boot/recipe.toml",
"chars": 79,
"preview": "description = \"copy umbrelOS boot files to boot partition\"\npriority = -800_000\n"
},
{
"path": "packages/os/rugix/recipes/umbrelos-boot/steps/00-install.sh",
"chars": 572,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nBOOT_DIR=\"${RUGIX_LAYER_DIR}/roots/boot\"\n\nmkdir -p \"${BOOT_DIR}\"\n\ncase \"${RUGIX_ARCH}\" i"
},
{
"path": "packages/os/rugix/recipes/umbrelos-cleanup/recipe.toml",
"chars": 88,
"preview": "description = \"cleanup and restore original umbrelOS configuration\"\npriority = -800_000\n"
},
{
"path": "packages/os/rugix/recipes/umbrelos-cleanup/steps/00-install.sh",
"chars": 94,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nmv /etc/resolv.conf.original /etc/resolv.conf\nrm -rf /var/log\n"
},
{
"path": "packages/os/rugix/recipes/umbrelos-prepare/recipe.toml",
"chars": 109,
"preview": "description = \"prepare umbrelOS base image for Rugix\"\npriority = 800_000\ndependencies = [\"umbrelos-cleanup\"]\n"
},
{
"path": "packages/os/rugix/recipes/umbrelos-prepare/steps/00-install.sh",
"chars": 327,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nmv /etc/resolv.conf /etc/resolv.conf.original\necho \"nameserver 1.1.1.1\" > /etc/resolv.co"
},
{
"path": "packages/os/rugix/rugix-bakery.toml",
"chars": 3573,
"preview": "#:schema https://raw.githubusercontent.com/silitics/rugix/refs/tags/v0.8.0/schemas/rugix-bakery-project.schema.json\n\n# I"
},
{
"path": "packages/os/rugix/run-bakery",
"chars": 1491,
"preview": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nDOCKER=${DOCKER:-\"docker\"}\nDOCKER_FLAGS=${DOCKER_FLAGS:-\"\"}\n\nRUGIX_DEV=${RUGIX_D"
},
{
"path": "packages/os/rugpi-image",
"chars": 3872,
"preview": "#!/usr/bin/env python3\n#\n# Copyright 2023-2024 Silitics GmbH <info@silitics.com>\n#\n# This file is part of Rugpi (https:/"
},
{
"path": "packages/os/trigger-change",
"chars": 29,
"preview": "Thu 15 Jan 2026 17:12:57 +07\n"
},
{
"path": "packages/os/umbrelos.Dockerfile",
"chars": 7923,
"preview": "ARG DEBIAN_VERSION=trixie\nARG SNAPSHOT_DATE=20251229\n\nARG DOCKER_VERSION=28.5.0\nARG DOCKER_INSTALL_SCRIPT_COMMIT=5c8855e"
},
{
"path": "packages/os/usb-installer/.gitignore",
"chars": 6,
"preview": "build\n"
},
{
"path": "packages/os/usb-installer/build.sh",
"chars": 964,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nrootfs_dir=\"/tmp/rootfs\"\niso_image=\"/tmp/umbrelos-amd64-usb-installer.iso\"\n\necho "
},
{
"path": "packages/os/usb-installer/builder.Dockerfile",
"chars": 102,
"preview": "FROM debian:bookworm\n\nRUN apt-get -y update\nRUN apt-get -y install grub-common grub-efi xorriso mtools"
},
{
"path": "packages/os/usb-installer/overlay/etc/systemd/system/custom-tty.service",
"chars": 221,
"preview": "[Unit]\nDescription=Custom TTY\nAfter=multi-user.target\n\n[Service]\nExecStart=/opt/custom-tty\nStandardInput=tty\nStandardOut"
},
{
"path": "packages/os/usb-installer/overlay/opt/custom-tty",
"chars": 2148,
"preview": "#!/usr/bin/env bash\n\nsleep 1\n\nclear\n\ncat << 'EOF'\n\n ,;###GGGGGGGGGGl#Sp\n ,##GGGlW\"\"^' '`\"\"%GGGG#"
},
{
"path": "packages/os/usb-installer/run.sh",
"chars": 1087,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nmkdir -p build\ndocker buildx build --load -f usb-installer.Dockerfile --platform "
},
{
"path": "packages/os/usb-installer/usb-installer.Dockerfile",
"chars": 1158,
"preview": "FROM debian:bookworm-slim\n\nRUN echo \"root:root\" | chpasswd\n\nRUN apt-get -y update\n\n# Install Linux kernel, systemd, boot"
},
{
"path": "packages/os/vm.sh",
"chars": 15529,
"preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nSTATE_DIR=\"${VM_STATE_"
},
{
"path": "packages/ui/.dockerignore",
"chars": 66,
"preview": "node_modules\n.git\n.dockerignore\nDockerfile\nREADME.md\nnpm-debug.log"
},
{
"path": "packages/ui/.gitignore",
"chars": 280,
"preview": "# custom\npublic/generated-tabler-icons\ntodo.md\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-de"
},
{
"path": "packages/ui/.prettierignore",
"chars": 52,
"preview": "package-lock.json\nnode_modules\npublic/locales/*.json"
},
{
"path": "packages/ui/.prettierrc.js",
"chars": 601,
"preview": "import baseConfig from '../../.prettierrc.js'\n\n/**\n * @type {import('prettier').Config & import(\"@ianvs/prettier-plugin-"
},
{
"path": "packages/ui/Dockerfile",
"chars": 408,
"preview": "FROM node:18.19.1-buster-slim\n\n# Set the working directory\nWORKDIR /app\n\n# Copy the package.json and package-lock.json\nC"
},
{
"path": "packages/ui/app-auth/README.md",
"chars": 570,
"preview": "# Local testing\n\nMake sure umbreld is running\n\n```\ncd packages/umbreld\nnpm run dev\n```\n\nThen in another terminal\n\n```\ncd"
},
{
"path": "packages/ui/app-auth/index.html",
"chars": 980,
"preview": "<!doctype html>\n<html class=\"h-full min-h-full\">\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"wid"
},
{
"path": "packages/ui/app-auth/src/login-with-umbrel.tsx",
"chars": 6485,
"preview": "import {ReactNode, useEffect, useState} from 'react'\nimport {arrayIncludes} from 'ts-extras'\n\nimport {AppIcon} from '@/c"
},
{
"path": "packages/ui/app-auth/src/main.tsx",
"chars": 319,
"preview": "import {BrowserRouter} from 'react-router-dom'\n\nimport {init} from '../../src/init'\nimport LoginWithUmbrel from './login"
},
{
"path": "packages/ui/app-auth/vite.config.ts",
"chars": 522,
"preview": "import path from 'node:path'\nimport react from '@vitejs/plugin-react'\nimport {defineConfig} from 'vite'\n\n// https://vite"
},
{
"path": "packages/ui/components.json",
"chars": 373,
"preview": "{\n\t\"$schema\": \"https://ui.shadcn.com/schema.json\",\n\t\"style\": \"default\",\n\t\"rsc\": false,\n\t\"tsx\": true,\n\t\"tailwind\": {\n\t\t\"c"
},
{
"path": "packages/ui/eslint.config.js",
"chars": 1710,
"preview": "import js from '@eslint/js'\nimport pluginQuery from '@tanstack/eslint-plugin-query'\nimport pluginReact from 'eslint-plug"
},
{
"path": "packages/ui/index.html",
"chars": 971,
"preview": "<!doctype html>\n<html class=\"h-full min-h-full\">\n\t<head>\n\t\t<meta charset=\"UTF-8\" />\n\t\t<meta name=\"viewport\" content=\"wid"
},
{
"path": "packages/ui/package.json",
"chars": 4467,
"preview": "{\n\t\"name\": \"ui\",\n\t\"private\": true,\n\t\"version\": \"0.0.0\",\n\t\"type\": \"module\",\n\t\"engines\": {\n\t\t\"node\": \"^22.13.0\"\n\t},\n\t\"scri"
},
{
"path": "packages/ui/public/locales/de.json",
"chars": 89267,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Eine zweite Sicherheitsebene für dein Umbrel-Login und Apps\",\n \"2fa.disable.tit"
},
{
"path": "packages/ui/public/locales/en.json",
"chars": 81022,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"A second layer of security for your Umbrel login and apps\",\n \"2fa.disable.title"
},
{
"path": "packages/ui/public/locales/es.json",
"chars": 88417,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Una segunda capa de seguridad para tu inicio de sesión en Umbrel y aplicaciones\""
},
{
"path": "packages/ui/public/locales/fr.json",
"chars": 90205,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Une seconde couche de sécurité pour votre connexion Umbrel et applications\",\n \""
},
{
"path": "packages/ui/public/locales/hu.json",
"chars": 87475,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Egy második biztonsági réteg az Umbrel bejelentkezésedhez és az alkalmazásaidhoz"
},
{
"path": "packages/ui/public/locales/it.json",
"chars": 87335,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Un secondo livello di sicurezza per il tuo login e app Umbrel\",\n \"2fa.disable.t"
},
{
"path": "packages/ui/public/locales/ja.json",
"chars": 66887,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Umbrelのログインとアプリのための二段階認証\",\n \"2fa.disable.title\": \"二段階認証を無効にする\",\n \"2fa.enable.o"
},
{
"path": "packages/ui/public/locales/ko.json",
"chars": 66810,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Umbrel 로그인과 앱을 위한 2단계 인증\",\n \"2fa.disable.title\": \"2단계 인증 해제\",\n \"2fa.enable.or-"
},
{
"path": "packages/ui/public/locales/nl.json",
"chars": 85991,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Een tweede beveiligingslaag voor je Umbrel login en apps\",\n \"2fa.disable.title\""
},
{
"path": "packages/ui/public/locales/pt.json",
"chars": 86486,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Uma segunda camada de segurança para o seu login no Umbrel e aplicativos\",\n \"2f"
},
{
"path": "packages/ui/public/locales/tr.json",
"chars": 84259,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Umbrel girişiniz ve uygulamalarınız için ikinci bir güvenlik katmanı\",\n \"2fa.di"
},
{
"path": "packages/ui/public/locales/uk.json",
"chars": 85993,
"preview": "{\n \"2fa\": \"2FA\",\n \"2fa-description\": \"Другий рівень безпеки для вашого входу в Umbrel і програм\",\n \"2fa.disable.title"
},
{
"path": "packages/ui/public/site.webmanifest",
"chars": 343,
"preview": "{\n\t\"name\": \"\",\n\t\"short_name\": \"\",\n\t\"icons\": [\n\t\t{\n\t\t\t\"src\": \"/favicon/android-chrome-192x192.png\",\n\t\t\t\"sizes\": \"192x192\""
},
{
"path": "packages/ui/src/components/app-icon.tsx",
"chars": 1468,
"preview": "import {HTMLProps, useEffect, useState} from 'react'\n\nimport {cn} from '@/lib/utils'\nimport {APP_ICON_PLACEHOLDER_SRC} f"
},
{
"path": "packages/ui/src/components/caret-right.tsx",
"chars": 460,
"preview": "const SvgComponent = ({className}: {className?: string}) => (\n\t<svg xmlns='http://www.w3.org/2000/svg' width={27} height"
},
{
"path": "packages/ui/src/components/chevron-down.tsx",
"chars": 379,
"preview": "/**\n * Most icons have a box around them. This one's bounding box matches the icon.\n */\nexport function ChevronDown() {\n"
}
]
// ... and 701 more files (download for full content)
About this extraction
This page contains the full source code of the getumbrel/umbrel GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 901 files (3.9 MB), approximately 1.1M tokens, and a symbol index with 1857 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.