mirror of
https://github.com/immich-app/yucca.git
synced 2026-09-30 13:33:00 +08:00
feat(web): connections ui (#415)
This commit is contained in:
@@ -114,9 +114,10 @@ to victoria-*).
|
||||
| `yucca-admin-api` | NestJS | Admin API (user/session/repository management). Shares the same DB + JWT validation. |
|
||||
| `michael` | Go | **Production** restic REST backend — S3 proxy implementing restic's HTTP protocol, with JWT (ECDSA pubkey) verification, WORM enforcement, multi-backend pool/DNS load-balancing. Deployed in k8s (`kubernetes/apps/base/michael`). |
|
||||
| `restic-api` | NestJS | Earlier TypeScript implementation of the same restic backend, kept as a **reference** (`mise restic-api:dev-reference`); not in the deployed app set. |
|
||||
| `yucca-metrics-worker` | NestJS | Cron worker (every 5 min): reads bucket usage from RadosGW, writes meter tables, emits OTel gauges. |
|
||||
| `yucca-metrics-worker` | NestJS | Cron worker (every 5 min): reads bucket usage from RadosGW, writes meter tables, **rolls usage up per connection into `connectionMetrics` (with the per-type billing floor)**, emits OTel gauges. |
|
||||
| `redis` (valkey) | | **Generic shared platform cache** (ephemeral by design; keys namespaced `yucca:<service>:<purpose>:*`). First tenant: michael's restic-token **verdict cache** (`yucca:michael:verdict:<jti>`, DELed by the APIs on revoke); future: michael rate limiting. In-repo chart `charts/apps/redis`; primary-region only. |
|
||||
| `mock-oidc-provider` | Node | Dev/test OIDC IdP (code + device flow). Used by compose and k3d when no real issuer is configured. |
|
||||
| `common` (`@common/server`) | TS lib | Shared OTel init, pino logger repository, logging interceptor. |
|
||||
| `common` (`@common/server`) | TS lib | Shared OTel init, pino logger repository, logging interceptor, **the feature-flag registry (`FeatureFlags`) and connection types (`ConnectionTypes`)**. |
|
||||
|
||||
**Frontend** (`packages/web`) is **SvelteKit 5 + Tailwind 4**, using `@immich/ui`, lingui i18n
|
||||
(`mise web:lingui:*` to extract/compile — compiled locales are generated, not edited), and the
|
||||
@@ -134,6 +135,51 @@ generated API client. It also embeds the orchestration UI (`@futo-org/backups-or
|
||||
consumed by web). `fetch-client.ts` is generated (eslint-ignored). When you change an API
|
||||
contract, regenerate rather than editing the client.
|
||||
|
||||
### Connections, feature flags, and restic-token revocation
|
||||
|
||||
- **Connections** (`connections` table) make "what backs up this account" first-class: a user has N
|
||||
connection instances of type `immich` or `restic`. Every repository has a `connectionId`
|
||||
(NOT NULL); device-flow sessions bind to a connection via `?connection_type=&connection_name=` on
|
||||
`/auth/oidc/device`. Existing repos were backfilled onto a default `immich` connection; instance
|
||||
attribution is client-driven via `POST /connections/:id/adopt` (moves default-connection repos to a
|
||||
named instance), never guessed server-side. The in-repo orchestrator (`yucca-sdk`) does this on
|
||||
device-flow login: it registers as an `immich` connection named after its external host, then
|
||||
best-effort adopts its existing repositories onto that instance. The `/connections` API surface
|
||||
(list, create, adopt, manage, including multiple `immich` instances) is open to **every**
|
||||
authenticated user.
|
||||
- **Feature flags** = registry in code (`@common/server` `FeatureFlags`), strict-boolean per-user
|
||||
overrides in `userFeatureFlagOverride`. Resolution is `override ?? registry default`; the default
|
||||
flips at GA via a release (code-only defaults). Flags gate self-service use of the individual
|
||||
non-default connection *type*, not the whole surface: `connection-restic`
|
||||
(`experimental`, default off): `immich` needs none. The mapping lives in `@common/server`
|
||||
`ConnectionTypeFlags`/`connectionTypeFlag()`, checked in `ConnectionService.create` and the device
|
||||
flow; admin-provisioned connections bypass it (admin authority). `@RequireFeature` remains as the
|
||||
generic route-level guard for future whole-route gating. Manage from yuctl: `users features
|
||||
set/clear`, `features enable-batch`. **Boundary rule:** env/cluster-settings = deployment config
|
||||
(ops-owned, per-partition); feature flags = per-user product gating (admin-owned, runtime).
|
||||
- **Per-type descriptor + billing** live in the code registry (`@common/server` `ConnectionTypeInfos`):
|
||||
each type declares its metering tiers, `reportsActivity`, `minObjectSizeBytes` (billing floor), and
|
||||
`revocable`. Billing keys off the always-available **storage** tier: `yucca-metrics-worker` rolls each
|
||||
connection's per-repo RadosGW readings up into `connectionMetrics` and computes
|
||||
`billableBytes(type, size, objects) = max(size, objects * minObjectSizeBytes)` (immich floor 0; non-immich
|
||||
1 MiB, an aggregate approximation, RadosGW gives no per-object histogram). `GET /connections` returns the
|
||||
rollup. **Self-serve restic** (flagged): `POST /connections/restic` creates connection+repo+long-lived URL
|
||||
in one shot; `POST /repository/:id/restic` mints for an existing repo, **long-lived is revocable-only**
|
||||
(restic: default `RESTIC_JWT_EXPIRES_IN` 90d, `expiresIn` capped by `RESTIC_JWT_MAX_EXPIRES_IN` 365d;
|
||||
immich: short `JWT_EXPIRES_IN` lifetime, custom `expiresIn` rejected: michael never validity-checks
|
||||
non-revocable types, so they must not be long-lived); `GET /repository/:id/restic-tokens` +
|
||||
`DELETE /restic-tokens/:jti` are owner-scoped. See `docs/connections.md`.
|
||||
- **Restic-token revocation** = **postgres truth + layered caches, bounded grace**. michael checks a token's
|
||||
liveness (revocable types only: `REVOCABLE_CONNECTION_TYPES`, default `restic`; immich is skipped) through:
|
||||
L1 per-process cache (`REVOCATION_FRESH_TTL_MS` 60s fresh / `REVOCATION_GRACE_MS` 30min grace) → shared
|
||||
valkey **verdict cache** (`yucca:michael:verdict:<jti>`, `VERDICT_CACHE_TTL_MS` 5min, read-through; errors
|
||||
fall through) → yucca-api's internal introspection endpoint (`GET /internal/restic-tokens/:jti`, shared
|
||||
secret `TOKEN_INTROSPECTION_SECRET`, answers `{active}` from `resticTokens`). Mint writes only the DB row;
|
||||
revoke flips the row then best-effort **DELs the L2 key** (lands within ~L1 fresh; a missed DEL self-heals
|
||||
via L2 TTL, no reconcile job). Valkey restart/outage = cache miss → postgres, harmless. Introspection outage:
|
||||
previously-valid jtis honored for the grace window, then **fail closed**. Enforced only where
|
||||
`TOKEN_INTROSPECTION_URL` is set (primary regions). yuctl: `tokens list/revoke`, `repos url --ttl`.
|
||||
|
||||
### Database
|
||||
|
||||
PostgreSQL accessed via **Kysely**. The schema lives in `packages/yucca-api/src/schema/`
|
||||
@@ -188,6 +234,11 @@ release tag into both prod pins (`kubernetes/clusters/prod/htz-fsn1/{flux-releas
|
||||
trailing commas, width 120.
|
||||
- Generated files are eslint-ignored: `**/fetch-client.ts`, `packages/web/src/locales`, `dist`,
|
||||
`build`, `.svelte-kit`.
|
||||
- **Default to zero comments.** No narration, no restating what the code already says; make the
|
||||
code self-explanatory instead. Add a comment only in the rare case it captures something the
|
||||
code cannot (a why, a constraint, a gotcha) that a reader would otherwise miss.
|
||||
- **Match the package you are in.** Read the surrounding code first and follow its existing
|
||||
style and patterns; write code that looks like what is already there, not your own conventions.
|
||||
|
||||
### Naming
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# Connections
|
||||
|
||||
A **connection** is what backs up a user's account. It makes "what is using this
|
||||
account" first-class, so usage can be attributed and billed per source and so a
|
||||
user can run more than one backup client against one account.
|
||||
|
||||
```
|
||||
User ──1:N──> Connection ──1:N──> Repository ──1:1──> S3 bucket (via michael)
|
||||
```
|
||||
|
||||
- **User**, the account and **billing unit** (plan/quota live here).
|
||||
- **Connection**, attribution + capability + billing-rollup unit. Carries a
|
||||
`type`; device-flow sessions bind to one; usage rolls up here.
|
||||
- **Repository**, the restic repo / bucket; per-repo metering happens here
|
||||
(`repositoryMeter`). Every repository has a NOT-NULL `connectionId`.
|
||||
|
||||
Connection→Repository is 1:N (a restic connection is reused across repositories);
|
||||
a repository belongs to exactly one connection. `POST /connections/:id/adopt`
|
||||
re-parents a repository that still sits on the user's **default** connection.
|
||||
|
||||
## Types (a code registry)
|
||||
|
||||
The set of connection types and their behavior is **code**, not data, only
|
||||
per-user/instance state is data. The descriptor lives in
|
||||
`packages/common/src/connections.ts` (`ConnectionTypeInfos`), exported from
|
||||
`@common/server`. Adding a type is a one-object change there.
|
||||
|
||||
| Type | Metering tiers | Reports activity | Min object size | Revocable | Self-serve flag |
|
||||
|---|---|---|---|---|---|
|
||||
| `immich` | storage, transfer, activity | yes | 0 | no | none (always on) |
|
||||
| `restic` | storage, transfer | no | 1 MiB | yes | `connection-restic` |
|
||||
| `s3` *(future)* | storage | no | 1 MiB | yes |, |
|
||||
|
||||
### Metering tiers
|
||||
|
||||
Billing keys off **storage**, the only universal tier.
|
||||
|
||||
| Tier | Source | immich | restic | s3 |
|
||||
|---|---|---|---|---|
|
||||
| **Storage** (bytes, objects) → **billed** | RadosGW | ✅ | ✅ | ✅ |
|
||||
| Transfer | michael | ✅ | ✅ | ❌ |
|
||||
| Activity (backup start/end) | client | ✅ | ❌ | ❌ |
|
||||
|
||||
## Billing rollup
|
||||
|
||||
`yucca-metrics-worker` meters each repository from RadosGW every 5 minutes, then
|
||||
rolls the readings up per connection into the `connectionMetrics` table
|
||||
(`sizeBytes`, `objectCount`, `billableBytes`, `repositoryCount`). `GET /connections`
|
||||
returns the rollup per connection.
|
||||
|
||||
**Billable bytes** apply a per-type object-size floor via
|
||||
`billableBytes(type, sizeBytes, objectCount)`:
|
||||
|
||||
```
|
||||
billableBytes = max(sizeBytes, objectCount * minObjectSizeBytes)
|
||||
```
|
||||
|
||||
immich is exempt (floor 0 → billed at raw size). Non-immich types bill each
|
||||
object at a minimum of 1 MiB. RadosGW exposes only total size + object count (no
|
||||
per-object histogram), so this is an **aggregate approximation** of
|
||||
`Σ max(objectSize_i, 1 MiB)`, it under-counts a repo that mixes large and small
|
||||
objects, but restic writes large pack files so `sizeBytes` dominates and the
|
||||
floor only bites for tiny/new repos or many-small-object raw-S3, the intended
|
||||
cases. Exact per-object billing (S3 `ListObjects`) is a documented future option.
|
||||
|
||||
*(This produces billable-bytes only. Pricing/plan/quota is a separate later layer.)*
|
||||
|
||||
## Revocation: postgres truth, layered caches, bounded grace
|
||||
|
||||
restic tokens are long-lived, so their liveness is checked against the **source
|
||||
of truth, postgres** (`resticTokens`), fronted by yucca-api's internal
|
||||
introspection endpoint and two cache layers in michael:
|
||||
|
||||
```
|
||||
michael request ──> L1 (per-process, fresh 60s / grace 30min)
|
||||
└miss──> L2 (shared valkey, yucca:michael:verdict:<jti>, TTL 5min)
|
||||
└miss/error──> GET yucca-api /internal/restic-tokens/:jti (postgres)
|
||||
```
|
||||
|
||||
- **Introspection** (`GET /internal/restic-tokens/:jti`, shared-secret header
|
||||
`X-Introspection-Secret`) answers `{active}`: minted, unrevoked, unexpired,
|
||||
**owner enabled** (disabling an account kills its credentials; re-enabling
|
||||
restores unexpired ones). Unknown, malformed, revoked, expired, and
|
||||
disabled-owner jtis all answer `active:false`.
|
||||
The route is **unreachable from the public internet**: the gateway
|
||||
short-circuits `/api/internal/*` to a bare 404 (`HTTPRouteFilter
|
||||
internal-404` shadowing the `/api` rule), so only pod-to-pod traffic,
|
||||
admitted by `allow-ingress-yucca-api`, ever reaches it; the shared secret
|
||||
is the second wall, not the only one.
|
||||
- **Mint** writes only the postgres row, no cache writes; the first request
|
||||
populates the caches read-through.
|
||||
- **Revoke** flips the DB row, then best-effort **DELs the L2 verdict key**,
|
||||
the revoke lands on every michael replica within ~the L1 fresh TTL
|
||||
(`REVOCATION_FRESH_TTL_MS`, default 60 s). A *missed* DEL self-heals when the
|
||||
L2 entry's TTL (`VERDICT_CACHE_TTL_MS`, default 5 min) lapses and the next
|
||||
miss re-asks postgres, no reconcile job exists or is needed.
|
||||
- **Valkey restart/outage is harmless**: L2 is pure cache, a miss or error
|
||||
falls through to introspection. (This is why the old marker model's
|
||||
restart-deny-window and reconcile cron are gone.)
|
||||
- **Introspection outage** (yucca-api/postgres unreachable): michael keeps
|
||||
honoring a *previously-valid* jti until a bounded **grace** window elapses
|
||||
(`REVOCATION_GRACE_MS`, default 30 min), then fails **closed**. The horizon is
|
||||
anchored to the last *authoritative* confirmation (L2 hits carry the entry's
|
||||
age via PTTL), so 30 min is a true end-to-end bound; repeated failures are
|
||||
also debounced (a short backoff gates introspection dials, so an outage never
|
||||
turns restic's request concurrency into a control-plane storm). A jti never
|
||||
confirmed valid is denied immediately, bounded grace, then deny.
|
||||
- michael **skips** the check entirely for non-revocable types (immich, whose
|
||||
access rides the device-flow session): `REVOCABLE_CONNECTION_TYPES` (default
|
||||
`restic`) mirrors the descriptor's `revocable` set.
|
||||
|
||||
michael enforces validity only where `TOKEN_INTROSPECTION_URL` is set (primary
|
||||
regions, secondaries have no local yucca-api and run with checking off). The
|
||||
valkey is the **generic shared platform cache** (`charts/apps/redis`, ephemeral
|
||||
by design, keys namespaced `yucca:<service>:<purpose>:*`); the verdict cache is
|
||||
its first tenant, with michael rate limiting a likely second.
|
||||
|
||||
## Self-serve restic
|
||||
|
||||
A user with the `connection-restic` flag can stand up a restic backup in one call:
|
||||
|
||||
- **`POST /connections/restic`**, get-or-create the user's restic connection,
|
||||
create a repository under it, mint a **long-lived** rest: URL, and return
|
||||
`{ connection, repository, url, jti, expiresAt }`. Idempotent on the connection
|
||||
(reused across repositories). Gated on `connection-restic` (403 without).
|
||||
- **`POST /repository/:id/restic`**, mint a URL for an existing repository.
|
||||
Optional `expiresIn` and `label`. **Long-lived tokens are revocable-only**: for
|
||||
restic repositories the default is `RESTIC_JWT_EXPIRES_IN` (90d), capped at
|
||||
`RESTIC_JWT_MAX_EXPIRES_IN` (365d); for non-revocable types (immich, michael
|
||||
never validity-checks them) the token keeps the short session-JWT lifetime
|
||||
(`JWT_EXPIRES_IN`, 1d) and a custom `expiresIn` is rejected.
|
||||
- **`GET /repository/:id/restic-tokens`**, list a repository's minted tokens
|
||||
(owner-scoped).
|
||||
- **`DELETE /restic-tokens/:jti`**, revoke your own token (owner-scoped; unknown
|
||||
or other-owner jtis 404 identically, so ownership isn't leaked). Invalidates
|
||||
michael's cached verdict.
|
||||
|
||||
The `/connections` surface (list, create, adopt, manage) is open to **every**
|
||||
authenticated user; the individual non-default *type* is flag-gated on every
|
||||
**credential-creating** operation, creating a connection of the type, creating
|
||||
a repository under one, and minting a URL, so a `false` override is a real
|
||||
kill-switch for new self-service credentials (existing tokens keep working until
|
||||
revoked or expired; **revoke and list are deliberately never gated**). Admin
|
||||
provisioning (`yucca-admin-api`, yuctl) bypasses the flag (admin authority).
|
||||
|
||||
## Web UI
|
||||
|
||||
The `Connections` dashboard page (`packages/web/src/routes/dashboard/connections/`)
|
||||
lists each connection with its type and usage rollup. It's a thin SvelteKit route:
|
||||
`+page.ts` loads `listConnections` + `getRepositories` via the generated client, and
|
||||
`+page.svelte` renders it with `@immich/ui`.
|
||||
|
||||
**Restic self-serve is invisible without the flag.** The "New restic backup" button and
|
||||
all restic actions render only when `data.user.features['connection-restic']` is true,
|
||||
absent from the DOM otherwise, not merely disabled. The create flow (`CreateResticModal`)
|
||||
calls `POST /connections/restic` and opens a result modal (`ResticResultModal`) showing the
|
||||
`rest:` URL, a `restic -r … init` snippet (copy buttons), and a repository-password
|
||||
reminder. Per-repository access keys are listed/revoked/re-minted in `ManageTokensModal`.
|
||||
|
||||
## Where things live
|
||||
|
||||
| Concern | Location |
|
||||
|---|---|
|
||||
| Type descriptor + billing floor | `packages/common/src/connections.ts` |
|
||||
| Schema (`connections`, `connectionMetrics`, `resticTokens`) | `packages/yucca-api/src/schema/` |
|
||||
| Connection + self-serve restic API | `packages/yucca-api/src/{controllers,services}/` |
|
||||
| Web Connections page + restic modals | `packages/web/src/routes/dashboard/connections/`, `packages/web/src/lib/components/connections/` |
|
||||
| Billing rollup | `packages/yucca-metrics-worker/` |
|
||||
| Validity check (michael: L1/L2/introspection) | `packages/michael/internal/revocation/` |
|
||||
| Introspection endpoint | `packages/yucca-api/src/controllers/introspection.controller.ts` |
|
||||
| Admin provisioning | `packages/yucca-admin-api/`, `packages/yuctl/` |
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
const login = async (page: import('@playwright/test').Page) => {
|
||||
await page.goto('http://localhost:36033/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Sign-in' })).toBeVisible();
|
||||
await page.getByPlaceholder('Enter any login').fill('foo');
|
||||
await page.getByPlaceholder('and password').fill('password');
|
||||
await page.getByRole('button', { name: 'Sign-in' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Authorize' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
await expect(page.getByText('Backup Health')).toBeVisible();
|
||||
};
|
||||
|
||||
test('connections page lists the default connection and hides restic without the flag', async ({
|
||||
page,
|
||||
}) => {
|
||||
await login(page);
|
||||
|
||||
await page.getByRole('link', { name: 'Connections' }).click();
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Connections' }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(page.getByText('Immich').first()).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'New restic backup' }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { mdiImageMultiple } from '@mdi/js';
|
||||
|
||||
export type ConnectionTypeMeta = {
|
||||
type: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
limitation: string;
|
||||
addable: boolean;
|
||||
};
|
||||
|
||||
export const CONNECTION_TYPES: ConnectionTypeMeta[] = [
|
||||
{
|
||||
type: 'immich',
|
||||
label: 'Immich',
|
||||
icon: mdiImageMultiple,
|
||||
description: 'Back up an Immich instance: photos, videos and the database.',
|
||||
limitation:
|
||||
'Added automatically when you connect FUTO Backups from the Immich app; not created here.',
|
||||
addable: false,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
import { toastManager } from '@immich/ui';
|
||||
|
||||
export const handleError = (error: unknown, fallback: string) => {
|
||||
console.error(error);
|
||||
|
||||
let message = fallback;
|
||||
const data = (error as { data?: unknown } | undefined)?.data;
|
||||
const raw = typeof data === 'string' ? tryParse(data) : data;
|
||||
if (raw && typeof raw === 'object' && 'message' in raw) {
|
||||
const m = (raw as { message?: unknown }).message;
|
||||
if (typeof m === 'string' && m.length > 0) {
|
||||
message = m.length > 120 ? m.slice(0, 117) + '...' : m;
|
||||
}
|
||||
}
|
||||
|
||||
toastManager.danger(message);
|
||||
};
|
||||
|
||||
const tryParse = (value: string): unknown => {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
@@ -13,32 +13,211 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "{0} objects"
|
||||
#~ msgstr "{0} objects"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "{0} repositories"
|
||||
msgstr "{0} repositories"
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#~ msgid "{num, plural, one {# item} other {# items}}"
|
||||
#~ msgstr "{num, plural, one {# item} other {# items}}"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "1 year"
|
||||
#~ msgstr "1 year"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "30 days"
|
||||
#~ msgstr "30 days"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "90 days"
|
||||
#~ msgstr "90 days"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "A connection is a client that backs up this account — an Immich instance or a restic repository."
|
||||
#~ msgstr "A connection is a client that backs up this account — an Immich instance or a restic repository."
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "A label for this repository."
|
||||
#~ msgstr "A label for this repository."
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access key"
|
||||
#~ msgstr "Access key"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Access key lifetime"
|
||||
#~ msgstr "Access key lifetime"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access key revoked"
|
||||
#~ msgstr "Access key revoked"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "Access keys"
|
||||
#~ msgstr "Access keys"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access keys — {0}"
|
||||
#~ msgstr "Access keys — {0}"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access keys: {0}"
|
||||
#~ msgstr "Access keys: {0}"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "Add {0} connection"
|
||||
#~ msgstr "Add {0} connection"
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "Automatic"
|
||||
#~ msgstr "Automatic"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "billed"
|
||||
msgstr "billed"
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "Cancel"
|
||||
#~ msgstr "Cancel"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Choose a strong repository password"
|
||||
#~ msgstr "Choose a strong repository password"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Close"
|
||||
#~ msgstr "Close"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "Connections"
|
||||
msgstr "Connections"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "Connections are the sources of data that back up to us — right now, that's mostly Immich."
|
||||
#~ msgstr "Connections are the sources of data that back up to us — right now, that's mostly Immich."
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "Connections are the sources of data that back up to us. Right now, that's mostly Immich."
|
||||
msgstr "Connections are the sources of data that back up to us. Right now, that's mostly Immich."
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Continue"
|
||||
msgstr "Continue"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copied"
|
||||
#~ msgstr "Copied"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copied to clipboard"
|
||||
#~ msgstr "Copied to clipboard"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copy"
|
||||
#~ msgstr "Copy"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copy command"
|
||||
#~ msgstr "Copy command"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copy URL"
|
||||
#~ msgstr "Copy URL"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Couldn't copy — select the text manually"
|
||||
#~ msgstr "Couldn't copy — select the text manually"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Couldn't copy. Select the text manually"
|
||||
#~ msgstr "Couldn't copy. Select the text manually"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Create"
|
||||
#~ msgstr "Create"
|
||||
|
||||
#: src/routes/dashboard/backups/+page.svelte
|
||||
#~ msgid "Create new backup"
|
||||
#~ msgstr "Create new backup"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Created {0} · expires {1}"
|
||||
#~ msgstr "Created {0} · expires {1}"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Done"
|
||||
#~ msgstr "Done"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "e.g. laptop"
|
||||
#~ msgstr "e.g. laptop"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "e.g. my laptop"
|
||||
#~ msgstr "e.g. my laptop"
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Enter an invite code to continue."
|
||||
msgstr "Enter an invite code to continue."
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Expired"
|
||||
#~ msgstr "Expired"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Failed to create restic backup"
|
||||
#~ msgstr "Failed to create restic backup"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Failed to load access keys"
|
||||
#~ msgstr "Failed to load access keys"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Failed to mint a new access key"
|
||||
#~ msgstr "Failed to mint a new access key"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Failed to revoke access key"
|
||||
#~ msgstr "Failed to revoke access key"
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#~ msgid "From the {0}: {1}"
|
||||
#~ msgstr "From the {0}: {1}"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "How long the URL stays valid (max 1 year). Revoke the key any time to end access sooner."
|
||||
#~ msgstr "How long the URL stays valid (max 1 year). Revoke the key any time to end access sooner."
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "How long the URL stays valid, e.g. 90d. Blank uses the default."
|
||||
#~ msgstr "How long the URL stays valid, e.g. 90d. Blank uses the default."
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "How long the URL stays valid. \"No expiry\" relies on revoking the key instead."
|
||||
#~ msgstr "How long the URL stays valid. \"No expiry\" relies on revoking the key instead."
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Initialize the repository"
|
||||
#~ msgstr "Initialize the repository"
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Invite code"
|
||||
msgstr "Invite code"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Key label"
|
||||
#~ msgstr "Key label"
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#: src/routes/+page.svelte
|
||||
msgid "Login"
|
||||
@@ -48,10 +227,92 @@ msgstr "Login"
|
||||
msgid "Logout"
|
||||
msgstr "Logout"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Name"
|
||||
#~ msgstr "Name"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "New access key"
|
||||
#~ msgstr "New access key"
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "New connection"
|
||||
#~ msgstr "New connection"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "New restic backup"
|
||||
#~ msgstr "New restic backup"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "New restic repository"
|
||||
#~ msgstr "New restic repository"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "No access keys yet."
|
||||
#~ msgstr "No access keys yet."
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "No connections yet."
|
||||
#~ msgstr "No connections yet."
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "No expiry"
|
||||
#~ msgstr "No expiry"
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "Pick what you want to back up. Each connection type works a little differently."
|
||||
#~ msgstr "Pick what you want to back up. Each connection type works a little differently."
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Prevent this repository from being deleted or overwritten."
|
||||
#~ msgstr "Prevent this repository from being deleted or overwritten."
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#~ msgid "Purchase Immich"
|
||||
#~ msgstr "Purchase Immich"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Repository"
|
||||
#~ msgstr "Repository"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Repository URL"
|
||||
#~ msgstr "Repository URL"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Restic backup ready"
|
||||
#~ msgstr "Restic backup ready"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "restic will ask you to set a password when you initialize. Keep it safe — it encrypts your backups and cannot be recovered."
|
||||
#~ msgstr "restic will ask you to set a password when you initialize. Keep it safe — it encrypts your backups and cannot be recovered."
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "restic will ask you to set a password when you initialize. Keep it safe: it encrypts your backups and cannot be recovered."
|
||||
#~ msgstr "restic will ask you to set a password when you initialize. Keep it safe: it encrypts your backups and cannot be recovered."
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Revoke"
|
||||
#~ msgstr "Revoke"
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Revoked"
|
||||
#~ msgstr "Revoked"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Shown in the access-key list."
|
||||
#~ msgstr "Shown in the access-key list."
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "This connection type can't be added from here — see the note above."
|
||||
#~ msgstr "This connection type can't be added from here — see the note above."
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Write-once (WORM)"
|
||||
#~ msgstr "Write-once (WORM)"
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Your email isn't part of the beta yet."
|
||||
msgstr "Your email isn't part of the beta yet."
|
||||
|
||||
@@ -13,32 +13,211 @@ msgstr ""
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "{0} objects"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "{0} repositories"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#~ msgid "{num, plural, one {# item} other {# items}}"
|
||||
#~ msgstr "{num, plural, one {# hello} other {# hellos}}"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "1 year"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "30 days"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "90 days"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "A connection is a client that backs up this account — an Immich instance or a restic repository."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "A label for this repository."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access key"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Access key lifetime"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access key revoked"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "Access keys"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access keys — {0}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Access keys: {0}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "Add {0} connection"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "Automatic"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "billed"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "Cancel"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Choose a strong repository password"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Close"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "Connections"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "Connections are the sources of data that back up to us — right now, that's mostly Immich."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
msgid "Connections are the sources of data that back up to us. Right now, that's mostly Immich."
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Continue"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copied"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copied to clipboard"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copy"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copy command"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Copy URL"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Couldn't copy — select the text manually"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Couldn't copy. Select the text manually"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Create"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/backups/+page.svelte
|
||||
#~ msgid "Create new backup"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Created {0} · expires {1}"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Done"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "e.g. laptop"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "e.g. my laptop"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Enter an invite code to continue."
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Expired"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Failed to create restic backup"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Failed to load access keys"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Failed to mint a new access key"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Failed to revoke access key"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#~ msgid "From the {0}: {1}"
|
||||
#~ msgstr "{0}! {1}?"
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "How long the URL stays valid (max 1 year). Revoke the key any time to end access sooner."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "How long the URL stays valid, e.g. 90d. Blank uses the default."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "How long the URL stays valid. \"No expiry\" relies on revoking the key instead."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Initialize the repository"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Invite code"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Key label"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#: src/routes/+page.svelte
|
||||
msgid "Login"
|
||||
@@ -48,10 +227,92 @@ msgstr ""
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Name"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "New access key"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "New connection"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "New restic backup"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "New restic repository"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "No access keys yet."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/dashboard/connections/+page.svelte
|
||||
#~ msgid "No connections yet."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "No expiry"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "Pick what you want to back up. Each connection type works a little differently."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Prevent this repository from being deleted or overwritten."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/+page.svelte
|
||||
#~ msgid "Purchase Immich"
|
||||
#~ msgstr "Hello Immich"
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Repository"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Repository URL"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "Restic backup ready"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "restic will ask you to set a password when you initialize. Keep it safe — it encrypts your backups and cannot be recovered."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ResticResultModal.svelte
|
||||
#~ msgid "restic will ask you to set a password when you initialize. Keep it safe: it encrypts your backups and cannot be recovered."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Revoke"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/ManageTokensModal.svelte
|
||||
#~ msgid "Revoked"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Shown in the access-key list."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/NewConnectionModal.svelte
|
||||
#~ msgid "This connection type can't be added from here — see the note above."
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/lib/components/connections/CreateResticModal.svelte
|
||||
#~ msgid "Write-once (WORM)"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/login/invite/+page.svelte
|
||||
msgid "Your email isn't part of the beta yet."
|
||||
msgstr ""
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
HStack,
|
||||
NavbarItem,
|
||||
} from "@immich/ui";
|
||||
import { mdiBackupRestore, mdiViewDashboard } from "@mdi/js";
|
||||
import { mdiBackupRestore, mdiConnection, mdiViewDashboard } from "@mdi/js";
|
||||
import {
|
||||
setProvider,
|
||||
yuccaApiProvider,
|
||||
@@ -53,6 +53,12 @@
|
||||
icon={mdiBackupRestore}
|
||||
active={page.url.pathname === "/dashboard/backups"}
|
||||
/>
|
||||
<NavbarItem
|
||||
title="Connections"
|
||||
href="/dashboard/connections"
|
||||
icon={mdiConnection}
|
||||
active={page.url.pathname === "/dashboard/connections"}
|
||||
/>
|
||||
</div>
|
||||
</AppShellSidebar>
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import { CONNECTION_TYPES } from "$lib/components/connections/connection-types";
|
||||
import { type ConnectionDto } from "@futo-org/backups-api-client";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
FormatBytes,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@immich/ui";
|
||||
import { t } from "svelte-i18n-lingui";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
const connectionsByType = $derived.by(() => {
|
||||
const map = new Map<string, ConnectionDto[]>();
|
||||
for (const connection of data.connections) {
|
||||
const list = map.get(connection.type) ?? [];
|
||||
list.push(connection);
|
||||
map.set(connection.type, list);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const typeColor = (type: string) =>
|
||||
type === "immich" ? "primary" : "success";
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{$t`Connections`} · FUTO Backups</title></svelte:head
|
||||
>
|
||||
|
||||
<Stack gap={5}>
|
||||
<Heading tag="h1" size="small">{$t`Connections`}</Heading>
|
||||
|
||||
<Text color="muted"
|
||||
>{$t`Connections are the sources of data that back up to us. Right now, that's mostly Immich.`}</Text
|
||||
>
|
||||
|
||||
{#each CONNECTION_TYPES as meta (meta.type)}
|
||||
{@const connections = connectionsByType.get(meta.type) ?? []}
|
||||
<Stack gap={2}>
|
||||
<HStack gap={2}>
|
||||
<Icon icon={meta.icon} />
|
||||
<Heading size="tiny">{meta.label}</Heading>
|
||||
</HStack>
|
||||
|
||||
{#if connections.length === 0}
|
||||
<Text size="small" color="muted">{meta.limitation}</Text>
|
||||
{/if}
|
||||
|
||||
{#each connections as connection (connection.id)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<HStack class="justify-between">
|
||||
<HStack gap={2}>
|
||||
<CardTitle>{connection.name}</CardTitle>
|
||||
<Badge color={typeColor(connection.type)}
|
||||
>{connection.type}</Badge
|
||||
>
|
||||
</HStack>
|
||||
<Text size="small" color="muted">
|
||||
{$t`${connection.repositoryCount} repositories`} ·
|
||||
<FormatBytes bytes={connection.billableBytes} />
|
||||
{$t`billed`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
{/each}
|
||||
</Stack>
|
||||
{/each}
|
||||
</Stack>
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { getRepositories, listConnections } from '@futo-org/backups-api-client';
|
||||
|
||||
export const load: PageLoad = async ({ fetch }) => {
|
||||
const [connections, repositories] = await Promise.all([
|
||||
listConnections({ fetch }),
|
||||
getRepositories({ fetch }),
|
||||
]);
|
||||
return {
|
||||
connections: connections.connections,
|
||||
repositories: repositories.repositories,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user