Full Code of getumbrel/umbrel for AI

master 3e4ad453ad63 cached
901 files
3.9 MB
1.1M tokens
1857 symbols
1 requests
Download .txt
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
================================================
[![umbrelOS](https://github.com/user-attachments/assets/cabf8af7-51ce-45df-ad3a-a664cc91c610)](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)

[![umbrelOS use cases](https://github.com/user-attachments/assets/284feee7-15a1-48f2-a694-c968f1cc702f)](https://umbrel.com/umbrelos)
[![Umbrel App Store](https://github.com/user-attachments/assets/3d7846c7-d896-48f5-8a30-3578554702fa)](https://apps.umbrel.com)
[![Files on umbrelOS](https://github.com/user-attachments/assets/6c501256-47a0-4ce1-89ad-4ba02f4c9f2d)](https://umbrel.com/umbrelos)
[![umbrelOS Features](https://github.com/user-attachments/assets/6828da74-2b64-4b56-a7b7-5db603d023c8)](https://umbrel.com/umbrelos)
[![Backups in umbrelOS](https://github.com/user-attachments/assets/39778824-ed18-4f6f-a865-1d77bbfce833)](https://umbrel.com/umbrelos)
[![External Storage & NAS in umbrelOS](https://github.com/user-attachments/assets/4841c2dc-4ba4-4d47-bf0a-0e342bf60166)](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.

[![License](https://img.shields.io/badge/license-PolyForm%20Noncommercial%201.0.0-%235351FB)](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
================================================
[![Umbrel App Auth](https://static.getumbrel.com/github/github-banner-umbrel-app-auth.svg)](https://github.com/getumbrel/umbrel-app-auth)

[![Docker Build](https://img.shields.io/github/workflow/status/getumbrel/umbrel-app-auth/Docker%20build%20on%20push?color=%235351FB)](https://github.com/getumbrel/umbrel-app-auth/actions?query=workflow%3A"Docker+build+on+push")
[![Docker Pulls](https://img.shields.io/docker/pulls/getumbrel/app-auth?color=%235351FB)](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
================================================
[![Umbrel App Proxy](https://static.getumbrel.com/github/github-banner-umbrel-app-proxy.svg)](https://github.com/getumbrel/umbrel-app-proxy)

[![Docker Build](https://img.shields.io/github/workflow/status/getumbrel/umbrel-app-proxy/Docker%20build%20on%20push?color=%235351FB)](https://github.com/getumbrel/umbrel-app-proxy/actions?query=workflow%3A"Docker+build+on+push")
[![Docker Pulls](https://img.shields.io/docker/pulls/getumbrel/app-proxy?color=%235351FB)](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
================================================
[![Umbrel Tor](https://static.getumbrel.com/github/github-banner-umbrel-tor.svg)](https://github.com/getumbrel/umbrel-tor)

[![Docker Build](https://img.shields.io/github/workflow/status/getumbrel/umbrel-tor/Docker%20build%20on%20push?color=%235351FB)](https://github.com/getumbrel/umbrel-tor/actions?query=workflow%3A"Docker+build+on+push")
[![Docker Pulls](https://img.shields.io/docker/pulls/getumbrel/tor?color=%235351FB)](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
Download .txt
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
Download .txt
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": "[![umbrelOS](https://github.com/user-attachments/assets/cabf8af7-51ce-45df-ad3a-a664cc91c610)](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": "[![Umbrel App Auth](https://static.getumbrel.com/github/github-banner-umbrel-app-auth.svg)](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": "[![Umbrel App Proxy](https://static.getumbrel.com/github/github-banner-umbrel-app-proxy.svg)](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": "[![Umbrel Tor](https://static.getumbrel.com/github/github-banner-umbrel-tor.svg)](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.

Copied to clipboard!