diff --git a/apps/backend/cmd/preview/sprite_ops.go b/apps/backend/cmd/preview/sprite_ops.go index 92e4c8914f..900392fe24 100644 --- a/apps/backend/cmd/preview/sprite_ops.go +++ b/apps/backend/cmd/preview/sprite_ops.go @@ -182,6 +182,7 @@ cd /app export KANDEV_HOME_DIR=/data export KANDEV_DOCKER_ENABLED=false export KANDEV_LOG_LEVEL=info +export KANDEV_WEB_TITLE_PREFIX=Preview export KANDEV_WEB_DIST_DIR=/app/apps/web/dist # Preview mode: keep the mock agent while loading the built-in agent catalogue # so Settings can show agents that are available to install. diff --git a/apps/backend/cmd/preview/sprite_ops_test.go b/apps/backend/cmd/preview/sprite_ops_test.go index 76af022cbe..dd4e51e29d 100644 --- a/apps/backend/cmd/preview/sprite_ops_test.go +++ b/apps/backend/cmd/preview/sprite_ops_test.go @@ -154,6 +154,9 @@ func TestBuildExtractScript(t *testing.T) { if !strings.Contains(script, "KANDEV_WEB_DIST_DIR=/app/apps/web/dist") { t.Errorf("expected KANDEV_WEB_DIST_DIR to point at packaged Vite dist") } + if !strings.Contains(script, "KANDEV_WEB_TITLE_PREFIX=Preview") { + t.Errorf("expected preview title prefix in script") + } if !strings.Contains(script, "ln -sf /app/apps/backend/bin/kandev /usr/local/bin/kandev") { t.Errorf("expected native kandev symlink in script") } diff --git a/apps/backend/internal/profiles/profiles.yaml b/apps/backend/internal/profiles/profiles.yaml index 7062db83bd..5f5c6c3c9d 100644 --- a/apps/backend/internal/profiles/profiles.yaml +++ b/apps/backend/internal/profiles/profiles.yaml @@ -158,6 +158,15 @@ debug: dev: "true" e2e: "" +# ── Browser identity ───────────────────────────────────────────── +# Distinguish the local development tab from release and preview tabs without +# changing the title of production or e2e environments. +ui: + KANDEV_WEB_TITLE_PREFIX: + prod: "" + dev: "Dev" + e2e: "" + # ── E2E test tuning ─────────────────────────────────────────────── # Behaviour knobs that only matter when running playwright. These # don't belong in dev (would mask real interactions) and never in diff --git a/apps/backend/internal/profiles/profiles_test.go b/apps/backend/internal/profiles/profiles_test.go index aca687493c..1e4c5769ed 100644 --- a/apps/backend/internal/profiles/profiles_test.go +++ b/apps/backend/internal/profiles/profiles_test.go @@ -52,6 +52,9 @@ func TestApplyProfile_DefaultsToProd(t *testing.T) { "false", ) } + if _, ok := os.LookupEnv("KANDEV_WEB_TITLE_PREFIX"); ok { + t.Errorf("KANDEV_WEB_TITLE_PREFIX is set in prod; want it unset") + } } // TestApplyProfile_DevUsesDevelopmentDefaults verifies the mixed dev profile: @@ -86,6 +89,23 @@ func TestApplyProfile_DevUsesDevelopmentDefaults(t *testing.T) { "false", ) } + if v := os.Getenv("KANDEV_WEB_TITLE_PREFIX"); v != "Dev" { + t.Errorf("KANDEV_WEB_TITLE_PREFIX = %q in dev; want %q", v, "Dev") + } +} + +func TestApplyProfile_ExplicitTitlePrefixWinsInDev(t *testing.T) { + clearProfileSelectors(t) + clearProfilesYAMLVars(t) + t.Setenv("KANDEV_DEBUG_DEV_MODE", "true") + t.Setenv("KANDEV_WEB_TITLE_PREFIX", "Custom") + + if _, _, err := ApplyProfile(); err != nil { + t.Fatalf("ApplyProfile: %v", err) + } + if v := os.Getenv("KANDEV_WEB_TITLE_PREFIX"); v != "Custom" { + t.Errorf("ApplyProfile overwrote explicit title prefix: got %q; want %q", v, "Custom") + } } // TestApplyProfile_E2EWinsOverDev checks the documented detection @@ -118,6 +138,9 @@ func TestApplyProfile_E2EWinsOverDev(t *testing.T) { "false", ) } + if _, ok := os.LookupEnv("KANDEV_WEB_TITLE_PREFIX"); ok { + t.Errorf("KANDEV_WEB_TITLE_PREFIX is set in e2e; want it unset") + } } // TestApplyProfile_ExistingEnvWins is the precedence guarantee that @@ -230,6 +253,7 @@ func TestProfilesYAML_ContainsRequiredSections(t *testing.T) { "KANDEV_FEATURES_OFFICE:", "KANDEV_FEATURES_APP_STATUS_BAR:", "KANDEV_FEATURES_CLAUDE_BACKGROUND_PROMPT_HANDOFF:", + "KANDEV_WEB_TITLE_PREFIX:", } { if !strings.Contains(yaml, section) { t.Errorf("embedded profiles.yaml missing section/key %q; embed broken or file truncated", section) diff --git a/apps/cli/src/supervisor/manifest.test.ts b/apps/cli/src/supervisor/manifest.test.ts index b485cd429c..6c851b748a 100644 --- a/apps/cli/src/supervisor/manifest.test.ts +++ b/apps/cli/src/supervisor/manifest.test.ts @@ -29,6 +29,7 @@ describe("supervisor launch manifest", () => { KANDEV_HOME_DIR: home, KANDEV_SERVER_PORT: "38429", KANDEV_DESKTOP_HEALTH_TOKEN: "health-token", + KANDEV_WEB_TITLE_PREFIX: "Preview", SECRET_TOKEN: "do-not-write", }, home_dir: home, @@ -45,6 +46,7 @@ describe("supervisor launch manifest", () => { KANDEV_HOME_DIR: home, KANDEV_SERVER_PORT: "38429", KANDEV_DESKTOP_HEALTH_TOKEN: "health-token", + KANDEV_WEB_TITLE_PREFIX: "Preview", }); if (process.platform !== "win32") { expect((fs.statSync(target).mode & 0o777).toString(8)).toBe("600"); @@ -75,6 +77,7 @@ describe("supervisor launch manifest", () => { KANDEV_HOME_DIR: "/tmp/kandev", KANDEV_CONSOLE_LOG_LEVEL: "warn", KANDEV_DESKTOP_HEALTH_TOKEN: "health-token", + KANDEV_WEB_TITLE_PREFIX: "Dev", AWS_SECRET_ACCESS_KEY: "secret", GITHUB_TOKEN: "secret", }), @@ -82,6 +85,7 @@ describe("supervisor launch manifest", () => { KANDEV_HOME_DIR: "/tmp/kandev", KANDEV_CONSOLE_LOG_LEVEL: "warn", KANDEV_DESKTOP_HEALTH_TOKEN: "health-token", + KANDEV_WEB_TITLE_PREFIX: "Dev", }); }); }); diff --git a/apps/cli/src/supervisor/manifest.ts b/apps/cli/src/supervisor/manifest.ts index 81686d83eb..d830b902f4 100644 --- a/apps/cli/src/supervisor/manifest.ts +++ b/apps/cli/src/supervisor/manifest.ts @@ -29,6 +29,7 @@ const ENV_ALLOWLIST = new Set([ "KANDEV_AGENT_STANDALONE_PORT", "KANDEV_LOG_LEVEL", "KANDEV_CONSOLE_LOG_LEVEL", + "KANDEV_WEB_TITLE_PREFIX", "KANDEV_DEBUG_DEV_MODE", "KANDEV_DEBUG_AGENT_MESSAGES", "KANDEV_DEBUG_PPROF_ENABLED", diff --git a/apps/web/e2e/fixtures/backend.ts b/apps/web/e2e/fixtures/backend.ts index ce57182ef1..20390ba8b0 100644 --- a/apps/web/e2e/fixtures/backend.ts +++ b/apps/web/e2e/fixtures/backend.ts @@ -438,8 +438,9 @@ export const backendFixture = base.extend({ // process.env.X before spawn — that already flows through the // `...sanitizeInheritedEnv(process.env)` spread above, and the // backend's ApplyProfile leaves already-set vars alone. (Note: - // KANDEV_FEATURES_* is the exception — it's stripped from the - // inherited env so the profile always governs feature flags.) + // KANDEV_FEATURES_* and KANDEV_WEB_TITLE_PREFIX are exceptions — + // they are stripped from the inherited env so the e2e profile + // always controls the baseline.) GIT_AUTHOR_NAME: "E2E Test", GIT_AUTHOR_EMAIL: "e2e@test.local", GIT_COMMITTER_NAME: "E2E Test", @@ -539,6 +540,9 @@ function writeGitShimLauncher(shimDir: string, shimScript: string): void { // in the test backend → /api/v1/office/* 404s. Dropping the whole // KANDEV_FEATURES_* namespace lets the e2e profile govern feature flags so // the suite always exercises them, regardless of where it's launched. +// - KANDEV_WEB_TITLE_PREFIX — the browser identity is profile-managed in +// this suite. Explicit per-test values are applied after this baseline is +// sanitized through `backend.restart(overrides)`. // - PATH casing aliases — Windows commonly inherits `Path`; retaining it // beside the fixture's new `PATH` makes child-process lookup order // ambiguous. The caller restores one canonical PATH after sanitizing. @@ -547,7 +551,9 @@ function sanitizeInheritedEnv(env: Record): Record { + test("uses Dev for the development profile", async ({ backend, testPage }) => { + await backend.restart({ KANDEV_E2E_MOCK: "", KANDEV_DEBUG_DEV_MODE: "true" }); + + try { + await testPage.goto("/"); + await expect(testPage).toHaveTitle("Dev Kandev"); + } finally { + await backend.restart(); + } + }); + + test("uses Preview for an explicitly configured preview backend", async ({ + backend, + testPage, + }) => { + await backend.restart({ KANDEV_WEB_TITLE_PREFIX: "Preview" }); + + try { + await testPage.goto("/"); + await expect(testPage).toHaveTitle("Preview Kandev"); + } finally { + await backend.restart(); + } + }); +}); diff --git a/docs/configuration.md b/docs/configuration.md index b6b6c683ae..320cbd22f8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -34,6 +34,10 @@ All env vars use the `KANDEV_` prefix. Nested keys map by replacing `.` with `_` The aliases on the right are explicit bindings - see `LoadWithPath` in `config.go` for the full list. New keys should follow the deterministic rule (`KANDEV_
_`) unless there is a reason to add an alias. +The `make dev` launcher uses `Dev` as the default browser title prefix, and PR +preview environments use `Preview`. An explicit `KANDEV_WEB_TITLE_PREFIX` +value overrides those environment defaults. + ## Full `config.yaml` example Every key shown here has a default - copying the whole file changes nothing. Use it as a starting point and delete what you don't need to override. diff --git a/docs/plans/dev-preview-title-prefixes/plan.md b/docs/plans/dev-preview-title-prefixes/plan.md new file mode 100644 index 0000000000..8199370f9a --- /dev/null +++ b/docs/plans/dev-preview-title-prefixes/plan.md @@ -0,0 +1,116 @@ +--- +spec: docs/specs/dev-preview-title-prefixes/spec.md +created: 2026-08-09 +status: complete +--- + +# Implementation Plan: Environment-specific browser tab title prefixes + +## Overview + +Reuse the merged PR #2459 title-prefix contract and supply environment-specific +defaults at the launch boundaries. The development profile will provide `Dev`, +the PR preview startup script will provide `Preview`, and the CLI supervisor +will preserve the prefix across restarts. Focused Go, CLI, browser, and docs +checks will cover the launcher contracts and the visible browser result. + +## Backend and launchers + +### Development profile + +- `apps/backend/internal/profiles/profiles.yaml` (the target of the root + `profiles.yaml` symlink): add `KANDEV_WEB_TITLE_PREFIX` with an empty `prod` + and `e2e` value and `Dev` as the `dev` value. +- `apps/backend/internal/profiles/profiles_test.go`: assert the dev profile + supplies `Dev` while the existing profile precedence remains intact. + +### PR preview launcher + +- `apps/backend/cmd/preview/sprite_ops.go`: export + `KANDEV_WEB_TITLE_PREFIX=Preview` in the generated preview startup script. +- `apps/backend/cmd/preview/sprite_ops_test.go`: pin the generated script to + the `Preview` prefix alongside its existing startup assertions. + +### Supervisor environment contract + +- `apps/cli/src/supervisor/manifest.ts`: add `KANDEV_WEB_TITLE_PREFIX` to the + restart-manifest allowlist so any configured prefix survives a supervised + backend restart. +- `apps/cli/src/supervisor/manifest.test.ts`: verify the title-prefix variable + is retained while arbitrary variables remain excluded. + +## Frontend and browser behavior + +No new frontend implementation is required. The existing PR #2459 path already +renders the configured prefix in the server shell and applies it for the +`/api/v1/app-state` boot path. + +Add `apps/web/e2e/tests/system/title-prefix.spec.ts` to restart the fixture +backend with the development profile and with an explicit preview prefix, then +assert both visible browser titles through the UI. The test restores the worker +environment afterward. + +### Mobile parity contract + +The affected surface is browser metadata (`document.title`), not layout, +navigation, touch input, scrolling, or viewport-bound UI. The desktop and +mobile outcomes are identical, so no mobile-specific interaction test is +needed; the E2E test and existing title helper contract cover the same +viewport-independent behavior. + +## Public documentation + +- `docs/public/configuration.md`: explain that `make dev` defaults the title + prefix to `Dev` and PR previews use `Preview`, while explicit environment + values still override defaults. +- `docs/configuration.md`: keep the internal configuration reference aligned. + +## Tests + +- **What:** the development profile resolves the title prefix to `Dev` and + leaves production/e2e defaults blank. + **File:** `apps/backend/internal/profiles/profiles_test.go`. + **How:** profile application unit test with isolated environment variables. +- **What:** the preview startup script exports `Preview`. + **File:** `apps/backend/cmd/preview/sprite_ops_test.go`. + **How:** generated-script contract assertion. +- **What:** the supervisor retains the title-prefix environment variable. + **File:** `apps/cli/src/supervisor/manifest.test.ts`. + **How:** allowlist unit test. +- **What:** a configured runtime prefix is visible in the browser title. + **File:** `apps/web/e2e/tests/system/title-prefix.spec.ts`. + **How:** worker backend restart plus Playwright UI assertion. + +## E2E Tests + +- **Scenarios:** GIVEN a fixture backend restarted with the development profile, + WHEN the browser opens Kandev, THEN the page title is `Dev Kandev`; GIVEN a + fixture backend restarted with `KANDEV_WEB_TITLE_PREFIX=Preview`, THEN the + page title is `Preview Kandev`. +- **File:** `apps/web/e2e/tests/system/title-prefix.spec.ts`. +- **What to verify:** the user-visible browser tab title after a fresh page + navigation. + +## Verification Results + +- `cd apps/backend && go test -tags fts5 ./internal/profiles ./cmd/preview` — + passed, 30 tests in 2 packages. +- `cd apps && pnpm --filter kandev test -- --run src/dev.test.ts src/supervisor/manifest.test.ts` — + passed, 2 files and 6 tests. +- `cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts` — + passed, 2 Playwright tests (`Dev Kandev` and `Preview Kandev`). +- `node --test scripts/validate-public-docs.test.mjs` — passed, 58 tests. +- `node scripts/validate-public-docs.mjs` — passed, 41 published pages. +- `pnpm exec prettier --check ...` — passed for the changed TypeScript files. +- `git diff --check` — passed. + +## Implementation Waves And Parallel Candidates + +Wave 1 (sequential; the launch contracts and their tests are one cohesive +change): + +- [x] [task-01-launch-prefixes](task-01-launch-prefixes.md) + +## Open Questions + +None. diff --git a/docs/plans/dev-preview-title-prefixes/task-01-launch-prefixes.md b/docs/plans/dev-preview-title-prefixes/task-01-launch-prefixes.md new file mode 100644 index 0000000000..b3b6765b62 --- /dev/null +++ b/docs/plans/dev-preview-title-prefixes/task-01-launch-prefixes.md @@ -0,0 +1,104 @@ +--- +id: "01-launch-prefixes" +title: "Set environment title prefixes" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +spec: "../../specs/dev-preview-title-prefixes/spec.md" +--- + +# Task 01: Set environment title prefixes + +## Acceptance + +- `make dev` selects a backend profile that produces `Dev Kandev` by default, + while normal production/start and e2e profiles remain unprefixed unless an + explicit environment override is supplied. +- PR preview startup produces `Preview Kandev`, and the CLI supervisor retains + `KANDEV_WEB_TITLE_PREFIX` when it writes and reuses its restart manifest. +- Focused browser, Go, TypeScript, and public-doc checks pass for the new + launcher behavior. + +## Verification + +Run the focused unit checks: + +```bash +cd apps/backend && go test -tags fts5 ./internal/profiles ./cmd/preview +cd ../ && pnpm --filter kandev test -- --run src/dev.test.ts src/supervisor/manifest.test.ts +``` + +Run the browser contract against the managed production E2E build: + +```bash +cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts +``` + +Validate public documentation: + +```bash +node --test scripts/validate-public-docs.test.mjs +node scripts/validate-public-docs.mjs +``` + +On a fresh worktree, install workspace dependencies first with +`cd apps && pnpm install --frozen-lockfile`. + +## Files likely touched + +- `apps/backend/internal/profiles/profiles.yaml` +- `apps/backend/internal/profiles/profiles_test.go` +- `apps/backend/cmd/preview/sprite_ops.go` +- `apps/backend/cmd/preview/sprite_ops_test.go` +- `apps/cli/src/supervisor/manifest.ts` +- `apps/cli/src/supervisor/manifest.test.ts` +- `apps/web/e2e/tests/system/title-prefix.spec.ts` +- `docs/configuration.md` +- `docs/public/configuration.md` + +## Dependencies + +None. + +## Parallelism + +Sequential. The profile, preview script, supervisor allowlist, and browser test +all implement one environment-to-title contract, and the tests share the same +behavioral acceptance criteria. + +## Inputs + +- `docs/specs/dev-preview-title-prefixes/spec.md` +- `docs/plans/dev-preview-title-prefixes/plan.md` +- The merged PR #2459 `KANDEV_WEB_TITLE_PREFIX` contract. +- Existing profile precedence: explicit shell/launcher environment overrides + profile defaults. +- Existing backend and web E2E fixtures. + +## Output contract + +Report the changed files, exact test commands and results, any E2E cleanup, and +whether the task and plan statuses were updated. Record blockers and remaining +risks explicitly. + +## Results + +Implemented the profile, preview launcher, and supervisor environment +contracts. Added browser coverage for both environment defaults and updated the +public and internal configuration references. + +Verification completed: + +- `cd apps/backend && go test -tags fts5 ./internal/profiles ./cmd/preview` — + passed, 30 tests in 2 packages. +- `cd apps && pnpm --filter kandev test -- --run src/dev.test.ts src/supervisor/manifest.test.ts` — + passed, 2 files and 6 tests. +- `cd apps/web && pnpm e2e:run --project chromium tests/system/title-prefix.spec.ts` — + passed, 2 Playwright tests. +- `node --test scripts/validate-public-docs.test.mjs` — passed, 58 tests. +- `node scripts/validate-public-docs.mjs` — passed, 41 published pages. +- `pnpm exec prettier --check ...` and `git diff --check` — passed. + +The managed E2E runner restored the fixture backend after each test; no +generated files are tracked in the worktree. diff --git a/docs/public/configuration.md b/docs/public/configuration.md index 3de49b8782..8caca1e880 100644 --- a/docs/public/configuration.md +++ b/docs/public/configuration.md @@ -58,7 +58,7 @@ Some common camelCase keys have explicit compatibility aliases. Use the document | `server.readTimeout` | `KANDEV_SERVER_READTIMEOUT` | `30` | HTTP read timeout in seconds. | | `server.writeTimeout` | `KANDEV_SERVER_WRITETIMEOUT` | `30` | HTTP write timeout in seconds. | | `server.webInternalUrl` | `KANDEV_WEB_INTERNAL_URL` | empty | Development reverse-proxy target for a separately running web app. Installed releases normally serve embedded assets. | -| `server.webTitlePrefix` | `KANDEV_WEB_TITLE_PREFIX` | empty | Prefixes the browser tab title as ` Kandev` (for example `TEST` renders `TEST Kandev`), so several instances stay distinguishable in adjacent tabs. Empty keeps the plain `Kandev` title. | +| `server.webTitlePrefix` | `KANDEV_WEB_TITLE_PREFIX` | empty | Prefixes the browser tab title as ` Kandev` (for example `TEST` renders `TEST Kandev`), so several instances stay distinguishable in adjacent tabs. `make dev` defaults to `Dev`; PR previews use `Preview`. An explicit value overrides these defaults. Empty keeps the plain `Kandev` title. | The default host exposes the server on every interface even though the CLI prints a `localhost` URL. The current local product path must not be treated as an authenticated multi-user perimeter. For remote access, bind to loopback and use a trusted authenticated tunnel/proxy, or isolate the network at the deployment layer. diff --git a/docs/specs/dev-preview-title-prefixes/spec.md b/docs/specs/dev-preview-title-prefixes/spec.md new file mode 100644 index 0000000000..58ab677248 --- /dev/null +++ b/docs/specs/dev-preview-title-prefixes/spec.md @@ -0,0 +1,84 @@ +--- +status: shipped +created: 2026-08-09 +owner: Kandev +--- + +# Environment-specific browser tab title prefixes + +## Why + +Developers often keep a local development instance and one or more pull-request +preview instances open at the same time. Identical `Kandev` browser tabs make it +easy to use the wrong instance. + +## What + +- A Kandev browser tab title uses the existing ` Kandev` composition. +- A backend started in the development profile, including `make dev`, uses the + default prefix `Dev`, so its tab title is `Dev Kandev`. +- A PR preview environment uses the prefix `Preview`, so its tab title is + `Preview Kandev`. +- An explicitly supplied `KANDEV_WEB_TITLE_PREFIX` continues to take precedence + over a profile default. Its value is trimmed by the existing title-prefix + contract. +- A normal production/start launch with no explicit prefix keeps the plain + `Kandev` title. +- The prefix is present both on the initial server-rendered page and after a + client boot from `/api/v1/app-state`, using the title behavior delivered by + PR #2459. +- A supervised backend restart keeps the configured prefix. + +## API surface + +The existing environment contract remains the public configuration surface: + +| Environment | `KANDEV_WEB_TITLE_PREFIX` | Result | +|---|---|---| +| Development profile (`make dev`) | `Dev` by default | `Dev Kandev` | +| PR preview launcher | `Preview` | `Preview Kandev` | +| Normal start with no override | unset | `Kandev` | +| Any launch with an explicit override | caller value | ` Kandev` | + +The prefix is process configuration. It is not stored in the database and is +not changed from the Kandev UI. + +## Failure modes + +- If the prefix is unset or blank, the title remains `Kandev`. +- If the preview launcher cannot set the environment variable, the preview + falls back to the existing plain-title behavior rather than failing startup. +- If a supervised restart occurs, the launcher preserves the allowlisted prefix + in the restart manifest so the restarted backend does not silently change its + title. + +## Persistence guarantees + +The title prefix lasts for the lifetime of the launched environment and its +supervised restarts. It is not persisted across separate launches unless the +caller supplies the same environment or profile again. + +## Scenarios + +- **GIVEN** a clean checkout with no title-prefix override, **WHEN** the user + runs `make dev` and opens the Kandev URL, **THEN** the browser title is + `Dev Kandev`. +- **GIVEN** a PR preview deployment, **WHEN** its startup service launches + Kandev, **THEN** the browser title is `Preview Kandev`. +- **GIVEN** a development profile and `KANDEV_WEB_TITLE_PREFIX=Custom`, **WHEN** + the backend starts, **THEN** the browser title is `Custom Kandev`. +- **GIVEN** a normal start with no title-prefix environment variable, **WHEN** + the user opens Kandev, **THEN** the browser title is `Kandev`. +- **GIVEN** a preview or development backend with a configured prefix, **WHEN** + its supervisor restarts the backend, **THEN** the browser title retains the + same prefix. +- **GIVEN** a phone-sized browser viewport, **WHEN** either environment is + opened, **THEN** the same title is shown; no viewport-specific interaction is + required. + +## Out of scope + +- Changing the title dynamically from the Kandev UI. +- Changing desktop window titles, CLI output, logs, or task titles. +- Automatically labeling arbitrary self-hosted instances beyond the existing + explicit environment variable.