Merge remote-tracking branch 'origin/main' into feat/i18n

This commit is contained in:
izzy
2026-09-29 14:10:03 +01:00
99 changed files with 184 additions and 166 deletions
+1
View File
@@ -11,6 +11,7 @@ e2e
**/.env*
Dockerfile
packages/**/build
!packages/**/.mise/tasks/**
.dev
.mise/data
**/tmp
+3 -3
View File
@@ -27,7 +27,7 @@ jobs:
# that goes stale after a migration re-date on main slips past it; the
# deploy workflow's gate is the authoritative check.
- name: Migration ordering vs main
run: .mise/tasks/yucca-api/check-migration-order origin/main
run: packages/yucca-api/.mise/tasks/check-migration-order origin/main
- name: Setup Mise
uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
@@ -80,8 +80,8 @@ jobs:
( mise k3d:up && mise tilt:ci-infra ) > /tmp/k3d-infra.log 2>&1 &
infra=$!
mise run install:frozen
mise run common:server:build
mise run common:emails:build
mise run //packages/common:build
mise run //packages/emails:build
wait "$infra" || { echo "::error::k3d infra failed"; cat /tmp/k3d-infra.log; exit 1; }
echo "::group::k3d infra log"; cat /tmp/k3d-infra.log; echo "::endgroup::"
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
with:
persist-credentials: false
fetch-depth: 2
- run: .mise/tasks/yucca-api/check-migration-order HEAD^
- run: packages/yucca-api/.mise/tasks/check-migration-order HEAD^
# == Build + push every app image (the ONLY delivery action CI performs) ==
# Release commits build like any other. v<version> image tags are published
+1 -2
View File
@@ -12,7 +12,6 @@ on:
- 'pnpm-workspace.yaml'
- '.npmrc'
- '.mise/config.toml'
- '.mise/tasks/docs/**'
- 'tf/pages/**'
- 'tf/op-run.sh'
- '.github/workflows/docs.yml'
@@ -49,7 +48,7 @@ jobs:
- name: Install 1Password CLI
uses: 1password/install-cli-action@a5215d3a7f75c1629216c465ea9ab3ab399c4b71 # v4.0.0
- run: mise run docs:destroy
- run: mise run //packages/docs:destroy
- name: Remove preview comment
if: ${{ github.event_name == 'pull_request_target' }}
+2 -3
View File
@@ -12,7 +12,6 @@ on:
- 'pnpm-workspace.yaml'
- '.npmrc'
- '.mise/config.toml'
- '.mise/tasks/docs/**'
- 'tf/pages/**'
- 'tf/op-run.sh'
- '.github/workflows/docs.yml'
@@ -45,7 +44,7 @@ jobs:
- run: mise run install:frozen
- run: mise run docs:build
- run: mise run //packages/docs:build
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
@@ -83,7 +82,7 @@ jobs:
path: packages/docs/build
- id: deploy
run: mise run docs:deploy
run: mise run //packages/docs:deploy
- name: Preview comment
if: ${{ github.event_name == 'pull_request' }}
+37 -11
View File
@@ -1,7 +1,23 @@
monorepo_root = true
[monorepo]
config_roots = ["ansible/ceph", "ansible/talos", "packages/restic-proxy"]
config_roots = [
"ansible/ceph",
"ansible/talos",
"packages/common",
"packages/docs",
"packages/emails",
"packages/futo-backups-bot",
"packages/restic-proxy",
"packages/standalone-app",
"packages/web",
"packages/yucca-admin-api",
"packages/yucca-api",
"packages/yucca-api-client",
"packages/yucca-metrics-worker",
"packages/yucca-sdk/orchestration-api",
"packages/yucca-sdk/orchestration-ui",
]
[tools]
node = "25.9.0"
@@ -38,34 +54,44 @@ opentofu = "1.12.6"
terragrunt = "0.99.5"
# ansible/mgmt convergence (mgmt:ansible task, run from CI on prod apply).
"pipx:ansible-core" = "2.21.4"
[tasks.dev]
description = "Start all services in development mode"
depends = ["install:deps", "common:build", "docker:start"]
run = [{ task = "*:dev" }]
run = [
{ tasks = [
"michael:dev",
"//packages/restic-proxy:dev",
"//packages/web:dev",
"//packages/yucca-admin-api:dev",
"//packages/yucca-api:dev",
"//packages/yucca-metrics-worker:dev",
"//packages/yucca-sdk/orchestration-api:dev",
"//packages/yucca-sdk/orchestration-ui:dev",
] },
]
[tasks.build]
description = "Build all packages"
depends = ["*:build"]
depends = ["*:build", "//packages/...:build"]
[tasks."common:build"]
description = "Build all common packages"
run = [{ task = "install:deps" }, { task = "common:*:build" }]
run = [{ task = "install:deps" }, { tasks = ["//packages/common:build", "//packages/emails:build"] }]
[tasks.test]
description = "Run all unit tests"
depends = ["*:test"]
depends = ["*:test", "//packages/...:test"]
[tasks."test:integration"]
description = "Run all integration tests"
# NB: mise flags must precede the task pattern — anything after it is forwarded
# to the tasks themselves (jest/go test choke on a stray --jobs).
# Both depths: `*` does not cross `:`, so the first pattern alone silently
# skipped yucca-sdk:orchestration-api:test:integration.
run = "mise run --jobs 1 '*:test:integration' ::: '*:*:test:integration'"
run = "mise run --jobs 1 '*:test:integration' ::: '//packages/...:test:integration'"
[tasks."test:e2e:web"]
description = "Run all web e2e tests"
run = [{ task = "test:e2e:wait" }, { task = "*:test:e2e:web" }]
run = [{ task = "test:e2e:wait" }, { task = "//packages/...:test:e2e:web" }]
[tasks."prepare"]
description = "Install and build packages"
@@ -73,11 +99,11 @@ run = [{ task = "install:frozen" }, { task = "build" }]
[tasks."check"]
description = "Run checks & unit tests"
run = [{ tasks = ["lint", "lint:go", "*:check"] }, { task = "format" }, { task = "test" }]
run = [{ tasks = ["lint", "lint:go", "//packages/...:check"] }, { task = "format" }, { task = "test" }]
[tasks.fix]
description = "Run all possible code fixes"
run = [{ task = "*:fix" }, { task = "yucca-sdk:orchestration-ui:lingui" }]
run = [{ task = "*:fix" }, { task = "//packages/yucca-sdk/orchestration-ui:lingui" }]
[tasks."o11y:render"]
description = "Render the o11y bundle"
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Lint all code files"
#MISE depends=["*:check"]
#MISE depends=["//packages/...:check"]
set -e
pnpm exec eslint "**/src/**/*.ts" --max-warnings 0
+1 -1
View File
@@ -3,7 +3,7 @@
#MISE depends=["test:e2e:wait", "//packages/restic-proxy:build"]
set -e
source "$(dirname "$0")/../../michael/env"
source "$(dirname "$0")/../../yucca-api/env"
source "$(dirname "$0")/../../../../packages/yucca-api/.mise/tasks/env"
export PATH="$(dirname "$0")/../../../../dist:$PATH"
pnpm --filter e2e test "$@"
+1 -1
View File
@@ -4,7 +4,7 @@
# handling in packages/e2e/k3d/run.sh and does not use this task).
set -e
source "$(dirname "$0")/../../michael/env"
source "$(dirname "$0")/../../yucca-api/env"
source "$(dirname "$0")/../../../../packages/yucca-api/.mise/tasks/env"
echo Ensure dev environment is running before invoking tests.
+2 -2
View File
@@ -61,5 +61,5 @@ echo "==> run the database-backed integration suites"
# same tables between tests. orchestration-api needs no infrastructure at all
# and rides along here because this is where integration suites are expected to
# run — the glob in `mise test:integration` had never matched its task name.
mise run --jobs 1 yucca-api:test:integration ::: yucca-admin-api:test:integration
mise run yucca-sdk:orchestration-api:test:integration
mise run --jobs 1 //packages/yucca-api:test:integration ::: //packages/yucca-admin-api:test:integration
mise run //packages/yucca-sdk/orchestration-api:test:integration
+1 -1
View File
@@ -62,4 +62,4 @@ export RADOS_ENDPOINT RADOS_ACCESS_KEY_ID RADOS_SECRET_ACCESS_KEY
echo "==> run the S3-backed integration suites"
# Concurrent: each scopes itself to freshly generated repository/bucket names.
mise run michael:test:integration ::: yucca-metrics-worker:test:integration
mise run michael:test:integration ::: //packages/yucca-metrics-worker:test:integration
-8
View File
@@ -1,8 +0,0 @@
#!/usr/bin/env bash
#MISE description="Run web in development mode"
#MISE depends=["yucca-api-client:build", "common:build", "yucca-sdk:orchestration-ui:build"]
set -e
source "$(dirname "$0")/../yucca-api/env"
export WEB_PORT=${WEB_PORT:-36033}
pnpm --filter web dev --host --port $WEB_PORT
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
#MISE description="Build yucca-sdk"
#MISE depends=["yucca-sdk:*:build"]
set -e
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
#MISE description="Check yucca-sdk"
#MISE depends=["yucca-sdk:*:check"]
set -e
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
#MISE description="Start yucca-sdk in development mode"
#MISE depends=["yucca-sdk:*:dev"]
set -e
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
#MISE description="Extract and compile i18n catalogs"
mise yucca-sdk:orchestration-ui:lingui:extract
mise yucca-sdk:orchestration-ui:lingui:compile
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
#MISE description="Test yucca-sdk"
#MISE depends=["yucca-sdk:*:test"]
set -e
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
#MISE description="Run end-to-end tests for yucca-sdk"
#MISE depends=["yucca-sdk:*:test:e2e-web"]
set -e
+7 -7
View File
@@ -51,8 +51,8 @@ Yucca is a multi-tenant **backup service**: OIDC-authenticated users get S3-back
```bash
mise dev # compose-based dev: deps, docker infra (postgres/minio/mock-oidc/victoria-*), all *:dev
mise <pkg>:dev # one service, e.g. mise web:dev, mise yucca-api:dev
mise docs:dev # docs site (packages/docs) on :36034
mise //packages/<pkg>:dev # one service, e.g. mise //packages/web:dev; Go services stay mise michael:dev
mise //packages/docs:dev # docs site (packages/docs) on :36034
mise check # lint + format check + svelte-check + unit tests (= the `checks` CI job)
mise fix # autofix lint/format + lingui extract
@@ -62,9 +62,9 @@ mise test # all unit tests (jest per NestJS pkg, vitest for web)
mise test:integration # integration tests (--jobs 1; needs infra up)
mise test:integration:k3d # CI split: the database-backed suites; :s3 = the Ceph-backed ones
mise test:e2e # e2e (needs the stack running); mise test:e2e:web = Playwright
mise <pkg>:test # one package; args after -- go to jest: mise yucca-api:test -- -t "name"
mise //packages/<pkg>:test # one package; args after -- go to jest: mise //packages/yucca-api:test -- -t "name"
mise yucca-api:migrations <args> # DB migrations (@immich/sql-tools; yucca-api owns the schema)
mise //packages/yucca-api:migrations <args> # DB migrations (@immich/sql-tools; yucca-api owns the schema)
```
### k3d + Tilt (prod-shaped dev)
@@ -117,7 +117,7 @@ Zod-validated `env.ts`, JWT auth guards via `@AuthRoute()`, OTel from `@common/s
| `emails` (`@common/emails`) | Svelte lib | Transactional email templates (better-svelte-email, web theme), prebuilt to JS for the NestJS apps. See `docs/email.md`. |
**Frontend** (`packages/web`): SvelteKit 5 + Tailwind 4, `@immich/ui`, lingui i18n
(catalogue owned by orchestration-ui, one `.po` per language shared with the SDK: `mise yucca-sdk:orchestration-ui:lingui:*`;
(catalogue owned by orchestration-ui, one `.po` per language shared with the SDK: `mise //packages/yucca-sdk/orchestration-ui:lingui:*`;
compiled locales are generated, not edited), generated API client.
**Docs** (`packages/docs`, https://docs.futo.cloud): the **end-user** documentation site (beta setup
guides for Immich and the standalone container). SvelteKit + `adapter-static`; every page is a
@@ -128,8 +128,8 @@ in `docs/` and the per-directory READMEs — do not move them onto the site.
**`packages/yucca-sdk/`** (orchestration-api + orchestration-ui) is separately versioned and
added explicitly in `pnpm-workspace.yaml`.
**API client generation**: yucca-api DTOs → `mise yucca-api:sync-openapi` →
`mise yucca-api-client:build` (oazapfts) → `packages/yucca-api-client/src/fetch-client.ts`.
**API client generation**: yucca-api DTOs → `mise //packages/yucca-api:sync-openapi` →
`mise //packages/yucca-api-client:build` (oazapfts) → `packages/yucca-api-client/src/fetch-client.ts`.
Generated — regenerate on contract changes, never hand-edit.
**Database**: PostgreSQL via **Kysely**. Schema in `packages/yucca-api/src/schema/`
@@ -1,12 +1,13 @@
#!/usr/bin/env bash
#MISE description="Publish packages/docs/build to Cloudflare Pages (ENVIRONMENT=prod|dev, TF_VAR_stage=pr-<n> for a preview)"
#MISE dir="{{config_root}}/../.."
set -euo pipefail
export ENVIRONMENT="${ENVIRONMENT:-dev}"
export TF_VAR_stage="${TF_VAR_stage:-}"
export OP_ENV_FILE=tf/pages/.env
[ -d packages/docs/build ] || { echo "docs:deploy: packages/docs/build is missing — run mise docs:build first" >&2; exit 1; }
[ -d packages/docs/build ] || { echo "docs:deploy: packages/docs/build is missing — run mise //packages/docs:build first" >&2; exit 1; }
tf/op-run.sh terragrunt run --all apply --non-interactive --parallelism 1 --working-dir tf/pages/docs
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
#MISE description="Tear down a docs preview stage (TF_VAR_stage=pr-<n>); the shared Pages project is kept"
#MISE dir="{{config_root}}/../.."
set -euo pipefail
export ENVIRONMENT="${ENVIRONMENT:-dev}"
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run docs in preview mode"
#MISE depends=["docs:build"]
#MISE depends=[":build"]
set -e
pnpm --filter docs preview --host
+5 -5
View File
@@ -7,10 +7,10 @@ This site is a SvelteKit app, published at [docs.futo.cloud](https://docs.futo.c
## Running the site
```bash
mise docs:dev # dev server with hot reload on http://localhost:36034
mise docs:check # svelte-check
mise docs:test # unit tests for the page index
mise docs:build # static build into packages/docs/build
mise //packages/docs:dev # dev server with hot reload on http://localhost:36034
mise //packages/docs:check # svelte-check
mise //packages/docs:test # unit tests for the page index
mise //packages/docs:build # static build into packages/docs/build
```
`mise check` and `mise build` include the docs, so CI builds the site on every pull request.
@@ -93,7 +93,7 @@ Because the page is compiled as Svelte, curly braces and tag-like text such as `
## Previews and deployment
The Docs workflow publishes the site to Cloudflare Pages. Every pull request that touches the documentation gets its own preview at `docs.pr-<number>.dev.futo.cloud`, linked from a comment on the pull request and removed when it closes; merging to `main` deploys [docs.futo.cloud](https://docs.futo.cloud). The Pages project and its domains are Terraform under `tf/pages/docs`, and `mise docs:deploy` and `mise docs:destroy` are the tasks behind the workflow.
The Docs workflow publishes the site to Cloudflare Pages. Every pull request that touches the documentation gets its own preview at `docs.pr-<number>.dev.futo.cloud`, linked from a comment on the pull request and removed when it closes; merging to `main` deploys [docs.futo.cloud](https://docs.futo.cloud). The Pages project and its domains are Terraform under `tf/pages/docs`, and `mise //packages/docs:deploy` and `mise //packages/docs:destroy` are the tasks behind the workflow.
## Search
+4 -4
View File
@@ -47,8 +47,8 @@ trap cleanup EXIT
# CI has already built the workspace by this point.
if [ -z "${YUCCA_E2E_PREBUILT:-}" ]; then
echo "==> build workspace libs (sdk consumed by orchestration-api + the e2e)"
mise run common:build >/dev/null
mise run yucca-sdk:orchestration-ui:build >/dev/null
mise run //:common:build >/dev/null
mise run //packages/yucca-sdk/orchestration-ui:build >/dev/null
fi
echo "==> build restic-proxy (spawned by orchestration-api and the proxy suites)"
@@ -98,7 +98,7 @@ echo "==> launch orchestration-api as a separate local process (:22676)"
# setsid gives it a process group cleanup can reap whole.
FUTO_BACKUPS_WELL_KNOWN_URL="http://localhost:8080/.well-known/yucca.json" \
NODE_OPTIONS="--require $HERE/hostmap.cjs" \
setsid mise run yucca-sdk:orchestration-api:dev >/tmp/yucca-e2e-orch.log 2>&1 &
setsid mise run //packages/yucca-sdk/orchestration-api:dev >/tmp/yucca-e2e-orch.log 2>&1 &
ORCH_PGID="$(ps -o pgid= -p $! 2>/dev/null | tr -d ' ')"
wait_for_http() {
@@ -120,7 +120,7 @@ echo "==> jest e2e (it-works, restic, yucca-api, orchestration-api)"
# shellcheck disable=SC1091
source .mise/tasks/michael/env
# shellcheck disable=SC1091
source .mise/tasks/yucca-api/env
source packages/yucca-api/.mise/tasks/env
# Three, not one per suite: orchestration-api alone runs ~105s and gates the
# phase, so a fourth worker adds no wall-clock — only a fourth CPU consumer on a
# runner that is already hosting the cluster these suites drive.
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build futo-backups-bot"
#MISE depends=["common:build"]
#MISE depends=["//:common:build"]
set -e
NODE_ENV=production pnpm --filter futo-backups-bot build
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
#MISE description="Run futo-backups-bot in development mode"
#MISE depends=["common:build"]
#MISE dir="{{config_root}}"
#MISE depends=["//:common:build"]
#MISE dir="{{config_root}}/../.."
set -e
source "$(dirname "$0")/env"
+1 -1
View File
@@ -66,7 +66,7 @@ RUN npm install -g pnpm@10.28.1
RUN curl https://mise.run | sh
RUN mise trust
RUN pnpm i --frozen-lockfile
RUN mise futo-backups-bot:build
RUN mise //packages/futo-backups-bot:build
RUN pnpm --filter futo-backups-bot deploy /deploy --prod --legacy
FROM ${ALPINE_IMAGE}
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build standalone-app"
#MISE depends=["yucca-sdk:orchestration-api:build", "yucca-sdk:orchestration-ui:build"]
#MISE depends=["//packages/yucca-sdk/orchestration-api:build", "//packages/yucca-sdk/orchestration-ui:build"]
set -e
NODE_ENV=production pnpm --filter @futo-org/backups-standalone build
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run standalone-app in development mode (vite UI + tsx server)"
#MISE depends=["yucca-sdk:orchestration-ui:generate-fetch-client", "yucca-sdk:orchestration-api:build", "yucca-sdk:orchestration-ui:build"]
#MISE depends=["//packages/yucca-sdk/orchestration-ui:generate-fetch-client", "//packages/yucca-sdk/orchestration-api:build", "//packages/yucca-sdk/orchestration-ui:build"]
set -e
export STANDALONE_WEB_PORT=${STANDALONE_WEB_PORT:-36067}
export PORT=${PORT:-22676}
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build the standalone-app Docker image"
#MISE dir="{{config_root}}/../.."
set -e
TAG=${STANDALONE_IMAGE_TAG:-yucca-standalone:dev}
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build web"
#MISE depends=["yucca-api-client:build", "yucca-sdk:orchestration-ui:build"]
#MISE depends=["//packages/yucca-api-client:build", "//packages/yucca-sdk/orchestration-ui:build"]
set -e
NODE_ENV=production pnpm --filter web build
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
#MISE description="Run web in development mode"
#MISE depends=["//packages/yucca-api-client:build", "//:common:build", "//packages/yucca-sdk/orchestration-ui:build"]
set -e
source "$(dirname "$0")/../../../yucca-api/.mise/tasks/env"
export WEB_PORT=${WEB_PORT:-36033}
pnpm --filter web dev --host --port $WEB_PORT
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run web in preview mode"
#MISE depends=["web:build"]
#MISE depends=[":build"]
set -e
pnpm --filter web preview --host
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run end-to-end tests for web"
#MISE depends=["web:test:install-deps"]
#MISE depends=[":test:install-deps"]
set -e
pnpm --filter web test:e2e "$@"
+1 -1
View File
@@ -68,7 +68,7 @@ RUN npm install -g pnpm@10.28.1
RUN curl https://mise.run | sh
RUN mise trust
RUN pnpm i --frozen-lockfile
RUN NODE_ENV=production mise web:build
RUN NODE_ENV=production mise //packages/web:build
RUN pnpm --filter web deploy /deploy --prod --legacy
FROM ${ALPINE_IMAGE}
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build yucca-admin-api"
#MISE depends=["common:build"]
#MISE depends=["//:common:build"]
set -e
NODE_ENV=production pnpm --filter yucca-admin-api build
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
#MISE description="Run yucca-admin-api in development mode"
#MISE depends=["common:build"]
#MISE dir="{{config_root}}"
#MISE depends=["//:common:build"]
#MISE dir="{{config_root}}/../.."
set -e
source "$(dirname "$0")/env"
@@ -9,7 +9,7 @@ export POSTGRES_DATABASE=${POSTGRES_DATABASE:-yucca}
export POSTGRES_PORT=${POSTGRES_PORT:-15432}
# The project's well-known local-dev ES256 keypair (same fixture as
# .mise/tasks/yucca-api/env); signs CLI session JWTs in dev.
# packages/yucca-api/.mise/tasks/env); signs CLI session JWTs in dev.
export JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:------BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCla79+Sip4o2hZ1K
nQJYp2xU/nsCJmRoZmvXjeX6cW+hRANCAASkubBRIE7Sns/VS8ZF5XEVO8CwJ+AZ
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
#MISE description="yucca-admin-api: sync OpenAPI specification"
#MISE dir="{{config_root}}/packages/yucca-admin-api"
#MISE depends=["yucca-admin-api:build"]
#MISE depends=[":build"]
set -e
source "$(dirname "$0")/env"
+1 -1
View File
@@ -66,7 +66,7 @@ RUN npm install -g pnpm@10.28.1
RUN curl https://mise.run | sh
RUN mise trust
RUN pnpm i --frozen-lockfile
RUN mise yucca-admin-api:build
RUN mise //packages/yucca-admin-api:build
RUN pnpm --filter yucca-admin-api deploy /deploy --prod --legacy
FROM ${ALPINE_IMAGE}
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Generate and build yucca-api-client"
#MISE depends=["yucca-api:sync-openapi"]
#MISE depends=["//packages/yucca-api:sync-openapi"]
set -e
pnpm --filter @futo-org/backups-api-client generate
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build yucca-api"
#MISE depends=["common:build"]
#MISE depends=["//:common:build"]
set -e
NODE_ENV=production pnpm --filter yucca-api build
@@ -1,5 +1,6 @@
#!/usr/bin/env bash
#MISE description="Fail if a new migration sorts before one already present on the base ref"
#MISE dir="{{config_root}}/../.."
set -euo pipefail
base="${1:-origin/main}"
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
#MISE description="Run yucca-api in development mode"
#MISE depends=["common:build"]
#MISE dir="{{config_root}}"
#MISE depends=["//:common:build"]
#MISE dir="{{config_root}}/../.."
set -e
source "$(dirname "$0")/env"
@@ -1,6 +1,5 @@
#!/usr/bin/env bash
#MISE description="yucca-api: database migrations script"
#MISE dir="{{config_root}}/packages/yucca-api"
set -e
source "$(dirname "$0")/env"
@@ -1,7 +1,6 @@
#!/usr/bin/env bash
#MISE description="yucca-api: sync OpenAPI specification"
#MISE dir="{{config_root}}/packages/yucca-api"
#MISE depends=["yucca-api:build"]
#MISE depends=[":build"]
set -e
source "$(dirname "$0")/env"
+1 -1
View File
@@ -66,7 +66,7 @@ RUN npm install -g pnpm@10.28.1
RUN curl https://mise.run | sh
RUN mise trust
RUN pnpm i --frozen-lockfile
RUN mise yucca-api:build
RUN mise //packages/yucca-api:build
RUN pnpm --filter yucca-api deploy /deploy --prod --legacy
FROM ${ALPINE_IMAGE}
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build yucca-metrics-worker"
#MISE depends=["common:build"]
#MISE depends=["//:common:build"]
set -e
NODE_ENV=production pnpm --filter yucca-metrics-worker build
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
#MISE description="Run yucca-metrics-worker in development mode"
#MISE depends=["common:build"]
#MISE dir="{{config_root}}"
#MISE depends=["//:common:build"]
#MISE dir="{{config_root}}/../.."
set -e
source "$(dirname "$0")/env"
+1 -1
View File
@@ -66,7 +66,7 @@ RUN npm install -g pnpm@10.28.1
RUN curl https://mise.run | sh
RUN mise trust
RUN pnpm i --frozen-lockfile
RUN mise yucca-metrics-worker:build
RUN mise //packages/yucca-metrics-worker:build
RUN pnpm --filter yucca-metrics-worker deploy /deploy --prod --legacy
FROM ${ALPINE_IMAGE}
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build yucca-sdk/orchestration-api"
#MISE depends=["yucca-api-client:build"]
#MISE depends=["//packages/yucca-api-client:build"]
set -e
NODE_ENV=production pnpm --filter @futo-org/backups-orchestrator-api build
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run yucca-sdk/orchestration-api in development mode"
#MISE depends=["yucca-sdk:orchestration-ui:generate-fetch-client"]
#MISE depends=["//packages/yucca-sdk/orchestration-ui:generate-fetch-client"]
set -e
pnpm --filter @futo-org/backups-orchestrator-api dev
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Build yucca-sdk/orchestration-ui"
#MISE depends=["yucca-sdk:orchestration-ui:generate-fetch-client"]
#MISE depends=[":generate-fetch-client"]
set -e
NODE_ENV=production pnpm --filter @futo-org/backups-orchestrator-ui build
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run Svelte checks for yucca-sdk/orchestration-ui"
#MISE depends=["yucca-sdk:orchestration-ui:lingui:compile"]
#MISE depends=[":lingui:compile"]
set -e
pnpm --filter @futo-org/backups-orchestrator-ui check
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run yucca-sdk/orchestration-ui in development mode"
#MISE depends=["yucca-sdk:orchestration-ui:generate-fetch-client", "yucca-sdk:orchestration-ui:lingui:compile"]
#MISE depends=[":generate-fetch-client", ":lingui:compile"]
set -e
export ORCHESTRATOR_WEB_PORT=${ORCHESTRATOR_WEB_PORT:-36066}
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Generate fetch client for yucca-sdk/orchestration-ui"
#MISE depends=["yucca-sdk:orchestration-api:build"]
#MISE depends=["//packages/yucca-sdk/orchestration-api:build"]
set -e
pnpm --filter @futo-org/backups-orchestrator-api sync:openapi
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
#MISE description="Extract and compile i18n catalogs"
mise //packages/yucca-sdk/orchestration-ui:lingui:extract
mise //packages/yucca-sdk/orchestration-ui:lingui:compile
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Ensure i18n catalogs are up to date"
#MISE depends=["yucca-sdk:orchestration-ui:lingui"]
#MISE depends=[":lingui"]
set -e
git status --porcelain -- "packages/yucca-sdk/orchestration-ui/src/lib/locales" | grep -q . && exit 1 || exit 0
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run yucca-sdk/orchestration-ui in preview mode"
#MISE depends=["yucca-sdk:orchestration-ui:build"]
#MISE depends=[":build"]
set -e
pnpm --filter @futo-org/backups-orchestrator-ui preview --host
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run unit tests for yucca-sdk/orchestration-ui"
#MISE depends=["yucca-sdk:orchestration-ui:lingui:compile"]
#MISE depends=[":lingui:compile"]
set -e
pnpm --filter @futo-org/backups-orchestrator-ui test:unit "$@"
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
#MISE description="Run end-to-end tests for yucca-sdk/orchestration-ui"
#MISE depends=["yucca-sdk:orchestration-ui:test:install-deps"]
#MISE depends=[":test:install-deps"]
set -e
pnpm --filter @futo-org/backups-orchestrator-ui test:e2e "$@"
@@ -5,7 +5,7 @@
defaultQuietHoursStart,
toBandwidthDto,
toBandwidthForm,
unlimitedBandwidth,
toBytesPerSec,
useConfig,
useUpdateConfig,
} from "$lib/services/config.service";
@@ -14,6 +14,7 @@
Field,
HStack,
LoadingSpinner,
NumberInput,
Select,
Stack,
Switch,
@@ -22,29 +23,15 @@
import { mdiSpeedometerSlow } from "@mdi/js";
import { t } from "svelte-i18n-lingui";
const bytesPerMegabyte = 1_000_000;
const speeds = [
...[0.5, 1, 2, 5, 10, 20, 50, 100].map((megabytes) => ({
value: String(megabytes * bytesPerMegabyte),
label: `${megabytes} MB/s`,
})),
];
const hours = Array.from({ length: 24 }, (_, hour) => ({
value: `${String(hour).padStart(2, "0")}:00`,
label: `${String(hour).padStart(2, "0")}:00`,
}));
const config = useConfig();
const speedOptions = $derived([
{ value: unlimitedBandwidth, label: $t`No limit` },
...speeds,
]);
const mutation = useUpdateConfig();
let speed = $state(unlimitedBandwidth);
let mbps = $state<number>();
let quiet = $state(false);
let quietStart = $state(defaultQuietHoursStart);
let quietEnd = $state(defaultQuietHoursEnd);
@@ -55,7 +42,7 @@
return;
}
({ speed, quiet, quietStart, quietEnd } = toBandwidthForm(
({ mbps, quiet, quietStart, quietEnd } = toBandwidthForm(
config.data.bandwidth,
));
};
@@ -69,11 +56,11 @@
load();
});
const limited = $derived(speed !== unlimitedBandwidth);
const limited = $derived(toBytesPerSec(mbps) > 0);
const onSave = () => {
mutation.mutate({
bandwidth: toBandwidthDto({ speed, quiet, quietStart, quietEnd }),
bandwidth: toBandwidthDto({ mbps, quiet, quietStart, quietEnd }),
});
};
</script>
@@ -89,13 +76,15 @@
{:else}
<Field
label={$t`Upload speed`}
description={$t`Slow down backup uploads without affecting restores.`}
description={$t`Slow down backup uploads without affecting restores. Leave empty for no limit.`}
color="primary"
>
<Select
options={speedOptions}
value={speed}
onChange={(value) => (speed = value)}
<NumberInput
bind:value={mbps}
min={0}
step={1}
placeholder={$t`No limit`}
trailingText="Mbps"
/>
</Field>
@@ -1805,7 +1805,11 @@ msgid "Size updated"
msgstr ""
#: src/lib/components/integrations/immich/settings/ImmichSettingsBandwidth.svelte
msgid "Slow down backup uploads without affecting restores."
#~ msgid "Slow down backup uploads without affecting restores."
#~ msgstr ""
#: src/lib/components/integrations/immich/settings/ImmichSettingsBandwidth.svelte
msgid "Slow down backup uploads without affecting restores. Leave empty for no limit."
msgstr ""
#: src/lib/components/onboarding/upsell/UpsellBackupFeaturesGrid.svelte
@@ -2041,8 +2041,12 @@ msgid "Size updated"
msgstr "Size updated"
#: src/lib/components/integrations/immich/settings/ImmichSettingsBandwidth.svelte
msgid "Slow down backup uploads without affecting restores."
msgstr "Slow down backup uploads without affecting restores."
#~ msgid "Slow down backup uploads without affecting restores."
#~ msgstr "Slow down backup uploads without affecting restores."
#: src/lib/components/integrations/immich/settings/ImmichSettingsBandwidth.svelte
msgid "Slow down backup uploads without affecting restores. Leave empty for no limit."
msgstr "Slow down backup uploads without affecting restores. Leave empty for no limit."
#: src/lib/components/onboarding/upsell/UpsellBackupFeaturesGrid.svelte
msgid "Snapshot restore"
@@ -15,7 +15,7 @@ describe('toBandwidthForm', () => {
expect(
toBandwidthForm({ bytesPerSec: 5_000_000, quietHours: '23:00-07:00' }),
).toEqual({
speed: '5000000',
mbps: 40,
quiet: true,
quietStart: '23:00',
quietEnd: '07:00',
@@ -24,7 +24,7 @@ describe('toBandwidthForm', () => {
it('falls back to the default quiet hours when none are saved', () => {
expect(toBandwidthForm({ bytesPerSec: 0 })).toEqual({
speed: '0',
mbps: undefined,
quiet: false,
quietStart: '22:00',
quietEnd: '06:00',
@@ -33,10 +33,10 @@ describe('toBandwidthForm', () => {
});
describe('toBandwidthDto', () => {
it('converts the selected speed to bytes per second with quiet hours', () => {
it('converts megabits per second to bytes per second with quiet hours', () => {
expect(
toBandwidthDto({
speed: '500000',
mbps: 4,
quiet: true,
quietStart: '22:00',
quietEnd: '06:00',
@@ -47,7 +47,7 @@ describe('toBandwidthDto', () => {
it('clears quiet hours when they are switched off', () => {
expect(
toBandwidthDto({
speed: '2000000',
mbps: 16,
quiet: false,
quietStart: '22:00',
quietEnd: '06:00',
@@ -58,7 +58,18 @@ describe('toBandwidthDto', () => {
it('disables the limit and drops quiet hours when set to no limit', () => {
expect(
toBandwidthDto({
speed: '0',
mbps: 0,
quiet: true,
quietStart: '22:00',
quietEnd: '06:00',
}),
).toEqual({ bytesPerSec: 0, quietHours: undefined });
});
it('treats an empty speed as no limit', () => {
expect(
toBandwidthDto({
mbps: undefined,
quiet: true,
quietStart: '22:00',
quietEnd: '06:00',
@@ -87,7 +98,7 @@ describe('useUpdateConfig', () => {
useUpdateConfig().mutate({
bandwidth: toBandwidthDto({
speed: '1000000',
mbps: 8,
quiet: true,
quietStart: '22:00',
quietEnd: '06:00',
@@ -106,7 +117,7 @@ describe('useUpdateConfig', () => {
useUpdateConfig().mutate({
bandwidth: toBandwidthDto({
speed: '0',
mbps: 0,
quiet: true,
quietStart: '22:00',
quietEnd: '06:00',
@@ -13,12 +13,12 @@ export const configKeys = {
all: ['config'] as const,
};
export const unlimitedBandwidth = '0';
const bytesPerSecPerMbps = 125_000;
export const defaultQuietHoursStart = '22:00';
export const defaultQuietHoursEnd = '06:00';
export type BandwidthForm = {
speed: string;
mbps?: number;
quiet: boolean;
quietStart: string;
quietEnd: string;
@@ -32,25 +32,29 @@ export const toBandwidthForm = ({
quietHours?.split('-') ?? [];
return {
speed: String(bytesPerSec),
mbps: bytesPerSec ? bytesPerSec / bytesPerSecPerMbps : undefined,
quiet: Boolean(quietHours),
quietStart,
quietEnd,
};
};
export const toBytesPerSec = (mbps = 0) =>
Math.max(0, Math.ceil(mbps)) * bytesPerSecPerMbps;
export const toBandwidthDto = ({
speed,
mbps,
quiet,
quietStart,
quietEnd,
}: BandwidthForm): BandwidthDto => ({
bytesPerSec: Number(speed),
quietHours:
speed !== unlimitedBandwidth && quiet
? `${quietStart}-${quietEnd}`
: undefined,
});
}: BandwidthForm): BandwidthDto => {
const bytesPerSec = toBytesPerSec(mbps);
return {
bytesPerSec,
quietHours: bytesPerSec && quiet ? `${quietStart}-${quietEnd}` : undefined,
};
};
export const useConfig = () =>
createQuery(
+1 -1
View File
@@ -1,4 +1,4 @@
# op:// references only, resolved by the docs:deploy / docs:destroy mise tasks
# op:// references only, resolved by the //packages/docs:deploy / //packages/docs:destroy mise tasks
# and .github/workflows/docs*.yml. Every item is readable by every env SA.
export AWS_ACCESS_KEY_ID="op://yucca_tf/TF_STATE_S3_ACCESS_KEY/password"
+4 -4
View File
@@ -24,7 +24,7 @@ previews; `TF_VAR_stage` is empty on main and `pr-<n>` for a preview.
`docs/site` stage (custom domain + CNAME). The Pages project and the uploaded
preview deployment itself are left in place.
Both run `mise run docs:deploy` / `mise run docs:destroy`, so an operator can
Both run `mise run //packages/docs:deploy` / `mise run //packages/docs:destroy`, so an operator can
do the same locally.
## Prerequisites
@@ -45,9 +45,9 @@ CI uses the existing `OP_TF_YUCCA_PROD_ENV` (main) and `OP_TF_YUCCA_STAGING_ENV`
## Running locally
```bash
mise docs:build
ENVIRONMENT=dev TF_VAR_stage=pr-0 mise docs:deploy # https://docs.pr-0.dev.futo.cloud
ENVIRONMENT=dev TF_VAR_stage=pr-0 mise docs:destroy
mise //packages/docs:build
ENVIRONMENT=dev TF_VAR_stage=pr-0 mise //packages/docs:deploy # https://docs.pr-0.dev.futo.cloud
ENVIRONMENT=dev TF_VAR_stage=pr-0 mise //packages/docs:destroy
OP_ENV_FILE=tf/pages/.env ENVIRONMENT=prod tf/op-run.sh terragrunt run --all plan --working-dir tf/pages/docs
```