diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 39ed6ba852c..fa83b54381c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,7 +10,9 @@ repos:
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- exclude: ^(schema/schema\.graphql|schema/openapi\.json)$
+ # Generator output, committed byte-exact. These generators emit no trailing newline, so
+ # appending one here would guarantee a mismatch against what CI regenerates.
+ exclude: ^(schema/schema\.graphql|schema/openapi\.json|docs/docs/reference/configuration\.mdx)$
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
diff --git a/backend/infrahub/config.py b/backend/infrahub/config.py
index 32f9c639409..0b163e01d6c 100644
--- a/backend/infrahub/config.py
+++ b/backend/infrahub/config.py
@@ -990,6 +990,10 @@ class AnalyticsSettings(BaseSettings):
class ExperimentalFeaturesSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="INFRAHUB_EXPERIMENTAL_")
graphql_enums: bool = False
+ dark_theme: bool = Field(
+ default=False,
+ description="Offer the dark theme in the web interface. Alpha: some surfaces still render incorrectly.",
+ )
value_db_index: bool = Field(
default=False,
deprecated="This setting has no effect and will be removed in a future version.",
diff --git a/changelog/+dark-theme.added.md b/changelog/+dark-theme.added.md
new file mode 100644
index 00000000000..48f39077ee1
--- /dev/null
+++ b/changelog/+dark-theme.added.md
@@ -0,0 +1 @@
+Added an experimental dark theme: development deployments default to it, and a switch in the account menu (marked alpha) toggles between light and dark, gated by the INFRAHUB_EXPERIMENTAL_DARK_THEME setting
diff --git a/dev/guidelines/frontend/styling.md b/dev/guidelines/frontend/styling.md
index ef3f28f6e0e..a6ac523d2ee 100644
--- a/dev/guidelines/frontend/styling.md
+++ b/dev/guidelines/frontend/styling.md
@@ -1,6 +1,6 @@
# Styling Guidelines
-> Part of: `dev/guidelines/frontend/`
+> Part of: `dev/guidelines/frontend/` | Related: [Theming](../../knowledge/frontend/theming.md)
## Layout Components
@@ -79,5 +79,18 @@ export const Button = ({ variant, size, className, ref, ...props }: ButtonProps)
| Inline `style={{}}` | Tailwind classes |
| CSS modules | Tailwind utilities |
| `bg-[#1e40af]` | `bg-custom-blue-700` (use theme) |
+| `bg-white`, `bg-gray-50`, `bg-gray-100` | `bg-content`, `bg-content-muted`, `bg-content-strong` |
+| `bg-white dark:bg-stone-900` | `bg-content` — one token already carries both themes |
+| `text-indigo-500`, `text-indigo-700` for an open or active state | `text-active`, `bg-active/10` |
| `
` | `` from `@/shared/components/container` |
| `
` | `
` from `@/shared/components/container` |
+
+### Why a fixed palette is forbidden, not just discouraged
+
+A class like `bg-white` is not theme-neutral — it paints light in *both* themes, so the surface stays bright when the rest of the page goes dark. This is easy to miss in review because the defect is the **absence** of a variant rather than the presence of a wrong one: searching for `dark:` finds the files that already work and none of the files that are broken.
+
+Pairing a literal with a `dark:` override (`bg-white dark:bg-stone-900`) renders correctly but duplicates in every call site what a token defines once, so the next palette change has to be repeated by hand in each of them.
+
+A `dark:` variant is legitimate only where no token can express the difference — swapping between two different assets, for example, or a dark-only effect such as a backdrop blur.
+
+Contrast is the other reason. A mid-ramp shade that reads well on one background rarely clears WCAG AA on its opposite: `text-indigo-500` measured 3.7:1 on the light sidebar and 4.3:1 on the dark one, failing the 4.5:1 threshold in both. Each theme needs its own end of the ramp, which is exactly what a token holds and a literal cannot.
diff --git a/dev/knowledge/frontend/theming.md b/dev/knowledge/frontend/theming.md
new file mode 100644
index 00000000000..2032684d05b
--- /dev/null
+++ b/dev/knowledge/frontend/theming.md
@@ -0,0 +1,125 @@
+# Theming
+
+> Part of: `dev/knowledge/frontend/` | Related: [Styling Guidelines](../../guidelines/frontend/styling.md)
+
+How the light and dark themes work, and how to change them safely. The short version: every colour
+the app paints should resolve through a semantic token defined once per theme in a single file, and
+dark mode is nothing more than a `dark` class on the document element swapping those definitions.
+
+## Where colours live
+
+`frontend/packages/ui/src/styles/theme.css` is the single source of truth. It has three parts, and
+a colour change touches one, two, or three of them depending on the change:
+
+| Block | What it holds |
+|---|---|
+| `:root { … }` | The light palette: one custom property per semantic token, plus `color-scheme: light` |
+| `.dark { … }` | The dark palette: the **same property names** with dark values, plus `color-scheme: dark` |
+| `@theme inline { … }` | The Tailwind bridge: `--color-: var(--)` lines that turn each token into utilities (`bg-`, `text-`, `ring-`, …) |
+
+### Change a colour in dark only
+
+Edit its value inside the `.dark` block. Nothing else — no component changes, no `dark:` variants,
+no light-theme risk, because the light value in `:root` is untouched.
+
+### Change a colour in both themes
+
+Edit the token's value in `:root` and in `.dark`. Every call site follows.
+
+### Add a new token
+
+Three edits in `theme.css`: a light value in `:root`, a dark value in `.dark`, and a
+`--color-: var(--);` line in `@theme inline`. Then use `bg-` / `text-`
+etc. in components. Name the token for its **role** (`--active`, `--danger-surface`, `--content`),
+never its colour — a token called `--indigo` cannot honestly hold anything else.
+
+Paired tokens follow the `X` / `X-surface` convention (`--danger` / `--danger-surface`,
+`--active` / `--active-surface`): the bare name is the foreground/stroke, `-surface` is the tinted
+background behind it.
+
+## How dark mode switches on
+
+The `dark` class on `document.documentElement` is the only switch. The primitives live in the
+design system (`frontend/packages/ui/src/theme/`), so anything built on `@infrahub/ui` can read and
+offer the theme; the application owns only the *policy* that decides it. Three things manage the
+class:
+
+1. **The pre-paint script** in `frontend/app/index.html` — a blocking inline script in ``
+ that applies the class before the first frame, from the `infrahub.theme.resolved` localStorage
+ mirror. It is deliberately outside the module graph (it must run before any bundle loads), so
+ the storage key is duplicated there verbatim — renaming the key means changing both files in
+ the same commit.
+2. **`ThemeProvider`** (`frontend/app/src/entities/config/ui/theme-provider.tsx`) — the policy.
+ Decides the real theme once config arrives: the `dark_theme` experimental flag gates whether
+ dark is offered at all, `infrahub.theme.choice` holds this browser's explicit choice, and the
+ resolved outcome is applied to the class and mirrored back to storage (`applyTheme` and the
+ storage helpers come from `@infrahub/ui`). It fills the design system's `ThemeContext`, which is
+ what makes `ThemeSwitchMenuItem` — the ready-made switch a menu can drop in — render and work.
+ An absent flag (backend predates it) counts as enabled under a Vite dev server only — see
+ `entities/config/domain/rules/can-offer-dark-theme.ts`.
+3. **`useResolvedTheme`** (from `@infrahub/ui`) — how components *read* the current theme: a
+ `useSyncExternalStore` subscription to the class via MutationObserver. Components never read
+ storage or config for this; the document element is the single source of truth.
+
+The deployment gate is `INFRAHUB_EXPERIMENTAL_DARK_THEME`, passed through in
+`development/docker-compose.yml` only (default `true` there). The root compose file deliberately
+has no passthrough while the theme is alpha.
+
+## Content that carries its own colours
+
+Three renderers bake colours into their output and cannot be themed by CSS tokens:
+
+- **Mermaid diagrams** — themed through `mermaid.initialize({ theme })`, called from a small rehype
+ plugin sequenced before the rendering plugin
+ (`shared/components/editor/markdown/markdown-with-mermaid.tsx`). The rendering plugin's own
+ `mermaidConfig` option is silently ignored by its browser build; its documentation says to call
+ `initialize` manually, and the browser build renders against that same global config. Two traps
+ worth knowing: the `mermaid` version range must stay compatible with the one `mermaid-isomorphic`
+ declares (two instances in the tree would mean configuring the wrong one), and the call must live
+ *inside* the pipeline — a render-phase call is dropped by the React Compiler, and an effect races
+ the child's async processing. A diagram's own `%%{init}%%` directive still wins, by mermaid's own
+ precedence.
+- **GraphiQL** — has its own theme; the sandbox page passes the app's resolved theme through
+ `forcedTheme` so it can never disagree with the app around it.
+- **Schema-defined colours** (role badges, kind palettes, user-picked hex values) — data, not
+ style. Rendered as-is in both themes; out of scope for tokens.
+
+## When a `dark:` variant is acceptable
+
+Almost never — a fixed palette class (`bg-white`, `bg-gray-50`) is a bug even when it *looks* fine
+in light, and pairing it with a `dark:` override duplicates per call site what a token defines
+once. The two legitimate exceptions, both from
+[Styling Guidelines](../../guidelines/frontend/styling.md):
+
+- No token can express the difference — swapping assets, dark-only effects (backdrop blur).
+- Categorical ramps where the hue carries no meaning (the sidebar avatar colours): there is no
+ semantic name to give a token, and the ramp has a single definition site, so the duplication a
+ token prevents cannot arise.
+
+## Verifying a colour change
+
+- **Contrast**: WCAG AA needs 4.5:1 for normal text, 3:1 for large text and UI parts. Measure
+ against the surface the element *actually sits on*, compositing translucent layers — a mid-ramp
+ shade that passes on one theme's background usually fails on the other's (that is why `--active`
+ holds `indigo-700` in light but `indigo-400` in dark).
+- **Probing gotcha**: Tailwind only generates classes that appear in source. A class assembled
+ dynamically in a devtools probe (`bg-${hue}-400/15`) silently resolves to nothing and reads as
+ transparent — probe with the exact class strings the component ships.
+- **Both themes, always**: toggle via the account-menu switch, or
+ `document.documentElement.classList.toggle("dark")` in the console. The light theme is the
+ shipped default; a dark fix must not move light pixels unless that is the intent.
+
+## Test coverage
+
+| Concern | Test |
+|---|---|
+| Flag/choice resolution, retention across flag flips | `entities/config/ui/theme-provider.test.tsx`, `entities/config/domain/rules/can-offer-dark-theme.test.ts` |
+| Reading the theme from the class | `shared/hooks/use-resolved-theme.test.tsx` |
+| The switch in the account menu, alpha tag, gating | `entities/user-profile/ui/account-menu.test.tsx` |
+| Mermaid renders in the active theme, reacts to a flip, author directive wins | `shared/components/editor/markdown/markdown-with-mermaid.test.tsx` (asserts the colours baked into the real SVG) |
+| First-paint, persistence, flag-off journeys | `tests/e2e/theme.spec.ts` (Playwright, needs a stack) |
+| Docs screenshots stay light | pinned in `tests/utils.ts` |
+
+The design-system package has no test runner, so tests for its theme primitives are hosted in the
+application suite. The pre-paint script itself is reachable only by the e2e suite — it sits outside
+the module graph, so no vitest test can import it.
diff --git a/dev/specs/infp-46-dark-theme-completion/alignment-check.md b/dev/specs/infp-46-dark-theme-completion/alignment-check.md
new file mode 100644
index 00000000000..1f08272ab24
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/alignment-check.md
@@ -0,0 +1,126 @@
+# Alignment Check: Dark Theme Completion
+
+**Date**: 2026-08-17 | **Spec**: [spec.md](./spec.md) | **Remediation passes used**: 0
+
+## Revision 2 — 2026-08-17, after idea grilling
+
+A structured grilling pass over the product core (the part that had wobbled twice) produced the
+seven decisions tabled below. **The feature got smaller.** Nothing was added.
+
+| Decision | Effect |
+|---|---|
+| Gate the whole feature behind a flag rather than defining an alpha exit date | `INFRAHUB_EXPERIMENTAL_DARK_THEME`, off by default, on in `development/docker-compose.yml`. Motivated by the release cycle for the target version being unknown — `1.11.0` has not shipped, the newest tag is `1.11.0b2`. |
+| Flag off → light only | The theme field is hidden entirely. ⚠ Not a light-only picker: offering match-system would let a dark-OS user reach the alpha palette straight through the gate. Stored `DARK` is ignored, never deleted. |
+| Flag defaults from dev config, **not** from the running version | Replaced the PEP 440 pre-release derivation. "Pre-release" is a property of a version, so it caught customer betas too — broader than "the deployments we run". Follows the convention both existing experimental settings already use. |
+| No removal date for the flag | Recorded as knowingly open-ended rather than left unstated. ⚠ `value_db_index` sits in the same settings class today as a dead flag with a deprecation notice — a realised failure mode. |
+| No organisation-wide theme default | FR-003 was an addition made during specification, never requested. With the feature flag-gated to the dev stack it has no user. Backend gains the scope for free; only the interface is deferred. |
+| The flag goes in `development/docker-compose.yml` **only** | The root `docker-compose.yml` does not get it, unlike its two experimental siblings there. Decided, not an oversight — a deployment brought up from the root file therefore cannot enable dark via the host env var, which is the intent while dark is alpha. |
+| Dogfooding defects are reported over Slack | Closes the critique's P3. Naming the destination is what makes "no new defects were found" checkable rather than an absence of evidence. |
+
+**Removed by this revision**: `core/preferences/theme.py`, PEP 440 version parsing, the
+`default_theme` config field and its payload entry, the organisation-scope interface, and the
+`Deployment default theme` entity. **One governance gate dropped** — no new dependency, since nothing
+parses versions any more.
+
+**Net against the original handover**: still all seven items, still fully covered.
+
+## Revision — 2026-08-17, after edge-case review
+
+The requester reviewed the Edge Cases section and directed six changes. All are applied across
+`spec.md`, `research.md`, `data-model.md`, `contracts/rest-config.md`, `plan.md`, `quickstart.md` and
+`tasks.md`. They do not change the seven-item coverage below.
+
+| Direction | Effect |
+|---|---|
+| "By default we should respect the user's browser/system config" — then, on seeing the consequence: "dark should remain alpha; respect system preferences only if you're in alpha" | **Net effect: no change.** An intermediate revision moved the production default to `system`; it was withdrawn once the requester saw that it would put dark-OS production users into the alpha palette without choosing it. Final state matches the original spec: production → `light`, non-production → `dark` (forced, ignoring the OS). Match-system stays available everywhere as an explicit user choice, never a default. |
+| "Couldn't we store something in localStorage?" | Confirmed — already the design. The three cache-related edge cases (pre-sign-in, preference-unavailable, first paint) are now stated as one problem with one mechanism rather than three bullets. |
+| "Multiple tabs — ignore this" | Moved to Out of Scope; the `storage` listener is dropped from the provider. |
+| "System theme changes — react, only if easy" | Kept (FR-007). It is a subscribable browser event, so the cost is small. |
+| "Content that carries its own colors — tackle separately" | Moved to Out of Scope. Former FR-021 (semantic distinguishability) removed; contrast promoted to FR-021 with an explicit boundary. `badge.tsx` becomes migrate-without-degrading rather than a palette redesign. |
+| "Existing automated tests — let's tackle this" | Confirmed in scope; T035 unchanged. |
+| "Build this as a stacked PR on the existing one" | Branch bases on `bab-dark-theme-app` and the PR targets it, not `develop`. Recorded that #10284's failing checks are inherited. |
+
+**Governing principle, now stated explicitly in the spec**: dark is never reached by inference. A
+user arrives at it only by choosing dark, or by choosing match-system on a dark machine. That single
+rule decides both defaults — flag-off is light rather than system-following, and the pre-paint
+script's empty-cache fallback is light rather than `prefers-color-scheme`.
+
+The mirror-image rule governs the other default: with the flag on, dark is forced *ignoring* the
+system, because following it would leave every engineer on a light machine out of the dogfooding.
+
+**Residual limitation, unchanged from the original design**: a first-ever visit to a flag-enabled
+deployment paints light for one frame before correcting to dark. Flag-off deployments are unaffected,
+since light is already the final answer there.
+
+## Source
+
+The source of truth is the **inline handover list** supplied by the requester: seven numbered
+"Known limitations / follow-ups" recorded by the author of the dark-theme series, together with the
+framing statement about taking over PR
+[#10284](https://github.com/opsmill/infrahub/pull/10284).
+
+It qualifies as a substantive PRD: structured, requirement-bearing, and well over the length
+threshold. No external PRD document was linked, so nothing needed fetching — the only URL in the ask
+is the pull request itself, which was read for context rather than as a requirements source.
+
+Two clarifications were obtained directly from the requester during specification and count as part
+of the source:
+
+- Scope confirmed at **all seven items**, including the separate schema-visualizer repository, after
+ being challenged as four.
+- PR #10284's failing end-to-end checks: **explicitly deferred**, out of scope.
+
+## Verdict
+
+**⚠️ MINOR DRIFT (proceeding)**
+
+All seven items are present and traceable. No requirement was dropped, softened, or reversed. The
+drift is entirely in one direction — the spec adds material the handover did not ask for — and every
+addition is either a necessary consequence of the chosen approach or a recorded judgement call. One
+finding was a genuine fidelity loss and has been corrected.
+
+## Coverage of the source ask
+
+| # | Handover item | Spec location | Status |
+|---|---|---|---|
+| 1 | No user preference to switch themes; `@custom-variant` is a dev-only crutch; "alpha" tag next to dark | US1, FR-001–FR-009, FR-019 | ✅ |
+| 2 | GraphiQL has its own dark theme, bind it to the app theme | US3, FR-014 | ✅ |
+| 3 | Mermaid only partially dark, bind to selected theme | US4, FR-015 | ✅ |
+| 4 | Schema visualizer is in another repo, not dark-compatible | US7, FR-016 | ✅ |
+| 5 | DataViewer uses a colder (neutral) tone than the warmer theme | US6, FR-018 | ✅ |
+| 6 | Legacy pages (e.g. Proposed Changes) have hardcoded `dark:` variants and raw colors | US5, FR-017 | ✅ |
+| 7 | "Make canary enabled by default" so non-production versions default to dark | US2, FR-010–FR-013 | ✅ |
+| — | Take over #10284; ignore its failing E2E | Context, Out of Scope | ✅ |
+
+## Findings
+
+| Severity | Category | Source reference | Spec reference | Description |
+|---|---|---|---|---|
+| Corrected | changed | Item 1 — "add an *alpha* tag" | FR-008, T024 | The spec had generalised the label to "pre-release". The requester named "alpha" specifically; a synonym is a small but real loss of fidelity in the one string users read. **Fixed** — FR-008 and T024 now require the literal word. |
+| Minor | added | not in source | FR-001, `Theme.SYSTEM` | A match-system option was added. The handover implies a light/dark toggle. Recorded in Assumptions: it is the conventional expectation, and adding it later would change the meaning of an already-stored value. Reviewer-overturnable. |
+| Minor | added | not in source | FR-003 | An organisation-wide default. Not requested, but it falls out of reusing the existing preference store, which is already two-layer — excluding it would have meant *removing* behaviour the machinery provides. |
+| Minor | added | not in source | FR-006, SC-002 | First-paint correctness. Not requested, but shipping an account-backed theme setting without it produces a visible flash on every load; treated as inherent to item 1 rather than new scope. |
+| Minor | added | critique | FR-021, SC-009 | A contrast requirement, added by the engineering/product critique. Justified for a feature whose entire subject is color. (Numbered FR-022 when added; renumbered to FR-021 in the revision above, when semantic-color distinguishability moved out of scope.) |
+| Minor | added | not in source | T047 | An automated guard so the token cleanup does not regress. Follows from SC-004's "standing property" wording rather than from the ask. |
+| Minor | added | house rules | T057, T058 | Changelog fragment and user-facing documentation. Required by `AGENTS.md` for a user-facing feature, not by the handover. |
+| Resolved | — | Item 7 — "for the coming weeks" | SC-008, US2 | Raised in the critique as P2/P3 and left open at the time. Both halves now answered by the requester: the period is bounded by a feature flag rather than a date or an exit criterion, and defects found during it are reported over Slack. SC-008 was rewritten to something countable in the meantime. |
+
+### On item 7's mechanism
+
+The handover asked to "make canary enabled by default". No `canary` concept exists anywhere in the
+repository, so the term had no referent to implement. Rather than guess silently, a mechanism was
+first chosen with evidence ([research.md](./research.md) §R1: PEP 440 pre-release status on the
+running version) — and then **replaced in Revision 2**, when grilling surfaced that both existing
+experimental settings follow a house convention instead: an `INFRAHUB_EXPERIMENTAL_*` env var
+passed through docker-compose. "Pre-release" is a property of a version and would have caught
+customer betas too — broader than "the deployments we run". The shipped mechanism is
+`INFRAHUB_EXPERIMENTAL_DARK_THEME`, defaulting to `true` in `development/docker-compose.yml` only.
+
+This is recorded as a **resolution of an underspecified item**, not as drift — the intent ("the
+non-production versions we usually run default to dark") is met exactly, and the requester chose
+the flag design explicitly when both options were presented.
+
+## Action
+
+Proceed. The one fidelity loss is corrected; the remaining drift is additive, documented, and
+individually reversible by a reviewer. No remediation pass was required.
diff --git a/dev/specs/infp-46-dark-theme-completion/checklists/requirements.md b/dev/specs/infp-46-dark-theme-completion/checklists/requirements.md
new file mode 100644
index 00000000000..082e97d9750
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/checklists/requirements.md
@@ -0,0 +1,50 @@
+# Specification Quality Checklist: Dark Theme Completion
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-08-17
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+Validation observations, recorded rather than silently passed:
+
+- **Implementation detail in Context, deliberately.** The Context section names the `.dark` class and
+ the `@custom-variant dark` declaration. These describe the *status quo* being replaced, not the
+ design of the solution, and the corresponding requirement (FR-019) is stated abstractly. Kept.
+- **SC-004 is close to the line.** "Zero application components specify per-theme color overrides or
+ raw color literals" describes a source property rather than a user-observable one. It is retained
+ because it is precisely the outcome requested on handover, and because the user-visible
+ consequence (SC-005, SC-006) alone would not catch debt that merely *happens* to look right today.
+- **Named surfaces are product scope, not implementation.** GraphQL sandbox, Mermaid diagrams, data
+ viewer and schema visualizer are named throughout. They are the user-facing surfaces the feature
+ is defined by; naming them is not a leak.
+- **Three decisions were resolved by judgement rather than marked for clarification**, per the
+ autonomous-execution mode this spec was generated under. All three are recorded in Assumptions:
+ production defaulting to light, the inclusion of match-system, and deriving "non-production build"
+ from the running version. Each is a reviewer-overturnable call, and the third is deliberately left
+ to the plan to make concrete.
diff --git a/dev/specs/infp-46-dark-theme-completion/contracts/graphql-preferences.md b/dev/specs/infp-46-dark-theme-completion/contracts/graphql-preferences.md
new file mode 100644
index 00000000000..bcd1085761b
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/contracts/graphql-preferences.md
@@ -0,0 +1,116 @@
+# Contract: GraphQL preferences — theme field
+
+**Feature**: [spec.md](../spec.md) | **Covers**: FR-001, FR-002, FR-003, FR-004
+
+Additive delta to the existing preferences surface. Every change mirrors how `date_format` is already
+modelled, so nothing below introduces a pattern the schema does not already use.
+
+⚠ `schema/schema.graphql` is generated and CI-validated (`uv run invoke docs.validate`). Regenerate
+with `uv run invoke schema.generate-graphqlschema` and commit, or CI fails on a stale file.
+
+⚠ GraphQL schema modifications are **Ask First** per `AGENTS.md`. This contract is a proposal
+requiring sign-off, not an approved change.
+
+## New enum
+
+```graphql
+"""
+Appearance choices. SYSTEM follows the operating system; the dark palette is pre-release.
+"""
+enum Theme {
+ LIGHT
+ DARK
+ SYSTEM
+}
+```
+
+⚠ The description **must stay on one line** in the Python source. `graphql-core`'s SDL printer
+dedents multi-line descriptions differently across versions, which makes the generated
+`schema.graphql` environment-dependent — a constraint already documented in
+`backend/infrahub/graphql/types/preferences.py`.
+
+## New effective-value type
+
+```graphql
+"""An effective `theme` value and the source it was resolved from."""
+type EffectiveTheme {
+ source: PreferenceSource!
+ value: Theme
+}
+```
+
+`value` is null when nothing is stored at any layer; `source` is then `DEFAULT` and the client
+substitutes the deployment default from the config payload.
+
+## Changed types
+
+```diff
+ type EffectivePreferencesType {
+ date_format: EffectiveDateFormat!
+ timezone: EffectiveTimezone!
++ theme: EffectiveTheme!
+ }
+
+ type RawPreferencesType {
+ date_format: DateFormat
+ timezone: String
++ theme: Theme
+ }
+```
+
+`EffectivePreferencesType.theme` is non-null (the wrapper always exists); the `value` inside it is
+nullable. That is the existing convention — the wrapper reports a source even when there is no value.
+
+## Changed mutation
+
+```diff
+-InfrahubSetPreferences(date_format: DateFormat, scope: PreferenceWriteScope!, timezone: String): InfrahubSetPreferences
++InfrahubSetPreferences(date_format: DateFormat, scope: PreferenceWriteScope!, theme: Theme, timezone: String): InfrahubSetPreferences
+```
+
+The payload gains a matching `theme: Theme` output field.
+
+### ⚠ Three-state argument semantics
+
+`InfrahubSetPreferences` distinguishes three cases via the `_UNSET` sentinel in
+`backend/infrahub/graphql/mutations/preferences.py`. `theme` must honour all three, and a naive
+`theme: Theme | None = None` parameter collapses the first two and makes clearing impossible:
+
+| Client sends | Meaning | Stored |
+|---|---|---|
+| argument omitted | leave untouched | unchanged |
+| `theme: null` | clear the override at this scope | `None` |
+| `theme: DARK` | set the override | `Theme.DARK` |
+
+Mirror the existing handling exactly:
+
+```python
+if theme is not _UNSET:
+ preference.theme = None if theme is None else ThemeEnum(theme)
+```
+
+## Behavioural contract
+
+⚠ The `GLOBAL` rows describe the **backend resolution chain**, which is shared across all
+preference fields and therefore accepts a global theme write. No interface offers one in this
+version — theme is user-scoped only (FR-003), achieved by leaving `theme` out of the
+global-preferences mutation document, so the global layer simply has no writer. The rows exist
+because the chain must keep resolving correctly through a layer that is always `null` today and
+gains a writer only when the organisation-wide default ships later.
+
+| Given | When | Then |
+|---|---|---|
+| No preference at any layer | `InfrahubEffectivePreferences` queried | `theme.value = null`, `theme.source = DEFAULT` |
+| Global set to `DARK`, no user value | queried | `theme.value = DARK`, `theme.source = GLOBAL` |
+| Global `DARK`, user `LIGHT` | queried | `theme.value = LIGHT`, `theme.source = USER` |
+| User `LIGHT` | mutation with `theme: null`, scope `USER` | user override cleared; next query resolves to global or default |
+| Any state | mutation omitting `theme` | `theme` unchanged; other supplied fields still written |
+| Caller lacks global-write permission | mutation with scope `GLOBAL` | rejected by the existing permission check; no new permission introduced |
+| Stored value not a `Theme` member | read from database | rejected at construction, as `date_format` already behaves |
+
+## Out of scope for this contract
+
+- The deployment default — it is not a preference and is not served over GraphQL. See
+ [rest-config.md](./rest-config.md).
+- Stage-2 resolution of `SYSTEM` to a concrete palette. The server returns the stored choice; only
+ the client can observe the operating system's appearance.
diff --git a/dev/specs/infp-46-dark-theme-completion/contracts/rest-config.md b/dev/specs/infp-46-dark-theme-completion/contracts/rest-config.md
new file mode 100644
index 00000000000..bac4ccfb86f
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/contracts/rest-config.md
@@ -0,0 +1,108 @@
+# Contract: the dark-theme feature flag on the config payload
+
+**Feature**: [spec.md](../spec.md) | **Covers**: FR-010, FR-011, FR-012, FR-013
+
+The theme feature is gated per deployment. The gate is not a preference — it is a property of the
+deployment, needed before a user exists — so it travels on the unauthenticated config payload.
+
+⚠ `schema/openapi.json` and `frontend/app/src/shared/api/rest/types.generated.ts` are generated.
+Regenerate with `uv run invoke schema.generate-jsonschema` and `cd frontend/app && pnpm codegen`, and
+commit; CI validates them.
+
+## Why this endpoint
+
+`backend/infrahub/api/internal.py` exposes two endpoints with different auth postures:
+
+| Endpoint | Auth | Carries |
+|---|---|---|
+| `GET /api/config` | **none** | `main`, `logging`, `analytics`, `experimental_features`, `sso`, `ldap`, `installation_type`, `policy` |
+| `GET /api/info` | `Depends(get_current_user)` | `deployment_id`, `version` |
+
+The login page must know whether the feature exists before there is a session, and `/api/config`
+already carries `experimental_features`. So the flag needs **no new field and no new endpoint** — it
+joins a payload the frontend already consumes.
+
+## Settings delta
+
+```diff
+ class ExperimentalFeaturesSettings(BaseSettings):
+ model_config = SettingsConfigDict(env_prefix="INFRAHUB_EXPERIMENTAL_")
+ graphql_enums: bool = False
+ value_db_index: bool = Field(default=False, deprecated="…")
++ dark_theme: bool = False
+```
+
+A plain `bool` defaulting to `False`, matching `graphql_enums` exactly. No tri-state is needed: unlike
+the earlier design there is nothing to distinguish "unset" from "off", because the flag no longer
+carries a derived value.
+
+## Deployment configuration
+
+Following the convention both existing flags already use in `development/docker-compose.yml` and the
+root `docker-compose.yml`:
+
+```diff
+ INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS: ${INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS:-false}
+ INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX: ${INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX:-false}
++ INFRAHUB_EXPERIMENTAL_DARK_THEME: ${INFRAHUB_EXPERIMENTAL_DARK_THEME:-true}
+```
+
+⚠ The development stack defaults this one to **`true`**, unlike its two neighbours — that single
+character is what delivers SC-008 (an engineer gets dark with zero further steps). The env var still
+overrides, so an engineer who wants light can set it without editing the file.
+
+⚠ Decided: the **root** `docker-compose.yml` gets **no passthrough at all** (T031). It is used for
+deployments beyond the dev stack, and while dark is alpha such a deployment cannot enable the flag
+by setting the env var on the host — there is no line to carry it. That consequence is accepted
+deliberately, not an oversight.
+
+## What the flag governs
+
+While dark is alpha the flag does two jobs at once. This is deliberate compression, not conflation —
+they separate when the flag is removed.
+
+| Flag | Theme setting offered | Default for a user who has not chosen |
+|---|---|---|
+| `false` | **No** — the field is absent entirely | light |
+| `true` | Yes — light / dark (alpha) / match-system | **dark** |
+
+⚠ With the flag off the field is hidden **entirely**, not reduced to light-only. Offering "light" and
+"match system" would leave a hole: a user on a dark operating system selects match-system and reaches
+the alpha palette, defeating the flag. A one-option picker is also not a setting.
+
+## Behavioural contract
+
+| Given | When | Then |
+|---|---|---|
+| Flag `true`, no stored preference | app loads | dark, whatever the operating system says |
+| Flag `true`, no stored preference, light OS | app loads | **dark** — the default never consults the system |
+| Flag `false`, no stored preference, dark OS | app loads | **light**, and no theme setting is rendered |
+| Flag `true`, user chose light | app loads | light — the user's choice beats the default |
+| Flag `true`, user chose match-system | OS appearance changes | follows live, because they asked for it |
+| Flag flipped `true` → `false`, user had dark stored | app loads | light; **the stored preference is retained** |
+| Flag flipped back `false` → `true` | app loads | that user's dark choice is honoured again |
+| Any state | anonymous request to `/api/config` | succeeds; no version information disclosed |
+
+## Consumer contract
+
+```text
+if (!config.experimental_features.dark_theme) → light; render no theme field
+else choice = effective.theme.value ?? DARK
+ resolved = choice == SYSTEM ? (prefers-color-scheme: dark ? dark : light) : choice
+```
+
+`resolved` is mirrored to local storage so the next load's pre-paint script paints correctly from the
+first frame.
+
+⚠ The pre-paint script's empty-cache fallback is **light**, never `prefers-color-scheme`. It runs
+before the config payload has arrived, so it cannot know whether the flag is on — and guessing from
+the operating system would put a dark-OS user into the alpha palette on a deployment where the
+feature is switched off entirely.
+
+## What this contract replaced
+
+An earlier revision published a computed `default_theme` derived from the running version's PEP 440
+pre-release status. Withdrawn: "pre-release" catches any beta or release candidate, including one a
+customer runs in their own environment, which is broader than the intended "the deployments we run".
+Following the existing experimental-settings convention targets exactly the intended deployments and
+removes the resolver, the version parsing, and the config field. See [research.md](../research.md) §R1.
diff --git a/dev/specs/infp-46-dark-theme-completion/contrast-audit.md b/dev/specs/infp-46-dark-theme-completion/contrast-audit.md
new file mode 100644
index 00000000000..81ea01eb4bf
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/contrast-audit.md
@@ -0,0 +1,44 @@
+# Contrast audit (T056, FR-021 / SC-009)
+
+Run 2026-08-18 against a live development stack, both themes, WCAG 2.1 AA thresholds
+(4.5:1 for normal text, 3:1 for large text — ≥24px, or ≥18.66px bold).
+
+## Method
+
+A Playwright sweep drives 12 routes twice (theme pinned through the storage keys the app itself
+uses), injecting an auditor that walks every visible text node and measures the colour it renders
+in against the surface it actually sits on. Colours are normalised and composited through a canvas
+pixel, so `oklch()` values and translucent layers measure as the browser paints them — a naive
+channel parse mis-reads `oklch()` as RGB bytes and produces garbage ratios, which is worth knowing
+before trusting any similar tool.
+
+Routes: `/`, `/login`, `/objects/NetworkDevice`, `/objects/NetworkDeviceType`, `/proposed-changes`,
+a proposed-change detail (with a rendered Mermaid diagram), `/branches`, `/tasks`, `/ipam`,
+`/graphql`, `/profile`, `/schema`.
+
+## Result
+
+**Zero AA failures in either theme** after the fixes below. The audit is what surfaced most of
+them — every one was invisible to review by eye:
+
+| Finding | Before | After | Fixed in |
+|---|---|---|---|
+| Sidebar active item, `text-indigo-500` both themes | 3.7 light / 4.3 dark | 6.3 / 6.2 (`--active` token) | `4739b1a5c` |
+| Avatar ramp letters, `-600` text on `-50` tiles | 2.8–4.2 on four of seven hues (light) | 4.7–7.2 light, 9.0–12.5 dark | `e217f3304` |
+| Alert close-button focus halo in dark | fixed `ring-offset-gray-50` | tokenised ring + offset | `c5cb03e34` |
+| GraphiQL logo text on its light chrome | 4.26 (vendor alpha-muted neutral) | ≥4.5 (full-strength neutral, both themes) | this audit |
+
+## Scope boundaries and limitations
+
+- **Semantic palettes are out of scope** (status/severity badge colors, syntax highlighting, diff
+ colors), per the task's boundary — they are tracked as a separate effort. Elements carrying
+ data-driven inline colors (schema-defined role colors, kind palettes) are skipped for the same
+ reason.
+- **Gradient surfaces are skipped**, not measured: the auditor cannot know which stop sits behind a
+ given glyph. The theme's gradients (`--card`, `--panel`, `--secondary`) are near-solid ramps of
+ the surfaces that *were* measured, so the residual risk is low, but it is a real hole — text
+ placed directly on a future high-range gradient would go unmeasured.
+- **SVG internals are excluded**: Mermaid themes its own output, and its labels sit on shape fills
+ rather than CSS backgrounds. The Mermaid test suite asserts its palette separately.
+- Popovers, menus and modals are audited only where a route renders them by default; the earlier
+ interaction sweep covered the common overlays by hand.
diff --git a/dev/specs/infp-46-dark-theme-completion/critiques/critique-20260817-112103.md b/dev/specs/infp-46-dark-theme-completion/critiques/critique-20260817-112103.md
new file mode 100644
index 00000000000..2cf1f0fc7ba
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/critiques/critique-20260817-112103.md
@@ -0,0 +1,220 @@
+# Critique: Dark Theme Completion
+
+**Date**: 2026-08-17 11:21:03 | **Spec**: [spec.md](../spec.md) | **Plan**: [plan.md](../plan.md)
+
+## Executive Summary
+
+**Verdict: ⚠️ PROCEED WITH UPDATES**
+
+The problem is well-evidenced (a `TODO: DELETE` in the tree, a documented handover list), the scope
+was explicitly confirmed by the requester, and the technical spine — resolve once, hand the result
+down — is the right shape. Two of the three genuinely hard questions were settled with evidence
+rather than assertion.
+
+Three must-address items. One is a real architecture violation in the plan's own file layout, caught
+against a rule this repository documents and a precedent it already implements. One is a missing
+requirement that matters more for a theme feature than for most (contrast). One is a robustness hole
+in the single piece of code that runs before everything else.
+
+None are fundamental. All three are fixed below and applied.
+
+The most interesting weakness is not a defect in any artifact: **the feature has no defined exit from
+"alpha"**. User Story 2 exists to surface visual defects through daily use, but nothing says where
+those defects go or what condition retires the pre-release marker. That is a product decision, not an
+engineering one, and it is raised as a question rather than resolved.
+
+## Product Lens Findings
+
+### 3a. Problem Validation
+
+Sound. The problem is concrete and self-evidencing: the dark palette is complete but unreachable, and
+the code carries an explicit marker saying so. Scope was proposed at four items, challenged, and
+confirmed at seven by the requester — recorded rather than assumed.
+
+**P4 (💡)** — User Story 7 is the only item whose completion depends on a merge in another
+repository. As written, this feature cannot be declared done on its own timeline. Nothing else
+depends on it, so the coupling buys nothing.
+
+### 3b. User Value Assessment
+
+Every story delivers user-visible value; US1 + US2 is a genuine MVP that would stand alone.
+
+**P5 (🤔)** — FR-003 introduces an organisation-wide default but never says who may set it. The plan
+asserts the existing global-write permission is reused; the spec should state that so the requirement
+is testable without reading the plan.
+
+### 3c. Alternative Approaches
+
+Considered and correctly rejected in research: a frontend build-time flag (baked at asset-build time,
+wrong for a per-deployment property), `installation_type` (community-vs-enterprise, wrong axis), and
+an experimental flag alone (needs per-deployment configuration, defeating SC-008). Extending the
+existing preference store rather than adding storage is the right call and is well argued.
+
+### 3d. Edge Cases & User Experience
+
+Covered well — pre-login, preference-unavailable, first paint, multi-tab, system-appearance change,
+semantically meaningful colors.
+
+**P1 (🎯) — Accessibility is absent.** For a feature whose entire subject is color, the spec never
+mentions contrast. FR-021 requires semantic colors stay "mutually distinguishable", which is about
+telling severities apart from each other — it says nothing about text remaining readable against its
+background. A dark theme can satisfy every requirement in the current spec and still be unusable.
+This is the one gap that could ship a defect the dogfooding period would rationalise as "looks fine
+to me".
+
+**P3 (💡) — No feedback path.** US2 exists to surface visual defects through daily use. Nothing says
+where a defect goes when someone finds one. Without a named destination, the dogfooding produces
+observations that evaporate, and the story's stated purpose is unmet even when the story is
+implemented perfectly.
+
+### 3e. Success Measurement
+
+Most criteria are measurable. SC-004 is verifiable by inspection, SC-005 by comparison.
+
+**P2 (💡)** — SC-008 ("continuously for the dogfooding period") is not measurable: the period has no
+length and no exit condition. Related to P3, and together they are the same underlying gap — nothing
+defines when dark stops being pre-release.
+
+## Engineering Lens Findings
+
+### 4a. Architecture Soundness
+
+The resolution model is sound: two stages, split at exactly the right seam (only the client can
+observe the operating system), with one resolved value handed to every consumer. Refusing to pass
+`"system"` to GraphiQL is the correct instinct.
+
+**E1 (🎯) — The plan's file layout violates this repository's layer rules.** `plan.md` places the
+theme provider at `entities/preferences/ui/theme-provider.tsx` and simultaneously has
+`shared/components/editor/markdown/*` and `shared/components/data-viewer/*` consume the resolved
+theme. That is a `shared/` → `entities/` import, which
+`dev/knowledge/frontend/entities-structure.md` prohibits: an entity's component "may be imported by
+other entities and by higher layers — **never by `shared/`**".
+
+The repository already solves this exact problem. `DatePreferencesProvider` lives in
+`entities/preferences/ui/` but fills a context declared in
+`shared/context/date-preferences-context.tsx`, whose docstring states it holds only
+`{ pattern, timezone }` and "never imports `entities`".
+
+Aggravating factor: these rules are "enforced by review only — there is no lint guard". Nothing would
+catch this automatically, so a plan that names the wrong location is likely to become code that has
+the wrong dependency direction.
+
+### 4b. Failure Mode Analysis
+
+Degradation is thought through — preference unavailable falls back to mirror, then to deployment
+default.
+
+**E2 (🎯) — The pre-paint script is not failure-safe.** It is the first thing that runs, it blocks
+rendering, and the plan gives it no error handling. Two concrete failures:
+
+- `localStorage` access **throws** when storage is disabled or unavailable (Safari private browsing
+ being the classic case). An uncaught throw in a blocking `` script degrades the load for a
+ cosmetic feature.
+- The stored value is applied to the document element without validation. Same-origin, so this is
+ robustness rather than a live vulnerability — but reading a string from storage and using it to
+ drive a class is a shape that should validate against the known set on principle.
+
+### 4c. Security & Privacy Review
+
+Good, and one decision is better than it first appears: publishing the **resolved** default rather
+than the version keeps the version string off an unauthenticated endpoint, where it would newly leak
+build information. Worth keeping explicit so a later "simplification" doesn't undo it.
+
+No CSP is configured anywhere in the backend or the HTML shell, so the inline script raises no
+policy problem today. ⚠ Recorded because it is exactly the kind of constraint added later that
+silently breaks an inline script.
+
+### 4d. Performance & Scalability
+
+No new queries or round trips; `theme` rides payloads already fetched. The one real hazard — the
+Mermaid plugin array being rebuilt every render and re-running the rehype pipeline — is identified in
+research and carried into the plan's risk table.
+
+### 4e. Testing Strategy
+
+Resolution logic is pure and table-testable on both sides.
+
+**E3 (💡)** — The inline pre-paint script lives in `index.html`, outside the module graph, so Vitest
+cannot reach it. It is the mechanism for FR-006 and SC-002 — the hardest requirement and the one most
+likely to regress silently — and would ship with no automated coverage. It needs end-to-end coverage
+specifically, not just the manual throttling check in the quickstart.
+
+### 4f. Operational Readiness
+
+Rollback is inherently clean: the default is derived, never written, so nothing to unwind. The
+version-flip consequence (a deployment moving pre-release → release changes what un-chosen users see)
+is identified and correct.
+
+### 4g. Dependencies & Integration Risks
+
+**E4 (💡)** — The GraphiQL binding relies on two behaviours verified by reading a bundled sourcemap,
+not documented public API: that `forcedTheme` is reactive via an effect, and that setting it hides
+GraphiQL's own picker. Both are load-bearing. A minor-version bump could change either without
+notice.
+
+## Cross-Lens Insights
+
+**X1 (💡) — US7's cross-repo dependency is both a product-close risk (P4) and a sequencing risk.**
+Both lenses reach the same conclusion: it should be tracked as its own deliverable so this feature
+can close, with the pointer bump landing separately. The requester explicitly asked for all seven
+items, so this is a tracking recommendation, **not** a scope reduction — the work stays in.
+
+**X2 — P1 (contrast) and E1 (layer violation) share a root cause**: both are places where the
+artifacts were written from the shape of the problem rather than checked against an external standard
+the repository already holds. The general lesson for the remaining phases is to verify against
+`dev/knowledge/` rather than reason from the code alone — which `AGENTS.md` in fact instructs.
+
+## Findings Summary
+
+| ID | Lens | Severity | Category | Finding | Suggestion |
+|----|------|----------|----------|---------|------------|
+| E1 | Engineering | 🎯 | Architecture | Plan places the theme provider so that `shared/` must import `entities/`, which the layer rules prohibit | Declare the context in `shared/context/theme-context.tsx`; fill it from `entities/preferences/ui/theme-provider.tsx`, mirroring `DatePreferencesProvider` |
+| P1 | Product | 🎯 | Accessibility | No contrast requirement; a compliant dark theme could still be unreadable | Add an explicit contrast requirement and a success criterion |
+| E2 | Engineering | 🎯 | Failure Modes | Pre-paint script has no error handling and applies an unvalidated stored value | Wrap storage access in `try`/`catch`; validate against the known set before applying |
+| P2 | Product | 💡 | Success Measurement | SC-008 has no duration and no exit condition | Make the dogfooding period time-bound with a stated exit criterion |
+| P3 | Product | 💡 | User Value | No destination for defects the dogfooding is meant to surface | Name where reports go |
+| E3 | Engineering | 💡 | Testing | The pre-paint script is untestable by Vitest, yet implements the hardest requirement | Require end-to-end coverage of first-paint correctness |
+| E4 | Engineering | 💡 | Dependencies | GraphiQL binding relies on behaviour verified from a sourcemap, not public API | Record the version dependency; re-verify on upgrade |
+| P4 | Product | 💡 | Scope | US7 gates this feature's completion on another repository | Track as a separate deliverable; keep the work in scope |
+| X1 | Both | 💡 | Scope × Sequencing | Same as P4 from both lenses | As above |
+| P5 | Product | 🤔 | Requirements | FR-003 does not say who may set the organisation default | State that it reuses the existing global-write permission |
+
+## Remediation Applied
+
+Per the autonomous execution mode this critique runs under, must-address items were applied rather
+than offered. Low-risk recommendations were applied; the rest are recorded for the requester.
+
+**Applied to `spec.md`:**
+
+> Post-merge note: this critique is a point-in-time record. The requirement it added as "FR-022"
+> was renumbered to **FR-021** when the spec's own FR list was consolidated; SC-009 kept its
+> number. Read the identifiers below against that mapping.
+
+- **P1** — added **FR-022** (contrast) and **SC-009** (verifiable contrast outcome).
+- **P5** — FR-003 now states the organisation default reuses the existing global-preference write
+ permission.
+- **E3** — SC-002 now requires first-paint correctness to be covered by an automated end-to-end
+ check, not manual observation alone.
+- **P4/X1** — the Dependencies section now records that US7 completes on the upstream repository's
+ timeline and is tracked as a separate deliverable, with the scope confirmation noted.
+
+**Applied to `plan.md`:**
+
+- **E1** — the project structure now declares `shared/context/theme-context.tsx` and states the
+ import direction explicitly, with the `DatePreferencesProvider` precedent named. Added to the risk
+ table, flagged that no lint guard exists.
+- **E2** — Phase A step 7 now requires the pre-paint script to be exception-safe and to validate the
+ stored value; added to the risk table.
+- **E4** — the GraphiQL risk row now records that the relied-upon behaviour is not documented public
+ API.
+
+**Not applied — for the requester:**
+
+- **P2** and **P3** are the same underlying gap: nothing defines when dark stops being pre-release,
+ or where the defects found in the meantime are collected. These are product decisions belonging to
+ whoever owns the dogfooding period, and inventing an answer would be worse than surfacing the
+ question.
+
+## Post-Critique
+
+Re-check after remediation: no must-address items remain. Proceed to task generation.
diff --git a/dev/specs/infp-46-dark-theme-completion/data-model.md b/dev/specs/infp-46-dark-theme-completion/data-model.md
new file mode 100644
index 00000000000..296d2516051
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/data-model.md
@@ -0,0 +1,177 @@
+# Data Model: Dark Theme Completion
+
+**Feature**: [spec.md](./spec.md) | **Research**: [research.md](./research.md) | **Date**: 2026-08-17
+
+Three entities from the spec, plus the resolution chain that connects them. Nothing here is a new
+storage concept: the theme preference is a new field on an existing record, and the feature flag is
+read from configuration, never persisted.
+
+## Entities
+
+### Theme
+
+A closed set of appearance choices. Persisted as its member name, like the existing `DateFormat`.
+
+| Value | Meaning |
+|---|---|
+| `LIGHT` | Always the light palette |
+| `DARK` | Always the dark palette. Pre-release (FR-008) |
+| `SYSTEM` | Follow the operating system's current appearance |
+
+`SYSTEM` is stored as an explicit choice, not as absence. Absence means "inherit" and is represented
+by `null`, exactly as `date_format` and `timezone` already do. Conflating the two would make "follow
+my OS" indistinguishable from "I never chose", so a user could never return to system-following after
+setting anything else.
+
+### Theme preference (a field, not a record)
+
+`theme` joins the existing `Preference` record rather than introducing storage of its own.
+
+```text
+Preference (StandardNode)
+ owner_id : str # account id, or GLOBAL_OWNER_ID sentinel
+ date_format : Optional[DateFormat] # existing
+ timezone : Optional[str] # existing
+ theme : Optional[Theme] # NEW — null means "not set at this layer"
+```
+
+Constraints inherited from the existing record, both load-bearing:
+
+- **`Optional[Theme]`, never `Theme | None`.** `StandardNode.guess_field_type` requires the former;
+ this is documented in `models.py` and is not lifted by Python 3.14.
+- **One row per owner**, with user rows keyed by account id and a single global row keyed by the
+ `GLOBAL_OWNER_ID` sentinel. Writes serialise per owner through `PREFERENCE_LOCK_NAMESPACE`.
+- **Reads never create a row.** A missing row is "nothing set at this layer".
+
+Adding a nullable field to a `StandardNode` is additive: rows written before this change have no
+`theme` property and read back as `None`, which is already a valid, meaningful state. No data
+migration is expected — see the governance flag in [research.md](./research.md) §R5.
+
+### Theme feature flag
+
+A per-deployment boolean, read from configuration, never stored against a user. It is not derived
+from anything — the deployment states it.
+
+```text
+dark_theme : bool = INFRAHUB_EXPERIMENTAL_DARK_THEME, default false
+```
+
+While dark is alpha it governs two things at once:
+
+| `dark_theme` | Theme setting offered | Default for a user who has not chosen |
+|---|---|---|
+| `false` | none — the field is absent | `LIGHT` |
+| `true` | `LIGHT` / `DARK` (alpha) / `SYSTEM` | `DARK` |
+
+Both defaults are concrete palettes, never `SYSTEM`, and both directions are deliberate:
+
+- **Flag off gives `LIGHT`, not `SYSTEM`.** Dark is alpha, so it is reached only by a user's own
+ choice. Deferring to the operating system would put dark-OS users into it by inference — and on a
+ deployment where the feature is switched off entirely, there is no choice to infer from.
+- **Flag on gives `DARK`, not `SYSTEM`.** Following the system would leave every engineer on a light
+ machine out of the dogfooding, which is the flag's whole point.
+
+`SYSTEM` remains available to users as an explicit choice wherever the flag is on — it is simply
+never a default.
+
+The two jobs separate when the flag is removed: the production default then becomes its own decision
+rather than a consequence of the gate.
+
+It is a *default*, not a value written anywhere: it never touches a stored preference (FR-013), so
+flipping the flag changes what un-chosen users see and changes nothing for users who chose.
+
+## Resolution chain
+
+Two distinct stages, deliberately separated. Conflating them is the mistake that makes GraphiQL and
+the application disagree (see [research.md](./research.md) §R3).
+
+### Stage 1 — resolve the stored choice
+
+Server-side, identical in shape to the existing preferences:
+
+```text
+effective.theme.value = user.theme ?? global.theme ?? null
+effective.theme.source = USER | GLOBAL | DEFAULT
+```
+
+The chain is the existing one, unchanged — but **theme is exposed at the user scope only**, so no
+interface writes the global layer and `global.theme` is always `null` in practice. The chain
+therefore reduces to `user.theme ?? null`. Nothing needs removing from the backend to achieve this:
+the mutation's `scope` argument is shared across fields, so the global layer simply has no writer.
+When an organisation-wide default is added later, the chain already supports it.
+
+`source` reports which layer answered, so the interface can say "Your preference" versus falling
+through to a default — the convention `preference-fields.tsx` already implements.
+
+When the chain yields `null` (source `DEFAULT`), the client substitutes the flag's default: `DARK`
+where the flag is on, `LIGHT` where it is off.
+
+### Stage 2 — resolve to a concrete palette
+
+Client-side, because only the client knows the operating system's appearance:
+
+```text
+resolved : LIGHT | DARK
+ = LIGHT when choice is LIGHT
+ | DARK when choice is DARK
+ | (prefers-color-scheme: dark) ? DARK
+ : LIGHT when choice is SYSTEM
+```
+
+`resolved` is a strict two-value output. Every consumer — the document class, GraphiQL's
+`forcedTheme`, Mermaid's `mermaidConfig.theme`, the schema visualizer — takes `resolved`, never the
+raw choice. That is what guarantees they cannot drift from the application or from each other.
+
+Stage 2 re-runs when the operating system's appearance changes while the page is open (FR-007), which
+is why it lives in the client and not in the resolution the server returns.
+
+## Client-side mirror
+
+A `localStorage` mirror of the resolution, existing solely to make the first paint correct (FR-006).
+
+| Key | Holds | Written when |
+|---|---|---|
+| choice | the stored choice, or the flag's default | the effective preference resolves |
+| resolved | `light` or `dark` | stage 2 completes |
+
+Read synchronously by the inline classification script before first paint. It is a cache, never a
+source of truth: the account-backed preference always wins on arrival, and a cleared mirror costs one
+corrected repaint rather than a wrong theme.
+
+⚠ On a cold start the mirror is empty and the fallback is **light** — not the browser's appearance.
+The script runs before the config payload arrives, so it cannot know whether the flag is even on;
+guessing from the operating system would put a dark-OS user into the alpha palette on a deployment
+where the feature is switched off entirely. Where the flag is off, light is also the final answer, so
+a first-ever visit is correct; where it is on, that visit paints light and corrects to dark once the
+config payload lands. That single frame is accepted — it affects flag-enabled deployments only.
+
+Cross-tab synchronisation is out of scope; a second tab picks up a change on its next load.
+
+## Relationships
+
+```text
+Account ──owns──▶ Preference(owner_id = account id).theme ─┐
+ ├─▶ effective choice ─▶ resolved ─▶ consumers
+Deployment ─────▶ dark_theme flag ─▶ default (DARK|LIGHT) ─┘ (document class,
+ GraphiQL,
+Operating system ─────────────────▶ (stage 2 only, and only for an explicit SYSTEM choice) Mermaid,
+ visualizer)
+```
+
+No organisation edge: the global `Preference` row exists for `date_format` and `timezone`, but
+nothing writes `theme` into it in this version.
+
+## Validation rules
+
+- `theme` accepts only `Theme` members; unknown values are rejected at construction, including on
+ load from the database — the behaviour `date_format` already relies on by being enum-typed.
+- Writing `null` clears the override at that layer and re-exposes the layer below; it is not an error
+ and is how a user returns to "Automatic (inherited)".
+- Writing the global layer requires the same permission as the existing global preference writes; no
+ new permission is introduced.
+- The flag's default is always concrete (`LIGHT` or `DARK`) and never `SYSTEM`. This is a policy
+ constraint, not a technical one — stage 2 could resolve a `SYSTEM` default perfectly well. It is
+ excluded because a defaulted user must never reach the alpha palette by inference, and because a
+ system-following default would defeat the dogfooding.
+- Turning the flag off MUST NOT delete a stored `theme`. The value is ignored while unreachable and
+ honoured again if the flag returns; a configuration change must never destroy user data.
diff --git a/dev/specs/infp-46-dark-theme-completion/handoff-schema-visualizer.md b/dev/specs/infp-46-dark-theme-completion/handoff-schema-visualizer.md
new file mode 100644
index 00000000000..9fadb55c159
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/handoff-schema-visualizer.md
@@ -0,0 +1,142 @@
+# Handoff: dark theme for the schema visualizer (Phase 9, T052–T055)
+
+Self-contained brief for a fresh session. Everything below was verified against the code on
+2026-08-18; re-verify anything load-bearing before acting on it.
+
+## The task
+
+Four tasks from [tasks.md](./tasks.md), Phase 9 (US7):
+
+- **T052** — Open a PR on `opsmill/infrahub-schema-visualizer` adding dark support: canvas, nodes,
+ edges, labels and controls, **with the theme accepted from the embedding application rather than
+ detected independently** (no `matchMedia`, no OS detection inside the package).
+- **T053** — Get it merged and released upstream.
+- **T054** — Bump the submodule pointer in `opsmill/infrahub` and pass the resolved theme in.
+ ⚠ Never point the submodule at an unpushed commit — it breaks every other checkout.
+- **T055** — Confirm no visualizer styling code landed in the infrahub repo (FR-016).
+
+## Where the code lives
+
+- Upstream repo: `https://github.com/opsmill/infrahub-schema-visualizer`.
+- Vendored in infrahub as a git submodule at `frontend/packages/schema-visualizer/`
+ (pointer at the time of writing: `f7d3cc5af`). Fresh worktrees leave it **uninitialized**
+ (`git submodule update --init frontend/packages/schema-visualizer`); while uninitialized,
+ `betterer ci` in `frontend/app` reports 2 phantom TS issues — that's the known cause, not a
+ regression.
+- The infrahub app consumes it as a pnpm workspace package (`"infrahub-schema-visualizer":
+ "workspace:*"`), imported by exactly two files:
+ - `frontend/app/src/pages/schema/graph.tsx` (the `/schema` graph page — main embed)
+ - `frontend/app/src/entities/path-traversal/ui/path-flow-graph.tsx` (utility imports)
+- The package has its own `AGENTS.md` and `guidelines/` (naming, typescript, styling,
+ component-patterns). Notable: **tab indentation** (biome), exports only via root `index.ts`,
+ must not depend on `frontend/app` internals, and component files must stay Node-API-free.
+
+## The two builds — this is the crux
+
+The package renders in two hosts with different styling pipelines:
+
+1. **Inside the infrahub app.** The package ships **no CSS** to the app. Instead the app's
+ Tailwind build scans the package source and generates its utilities:
+ `frontend/app/src/app/styles/index.css` line ~8:
+ `@source "../../../../packages/schema-visualizer/src/**/*.{js,ts,jsx,tsx}";`
+ Consequence: any class the package uses is compiled with the **app's** Tailwind config —
+ including the app's `@custom-variant dark (&:where(.dark, .dark *))` in
+ `frontend/packages/ui/src/styles/theme.css`.
+2. **The VS Code webview** (`vite.config.webview.ts` + `src/webview.css`). `webview.css` is the
+ standalone theming hook: it does `@import "tailwindcss"` and then hand-maintains a set of
+ fallback utilities **in hex** under `.schema-visualizer-root` (scrollbars, hovers, shadows,
+ focus rings — all light-only today). Any dark strategy must work here too, without the app.
+
+## Current state of the package (surveyed, exact)
+
+- **Zero `dark:` variants anywhere.** Fully light-hardcoded.
+- ~137 fixed-palette utility usages, heavily concentrated:
+ `text-gray-600` ×34, `text-gray-500` ×27, `bg-gray-100` ×26, `text-gray-400` ×24,
+ `text-gray-700` ×20, `border-gray-200` ×13, `border-gray-100` ×12, `bg-white` ×10,
+ plus indigo actives (`text-indigo-600`, `bg-indigo-600`, `border-indigo-500`…) and a tail of
+ one-offs.
+- **Semantic hex palette** in `src/utils/schema-to-flow.ts` (`getEdgeColorForType`):
+ `#009966` generics + inherited edges, `#7F22FE` profiles, `#F54900` templates, `#087895` nodes.
+ The same hexes are duplicated as arbitrary classes in `src/components/panels/legend-panel.tsx`
+ (`bg-[#087895]` etc.) — edge colors and legend swatches must stay in lockstep.
+- **ReactFlow** (`@xyflow/react` v12) in `src/components/graph/schema-visualizer.tsx` does **not**
+ set `colorMode`. v12 has a `colorMode: "light" | "dark"` prop that themes React Flow's own
+ chrome (controls, minimap, selection, attribution) — use it rather than restyling that chrome
+ by hand. The dotted `` also needs a dark-legible color.
+- `webview.css` sets light hex `color`/`background-color` on `.schema-visualizer-root` and light
+ scrollbar colors.
+
+## Design constraints already decided (in spec.md / by the user)
+
+- Theme comes **from the embedder**. In the app that's a prop; in VS Code the webview entry may
+ map VS Code's own theme class (`body.vscode-dark`) to the package's dark state — that counts as
+ "from the embedder".
+- The app side will pass the resolved theme from
+ `frontend/app/src/shared/hooks/use-resolved-theme.ts` (`useResolvedTheme()` — a
+ `useSyncExternalStore` over a MutationObserver on the document element's class). It re-renders
+ on toggle, so the graph re-themes live.
+- FR-016: no visualizer styling lands in the infrahub repo. Tokens/variants for the package live
+ **in the package**.
+- Dark palette direction in the app is warm (stone-based, `--background: black`,
+ surfaces `stone-800/900`, `white/5..10` tints). The visualizer should harmonize, not match
+ token-for-token.
+
+## Recommended approach (weighed, not yet reviewed by the user)
+
+Add a `theme?: "light" | "dark"` prop (default `"light"`) to `SchemaVisualizer`:
+
+- Drives `colorMode` on ``.
+- Sets a marker class (e.g. `sv-dark`) or `data-theme="dark"` on the package's root container.
+- Package defines its **own** small token layer in a package CSS file — light values on the root
+ container, dark overrides under the marker — and components use those tokens
+ (Tailwind 4 `bg-(--sv-surface)` arbitrary-value syntax compiles fine under the app's `@source`
+ scan). Both the app build and `webview.css` import the same token file.
+
+Why not plain `dark:` variants keyed on the app's `.dark`: it silently couples the package to the
+app's `@custom-variant`, which the app plans to delete once fully tokenized (T027 in tasks.md),
+and it does nothing for the webview build. Self-contained tokens satisfy both hosts and FR-016.
+If the fresh session finds a simpler path that keeps both hosts working, take it — but keep the
+"no independent detection" rule absolute.
+
+For the hex semantic palette (edges/legend): these are categorical colors, mid-tone enough that
+they may survive dark as-is — **measure, don't guess** (see contrast method below). If they need
+dark counterparts, define both ends in one place shared by `schema-to-flow.ts` and the legend.
+
+## Contrast methodology (used across this feature; reuse it)
+
+WCAG AA: 4.5:1 for normal text, 3:1 for large text/graphics. Measure the **composited** color —
+paint candidate colors into a canvas 1×N, stacking translucent layers over the real page
+background, read pixels back, then compute relative luminance. Two traps hit during this feature:
+Tailwind only generates classes that appear in source (probing a class that's never in source
+silently resolves to transparent — always echo the resolved color in the probe output), and
+`rtk`-filtered output garbles verification (use `/usr/bin/git` and plain tools when verifying).
+
+## Verifying in the live app (after T054, or with a local `file:` link during development)
+
+- The user's stack: old backend image on `:8000` (its `/api/config` lacks `dark_theme` — that's
+ fine, the frontend dev-server fallback enables the theme when the flag is absent under
+ `import.meta.env.DEV`), Vite dev server on `:8080` (`.claude/launch.json`, name `frontend-dev`).
+- Dark is the default; the switch lives in the account menu (bottom-left ellipsis →
+ "Light theme / Dark theme", alpha badge). It works logged-out.
+- The visualizer page: `/schema` (renders `pages/schema/graph.tsx`).
+- Pre-paint script reads `localStorage["infrahub.theme.resolved"]`; clear storage for a
+ fresh-visitor run.
+
+## Workflow order (from the root AGENTS.md — submodule discipline)
+
+1. Branch + implement + PR **on the upstream repo** first. Run the package's own gates
+ (`npm run lint`, its vitest browser tests, both builds).
+2. Merge upstream (T053).
+3. Only then, in infrahub: bump the submodule pointer, pass `theme={useResolvedTheme()}` at the
+ embed site(s), and open that as a follow-up commit/PR on the stacked branch
+ `dark-theme-completion-infp-46` (draft PR #10295, base `bab-dark-theme-app`).
+4. T055 check: `git diff` on the infrahub side must contain no visualizer styling — only the
+ pointer bump and the prop.
+
+## Related context (only if needed)
+
+- Spec: [spec.md](./spec.md) (US7, FR-016), plan: [plan.md](./plan.md) (R8 covers the visualizer).
+- The stacked-PR series: #10284 (`bab-dark-theme-app`, base) ← #10295
+ (`dark-theme-completion-infp-46`, this feature).
+- Remaining sibling work, not this session's problem: Phase 3 user preference (GraphQL — Ask
+ First gate), Phase 10 cross-cutting (contrast audit, changelog, docs, `/pre-ci`).
diff --git a/dev/specs/infp-46-dark-theme-completion/plan.md b/dev/specs/infp-46-dark-theme-completion/plan.md
new file mode 100644
index 00000000000..c5337169a17
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/plan.md
@@ -0,0 +1,291 @@
+# Implementation Plan: Dark Theme Completion
+
+**Branch**: `dark-theme-completion-infp-46`, **stacked on `bab-dark-theme-app`** (PR #10284) — the
+pull request targets that branch, not `develop`, and re-targets `develop` once #10284 merges.
+
+**Date**: 2026-08-17 | **Spec**: [spec.md](./spec.md)
+
+**Ticket**: [INFP-46](https://opsmill.atlassian.net/browse/INFP-46)
+
+**Input**: [spec.md](./spec.md), informed by [research.md](./research.md)
+
+## Summary
+
+The dark palette exists but is unreachable: it is activated only by a development-only
+`@custom-variant` and a manually added class. This plan makes it reachable, binds every surface that
+currently pins itself to light, retires the token debt, and turns the development stack dark by
+default so the team dogfoods it.
+
+The technical spine is a **single resolution, computed once and handed down**. A stored choice
+(`LIGHT`/`DARK`/`SYSTEM`) resolves server-side at the user layer; the client
+resolves `SYSTEM` against the operating system to a strict `light`/`dark`; every consumer — the
+document class, GraphiQL, Mermaid, the schema visualizer — takes that one resolved value. No
+consumer runs its own `prefers-color-scheme` check, which is what makes them incapable of drifting
+apart.
+
+Two decisions carry most of the risk and are settled in [research.md](./research.md): the
+dogfooding gate is an experimental flag following the convention the two existing experimental
+settings already use — off by default, enabled in the development stack (§R1); and the first paint is
+owned by an inline classification script reading a `localStorage` mirror, not by React (§R2).
+
+## Technical Context
+
+**Language/Version**: Python 3.14 (backend), TypeScript 5.9 (frontend)
+
+**Primary Dependencies**: FastAPI 0.131, Graphene, Pydantic 2.12; React 19.2, Vite 8.0, Tailwind CSS
+4.2, `graphiql` 5.2.4, `rehype-mermaid`, `infrahub-schema-visualizer` (submodule)
+
+**Storage**: Neo4j — one additional nullable field on the existing `Preference` `StandardNode`. No
+new records, no data migration expected.
+
+**Testing**: pytest 9.0 (backend unit), Vitest 4.1 (frontend unit), Playwright 1.60 and
+pytest/testcontainers (end-to-end)
+
+**Target Platform**: Web application, evergreen browsers
+
+**Project Type**: Web — Python backend + React frontend, plus one external repository
+
+**Performance Goals**: Correct theme in the first painted frame on every load after a browser's first
+visit. Theme switching repaints without a reload and without re-running the Mermaid pipeline on every
+React render.
+
+**Constraints**: The light theme must be visually unchanged (FR-020). Text must stay legible against
+its surface in both themes (FR-021); semantic palettes are out of scope. The login page must be
+themed before a session exists.
+
+**Scale/Scope**: 3 backend layers (constants/model/GraphQL) + 1 boolean setting; ~40 existing CSS
+tokens reused, extended where a role had no token yet (implementation added `--content`,
+`--content-muted`, `--content-strong`, `--active`, `--active-surface`); ~20 application files
+carrying hardcoded variants; 1 external repository.
+
+## Constitution Check
+
+*GATE: evaluated before Phase 0 research and re-checked after the Phase 1 design below.*
+
+| Principle | Assessment |
+|---|---|
+| **I. Schema-Driven Integrity** | ⚠ **Gate — requires sign-off.** Generated artifacts change: `schema/schema.graphql`, `schema/openapi.json`, `frontend/app/src/shared/api/rest/types.generated.ts`. All are regenerated, never hand-edited, and committed. `AGENTS.md` lists GraphQL schema modifications and database schema changes as **Ask First**; see [Open governance points](#open-governance-points). |
+| **II. Branch-Safe by Default** | ✅ Not applicable in substance. `Preference` is a `StandardNode` outside the branched graph, and a theme has no temporal or per-branch meaning. No branch-aware queries are introduced. |
+| **III. Type Safety & Explicit Contracts** | ✅ `Theme` is a closed enum end to end — rejected at construction on read, typed through GraphQL, and a discriminated union on the client. ⚠ Two inherited constraints must be honoured: `Optional[Theme]` not `Theme \| None` (`StandardNode.guess_field_type`), and single-line enum descriptions (SDL printer stability). |
+| **IV. Test Discipline** | ✅ Resolution logic (both stages) and the three-state mutation argument are pure functions with table-driven unit tests. The flag's two states are tested by setting the setting directly, never by faking a deployment. |
+| **V. Query Performance & Efficiency** | ✅ One extra nullable property on a record already fetched. No new queries, no new round trips — `theme` rides the existing effective-preferences query and the existing config payload. |
+| **VI. Security & Input Boundaries** | ✅ The config payload gains one boolean on a field it already carries; no version information is exposed to anonymous callers. Theme is user-scoped only, so no permission surface is touched at all. |
+| **VII. Simplicity & Maintainability** | ✅ Extends the existing preference machinery rather than adding storage. Removes more than it adds: the `@custom-variant` escape hatch, ~20 files of hardcoded variants, and GraphiQL's now-redundant theme picker. |
+
+**Post-design re-check**: no violations introduced. [Complexity Tracking](#complexity-tracking) is
+empty.
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+dev/specs/infp-46-dark-theme-completion/
+├── spec.md
+├── research.md
+├── data-model.md
+├── quickstart.md
+├── plan.md # this file
+├── contracts/
+│ ├── graphql-preferences.md
+│ └── rest-config.md
+├── checklists/
+│ └── requirements.md
+└── tasks.md # produced by the tasks phase
+```
+
+### Source code
+
+```text
+backend/infrahub/
+├── core/preferences/
+│ ├── constants.py # + Theme enum
+│ └── models.py # + Preference.theme: Optional[Theme]
+├── graphql/
+│ ├── types/preferences.py # + Theme, EffectiveTheme; += EffectivePreferencesType, RawPreferencesType
+│ ├── queries/preferences.py # resolve theme through the existing chain
+│ └── mutations/preferences.py # + theme argument, _UNSET three-state handling
+└── config.py # + ExperimentalFeaturesSettings.dark_theme: bool = False
+
+development/docker-compose.yml # + INFRAHUB_EXPERIMENTAL_DARK_THEME, defaulted true
+
+frontend/app/
+├── index.html # + inline pre-paint classification script
+└── src/
+ ├── shared/context/theme-context.tsx # NEW — holds resolved "light"|"dark"; imports no entity
+ ├── entities/preferences/
+ │ ├── domain/model/preference.ts # + theme
+ │ ├── domain/rules/theme.ts # NEW — stage-2 resolution, pure (no storage access)
+ │ ├── ui/theme-provider.tsx # NEW — fills the shared context; applies class, mirrors, listens
+ │ ├── ui/preference-fields.tsx # + theme field with "alpha" tag; hidden when flag off
+ │ └── ui/queries/*.ts # + theme in query and mutation documents
+ ├── pages/graphql/index.tsx # forcedTheme="light" → resolved theme
+ └── shared/components/
+ ├── editor/markdown/
+ │ ├── markdown-with-mermaid.tsx # memoised theme-dependent plugins
+ │ └── mermaid-diagram.tsx # bg-white → token
+ └── data-viewer/data-viewer.tsx # neutral/white → tokens
+
+frontend/packages/ui/src/styles/theme.css # − @custom-variant escape hatch
+```
+
+**Structure Decision**: Web application layout. The theme preference slots into the existing
+`entities/preferences/` vertical on both sides, and the gate slots into the existing experimental
+settings, so no new architectural seam appears and **no new backend module is needed**. An earlier
+revision added `core/preferences/theme.py` to hold a version→default derivation; adopting the
+existing flag convention removed it.
+
+⚠ **The context must live in `shared/`, not in the entity.** `shared/` components consume the
+resolved theme (Mermaid, the data viewer), and `dev/knowledge/frontend/entities-structure.md`
+prohibits `shared/` from importing an entity: an entity's component "may be imported by other
+entities and by higher layers — never by `shared/`". So the dependency runs one way only:
+
+```text
+shared/context/theme-context.tsx ← declares the context, imports no entity
+ ▲ ▲
+ │ fills │ consumes
+entities/preferences/ui/ shared/components/*, pages/*
+ theme-provider.tsx
+```
+
+This mirrors `DatePreferencesProvider` exactly — it lives in `entities/preferences/ui/` and fills
+`shared/context/date-preferences-context.tsx`, whose docstring records that the shared context
+"never imports `entities`". Copy that arrangement rather than inventing one.
+
+⚠ `domain/rules` may not touch browser storage, so `domain/rules/theme.ts` stays a pure function and
+the `localStorage` mirror lives in the provider.
+
+## Implementation phases
+
+Ordered by dependency, not by the numbering of the original handover list. Phase A is the keystone;
+B–E are independent of each other once A exists and can proceed in parallel.
+
+### Phase A — Theme preference (US1) · P1
+
+The foundation. Everything else consumes the resolved value it produces.
+
+1. **Backend store** — `Theme` enum in `constants.py`; `theme: Optional[Theme] = None` on
+ `Preference`; repository untouched (it persists whatever the model declares).
+2. **Backend GraphQL** — per [contracts/graphql-preferences.md](./contracts/graphql-preferences.md).
+ ⚠ The mutation's three-state `_UNSET` handling is the single easiest thing to get wrong: collapsing
+ "omitted" and "null" makes an override impossible to clear.
+3. **Regenerate** `schema/schema.graphql`, then frontend types.
+4. **Frontend model and query** — extend `PreferenceValues` / `EffectivePreferences`; add `theme` to
+ the effective-preferences query and the upsert mutation.
+5. **Stage-2 resolution** — `domain/rules/theme.ts`: pure `(choice, systemPrefersDark) → "light" |
+ "dark"`.
+6. **Theme provider** — fills the shared context, applies the class to the document element, writes
+ the `localStorage` mirror, and subscribes to `prefers-color-scheme` changes (FR-007). Sits
+ alongside the existing `date-preferences-provider.tsx`, which is the established pattern for this
+ shape. No `storage` listener: cross-tab synchronisation is out of scope.
+7. **Pre-paint script** — inline in `index.html` ``, before the module script, reading the
+ mirror. Per [research.md](./research.md) §R2, a browser's first-ever visit still corrects after
+ the config payload arrives; this is an accepted, documented boundary.
+ ⚠ It runs before everything and blocks rendering, so it must fail safe. `localStorage` access
+ **throws** when storage is disabled or unavailable (Safari private browsing), and an uncaught
+ throw here degrades the whole load for a cosmetic feature — wrap it in `try`/`catch` and fall
+ through to light. Validate the stored string against the known set before applying it rather than
+ using it directly as a class name.
+ ⚠ The empty-cache fallback is **light**, not `prefers-color-scheme`. Consulting the system there
+ would put a dark-OS user into the alpha palette before any preference has been read. The cost is
+ one corrected frame on a first-ever visit to a non-production deployment; production is correct
+ because light is already its default.
+ ⚠ No Content-Security-Policy is configured today, so the inline script is fine. If one is ever
+ added it needs a nonce or hash, or the first paint silently reverts to light.
+8. **Preference field** — a `Combobox` matching the existing fields, with the pre-release marker on
+ dark and a description on "match system" making clear it can resolve to the pre-release palette
+ (FR-008). "Automatic (inherited)" remains the empty-value label.
+9. **Retire the escape hatch** — remove `@custom-variant dark` and its `TODO: DELETE` (FR-019). ⚠ Do
+ this **last** within Phase A: it is what the whole current dark rendering depends on, so removing
+ it before the provider works leaves the tree with no way to reach dark at all.
+
+### Phase B — Non-production default (US2) · P1
+
+1. `ExperimentalFeaturesSettings.dark_theme: bool = False` — a plain bool, matching `graphql_enums`.
+ No new endpoint or payload field: `experimental_features` is already on the unauthenticated
+ `/api/config`. Regenerate the OpenAPI schema and frontend REST types.
+2. Enable it in `development/docker-compose.yml`:
+ `INFRAHUB_EXPERIMENTAL_DARK_THEME: ${INFRAHUB_EXPERIMENTAL_DARK_THEME:-true}`. ⚠ That default of
+ `true` — unlike its two neighbours — is what delivers SC-008. Decide deliberately whether the root
+ `docker-compose.yml` follows suit; it reaches beyond the deployments the team runs.
+3. With the flag off, render light and **omit the theme field entirely**. ⚠ Not a light-only picker:
+ offering match-system would let a dark-OS user reach the alpha palette straight through the gate.
+4. Client substitutes it when the effective preference resolves with source `DEFAULT`.
+5. Pin the theme explicitly in both end-to-end suites so they stop depending on the build's version.
+
+### Phase C — Embedded surfaces (US3, US4) · P2
+
+1. **GraphiQL** — replace `forcedTheme="light"` with the resolved value. ⚠ Pass `"light"`/`"dark"`,
+ never `"system"`: GraphiQL would then run its own detection and could disagree with the
+ application (see [research.md](./research.md) §R3).
+2. **Mermaid** — derive `mermaidConfig.theme`; ⚠ memoise the plugin array on the resolved theme, or
+ the rehype pipeline re-runs every render; tokenise the `bg-white` container and the error banner.
+
+### Phase D — Token discipline (US5, US6) · P2/P3
+
+1. Migrate the ~20 files carrying hardcoded `dark:` variants to tokens.
+2. `shared/components/ui/badge.tsx` last and separately — twelve occurrences that likely encode
+ semantic colors. ⚠ Redesigning semantic palettes is **out of scope** (tracked separately); the
+ rule here is do not *degrade* them. Where a mechanical swap would flatten two distinct severities
+ into one, keep the distinction and note it for that separate effort.
+3. Data viewer: `neutral`/`bg-white` → tokens.
+4. Add the automated guard that makes SC-004 a standing property. `betterer` is already in CI and
+ is the lower-friction option; a lint rule is the stricter one. Decide when writing the tasks.
+5. ⚠ Verify the light theme is unchanged after every batch, not once at the end — this is the
+ constraint a token swap breaks most easily, and a late discovery is expensive to bisect.
+
+### Phase E — Schema visualizer (US7) · P3
+
+1. `git submodule update --init frontend/packages/schema-visualizer`.
+2. Upstream pull request on `opsmill/infrahub-schema-visualizer`: dark support, theme accepted from
+ the embedding application.
+3. Merge and release upstream.
+4. Bump the pointer here. ⚠ Never point at an unpushed commit.
+
+## Open governance points
+
+Flagged rather than assumed, per `AGENTS.md` **Ask First**. Both want a decision before Phase A
+starts.
+
+1. **GraphQL schema modification.** New `Theme` enum, new `EffectiveTheme` type, new field on two
+ types, new mutation argument. Additive and non-breaking, but it changes the public schema.
+2. **Persisted model change.** A nullable field on the `Preference` `StandardNode`. Expected to be
+ additive with no data migration — pre-existing rows lack the property and read as `None`, which is
+ already the valid "nothing set" state. This expectation should be confirmed by someone who owns
+ the `StandardNode` persistence path rather than taken on the reasoning alone.
+
+## Risks
+
+| Risk | Impact | Mitigation |
+|---|---|---|
+| Removing `@custom-variant` before the provider works | Dark becomes unreachable mid-branch | Sequenced last within Phase A |
+| Mermaid plugin array rebuilt per render | Continuous re-render, pinned CPU | Memoise on resolved theme; watch the profiler during US4 verification |
+| Passing `"system"` to GraphiQL | Sandbox silently disagrees with the app | Pass only resolved `light`/`dark` |
+| GraphiQL's `forcedTheme` reactivity and picker-hiding are not documented public API — both were verified by reading the bundled source of 5.2.4 | A minor bump could break the binding without notice | Record the version dependency; re-verify on upgrade; cover the binding with a test |
+| `shared/` importing `entities/` for the theme | Prohibited dependency direction, and **no lint guard exists** — layer rules are review-enforced only | Context in `shared/`, provider in the entity, per the `DatePreferencesProvider` precedent |
+| Pre-paint script throws on unavailable `localStorage` | Blocking head script degrades every load | `try`/`catch` with a light fallback; validate the value before applying |
+| Token swap alters the light theme | Breaks FR-020, the one hard preservation constraint | Verify light after every batch |
+| Semantic colors flattened during migration | Status/severity no longer distinguishable, in work explicitly scoped out of redesigning them | Migrate `badge.tsx` without degrading existing distinctions; hand anomalies to the separate effort |
+| Default flip destabilises end-to-end suites | Failures misattributed | Pin the theme in both suites; baseline only from a green post-#10284 run |
+| #10284 is revised or does not land | This branch is stacked on it, so its history moves under us | Rebase onto `bab-dark-theme-app`; Phases A–C and E do not depend on its content, only D does |
+| Flag off still leaves a route to dark via match-system | The gate leaks for any dark-OS user | Hide the theme field entirely when the flag is off — not a light-only picker |
+| Turning the flag off deletes stored preferences | A config change destroys user data | Ignore the stored value while unreachable; never delete it |
+| The flag outlives the alpha and becomes permanent | `value_db_index` in the same settings class is already a dead flag with a deprecation notice | Recorded as knowingly open-ended; revisit when the release cycle for the target version is known |
+| #10284's failing e2e checks are inherited by this stacked PR | Reviewers misread them as caused by this work | State it in the PR description; baseline only from a green post-merge run |
+| Submodule pointer moved to an unpushed commit | Breaks every other checkout | Upstream merge strictly precedes the bump |
+
+## Dependencies
+
+- PR [#10284](https://github.com/opsmill/infrahub/pull/10284) — this branch is **stacked** on
+ `bab-dark-theme-app` rather than waiting for it to merge. Phase D consumes its surfaces; the other
+ phases only inherit its base.
+- Existing account-backed preference machinery (user/global layers, effective resolution, source
+ reporting, permissions, locking).
+- `hatch-vcs` version derivation (the INFP-566 work) — Phase B.
+- `opsmill/infrahub-schema-visualizer` — Phase E only.
+
+## Complexity Tracking
+
+No constitutional violations require justification. This feature reuses an existing store, an
+existing resolution chain, an existing permission model, and an existing token system; the only new
+module is a pure function extracted for testability.
diff --git a/dev/specs/infp-46-dark-theme-completion/quickstart.md b/dev/specs/infp-46-dark-theme-completion/quickstart.md
new file mode 100644
index 00000000000..1922947d7c9
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/quickstart.md
@@ -0,0 +1,186 @@
+# Quickstart: Dark Theme Completion
+
+**Feature**: [spec.md](./spec.md) | **Plan**: [plan.md](./plan.md)
+
+How to set the work up and how to verify each user story. Written to be usable before any of the
+implementation exists.
+
+## Prerequisites
+
+```bash
+git submodule update --init frontend/packages/schema-visualizer
+```
+
+Required for User Story 7, and it also clears the two phantom `betterer` findings an uninitialised
+submodule produces in a fresh worktree.
+
+```bash
+uv pip install -e python_sdk
+```
+
+Fresh worktrees skip building the editable SDK, and `infrahub_sdk` imports fail without this.
+
+### Branch base — this is a stacked PR
+
+The branch is based on `bab-dark-theme-app` (PR
+[#10284](https://github.com/opsmill/infrahub/pull/10284)) and the pull request **targets that
+branch**, not `develop`. That puts the surfaces User Story 5 migrates actually in the tree and keeps
+this review free of #10284's 151 files.
+
+```bash
+git fetch origin bab-dark-theme-app && git rebase origin/bab-dark-theme-app
+```
+
+Re-target `develop` once #10284 merges; rebase again if it is revised. ⚠ #10284's failing end-to-end
+checks are inherited by this pull request — say so in the description so reviewers do not read them
+as caused by this work.
+
+## Verification commands
+
+⚠ `pnpm test` and the other `pnpm` scripts abort before running in this environment. Call the
+binaries directly.
+
+```bash
+cd frontend/app && node_modules/.bin/vitest run
+```
+
+```bash
+cd frontend/app && node_modules/.bin/biome ci .
+```
+
+```bash
+cd frontend/app && node_modules/.bin/tsc --noEmit
+```
+
+```bash
+cd frontend/app && node_modules/.bin/betterer ci
+```
+
+Backend:
+
+```bash
+uv run invoke backend.test-unit
+```
+
+After changing the GraphQL schema or config model, regenerate and commit — CI fails on stale
+generated files:
+
+```bash
+uv run invoke schema.generate-graphqlschema && uv run invoke schema.generate-jsonschema
+```
+
+```bash
+cd frontend/app && pnpm codegen
+```
+
+## Manual verification by user story
+
+Run the stack, then walk each story. The theme class lands on the document element, so the fastest
+sanity check throughout is the browser console:
+
+```js
+document.documentElement.classList.contains("dark")
+```
+
+### US1 — Choose a theme
+
+1. Sign in, open preferences. The theme field shows the flag's default with a source note
+ distinguishing it from a personal choice.
+2. The dark option carries a visible **"alpha"** tag.
+3. Select dark — the interface repaints with no reload.
+4. Reload. It is dark **in the first painted frame**. To check honestly, throttle the network hard
+ (DevTools → Network → Slow 3G) so the preference query is visibly slow: a correct implementation
+ still paints dark immediately, a broken one shows light and flips.
+5. Sign in from a second browser: dark there too.
+6. Select "match system", then switch the operating system's appearance with the page open — the
+ interface follows without a reload.
+7. Clear your choice back to the inherited default and confirm the source note reverts to reporting a
+ default rather than your own preference.
+8. ⚠ Confirm there is **no** theme field on the organisation-wide preferences form — theme is
+ user-scoped in this version.
+
+### US2 — The feature flag
+
+1. With `INFRAHUB_EXPERIMENTAL_DARK_THEME` on (the dev stack default) and **no** stored theme, load
+ the app: dark. Set your operating system to light and reload — still dark. The default ignores the
+ system deliberately, or an engineer on a light machine would never dogfood it.
+2. Confirm `GET /api/config` reports `experimental_features.dark_theme: true`, unauthenticated, with
+ no version disclosed.
+3. Turn the flag off, restart, reload: light, **and the theme field is gone from preferences** — not
+ merely reduced to light. Check "match system" is absent too; leaving it would be a hole straight
+ through the flag for anyone on a dark operating system.
+4. With the flag off, confirm a previously stored `DARK` preference is **still in the database** —
+ ignored, not deleted. Turn the flag back on and confirm that user is dark again.
+5. Clear browser storage, set the operating system to **dark**, and reload with the flag on: the
+ first paint is **light**, then corrects to dark. Both halves matter — light because the pre-paint
+ script runs before it knows whether the flag is even on, and the correction because the flag's
+ default is dark.
+
+### US3 — GraphQL sandbox
+
+1. In dark, open the GraphQL sandbox: it renders dark.
+2. Change the theme in another tab or via preferences: the sandbox follows.
+3. Confirm GraphiQL's **own** theme picker is absent from its settings dialog — with `forcedTheme`
+ set, GraphiQL hides it, which is the intended single source of truth.
+4. In light, confirm it is unchanged from today.
+
+### US4 — Mermaid diagrams
+
+1. In dark, open content containing a Mermaid diagram. Both the diagram palette and the container
+ behind it are dark — the `bg-white` wrapper is the usual culprit if the diagram looks correct but
+ sits on a bright panel.
+2. Switch the theme with the diagram on screen: it re-renders to match.
+3. ⚠ Watch the console and the React profiler while doing this. The plugin array must be memoised on
+ the resolved theme; if it is rebuilt every render the pipeline re-runs continuously and the
+ diagram flickers or the page pins a CPU core.
+4. Render a deliberately invalid diagram and confirm the error banner is legible in both themes.
+
+### US5 — Token discipline
+
+1. In dark, walk the proposed-changes flow, a diff view, the checks view and path traversal. No
+ bright surface, and borders and text match the rest of the interface.
+2. Confirm no application component paints a fixed surface palette:
+
+ ```bash
+ git grep -nP '(?`, `
`,
+and a module script. `frontend/app/src/main.tsx` is a bare `createRoot(...).render()`. There
+is no server-side rendering and no template interpolation at serve time — the same static assets are
+served to every deployment.
+
+### Decision
+
+A synchronous, render-blocking classification script inline in ``, before the module script,
+which sets the theme class on the document element from a `localStorage` mirror. React never owns
+the first paint decision.
+
+The mirror is written whenever the effective theme resolves (from the account preference, or from the
+flag's default). Precedence inside the inline script:
+
+1. Mirrored resolved theme, if present.
+2. Mirrored raw choice of "system" → resolve against `prefers-color-scheme` at that instant.
+3. Nothing mirrored → light.
+
+⚠ **Step 3 is light, not `prefers-color-scheme`.** The script runs before the config payload arrives,
+so it cannot know whether the flag is even on. Guessing from the operating system would put a dark-OS
+user into the alpha palette on a deployment where the feature is switched off entirely. Where the
+flag is off, light is also the final answer, so this fallback is correct rather than merely safe.
+
+**Known and accepted limitation**: on a browser's *first ever* visit to a **flag-enabled**
+deployment, nothing is mirrored, so the first paint is light and corrects to dark once the config
+payload arrives. Every subsequent load is correct from the first frame. Eliminating even that one
+frame would require the server to template the HTML shell, which is disproportionate for a case
+affecting flag-enabled deployments only. Recorded as a deliberate boundary, not an oversight.
+
+**Reconciliation**: when the authoritative preference disagrees with the mirror, the class is updated
+and the mirror rewritten.
+
+Cross-tab synchronisation is **out of scope** — a second tab picks up a change on its next load. The
+mirror would make a `storage`-event implementation nearly free, so this is a deliberate deferral
+rather than a limitation of the design.
+
+## R3 — How is the GraphQL sandbox bound? (FR-014)
+
+**Question**: `frontend/app/src/pages/graphql/index.tsx:24` passes `forcedTheme="light"`.
+
+### Evidence
+
+From the installed `graphiql@5.2.4` sidebar implementation:
+
+```ts
+const THEMES = ['light', 'dark', 'system'] as const;
+forcedTheme?: (typeof THEMES)[number];
+
+useEffect(() => {
+ if (forcedTheme === 'system') setTheme(null);
+ else if (forcedTheme === 'light' || forcedTheme === 'dark') setTheme(forcedTheme);
+}, [forcedTheme, setTheme]);
+```
+
+Three facts follow. The prop is reactive, so a changing value propagates without remounting. When
+`forcedTheme` is set, GraphiQL hides its own theme picker (`{!forcedTheme && …}`) — desirable, since
+the application setting becomes the single source of truth. And GraphiQL persists its own theme in
+its storage, which `setTheme` overwrites.
+
+### Decision
+
+Pass the application's **resolved** theme (`"light"` or `"dark"`), never `"system"`.
+
+⚠ Passing `"system"` would make GraphiQL run its own `prefers-color-scheme` resolution independently
+of the application's. A user on "match system" whose application resolved to dark would be correct
+only by coincidence, and would diverge from an organisation default or an explicit choice. Resolving
+once, in the application, and handing down the answer is the only binding that cannot drift.
+
+## R4 — How are Mermaid diagrams bound? (FR-015)
+
+**Question**: `markdown-with-mermaid.tsx:11` pins `mermaidConfig: { theme: "default" }`.
+
+### Evidence
+
+The file uses `strategy: "inline-svg"`, so diagrams are rendered client-side in the browser and a
+theme change can re-render them without a build step. Three obstacles are visible in the source:
+
+1. ⚠ **`rehypePlugins` is a module-level constant.** Making it theme-dependent means constructing it
+ per render. A new array identity on every render re-runs the rehype pipeline continuously — the
+ plugin array must be memoised on the resolved theme, and nothing else.
+2. **`mermaid-diagram.tsx` hardcodes `className="relative bg-white"`** on the pan/zoom container.
+ This is the bright panel behind an otherwise dark diagram, independent of the diagram's own
+ palette.
+3. **The parse-error fallback** builds a `mermaid-error` element. FR-015 requires that state legible
+ in both themes, so its styling must be tokenised alongside the container.
+
+### Decision
+
+Derive `mermaidConfig.theme` from the resolved application theme, mapping to Mermaid's built-in
+`"dark"` and `"default"`. Memoise the plugin array on the resolved theme. Tokenise the container
+background and the error banner.
+
+Mermaid's own `"neutral"` and `"forest"` themes are not used: the warm palette is not reproducible in
+Mermaid's built-ins, and matching it precisely would mean a hand-authored theme-variables object —
+disproportionate for the first pass, and a reasonable later refinement.
+
+## R5 — How does the theme preference join the existing store? (FR-001 → FR-004)
+
+**Question**: whether to extend the existing preference machinery or add separate storage.
+
+### Evidence
+
+The existing machinery is a complete two-layer implementation, and theme is structurally identical to
+`date_format` — a small closed set of keys, nullable, resolved user → global → default.
+
+Backend:
+
+- `backend/infrahub/core/preferences/constants.py` — `DateFormat`, `PreferenceSource`,
+ `GLOBAL_OWNER_ID`, `PREFERENCE_LOCK_NAMESPACE`.
+- `backend/infrahub/core/preferences/models.py` — `Preference(StandardNode)` with `owner_id`,
+ `date_format`, `timezone`; plus `ResolvedPreference[T]` and `EffectivePreferences`.
+- `backend/infrahub/core/preferences/repository.py`, `permissions.py`.
+- `backend/infrahub/graphql/types/preferences.py` — enums built with `Enum.from_enum`, and one
+ `Effective…` `ObjectType` per field carrying `value` + `source`.
+- `backend/infrahub/graphql/queries/preferences.py`, `backend/infrahub/graphql/mutations/preferences.py`.
+
+Frontend:
+
+- `entities/preferences/domain/model/preference.ts` — `PreferenceValues`, `EffectivePreferences`.
+- `entities/preferences/ui/preference-fields.tsx` — `Combobox` fields, source tooltips, and the
+ `EMPTY_VALUE_LABEL = "Automatic (inherited)"` convention for "no override".
+- `entities/preferences/ui/{preferences-form,global-preferences-form,user-preferences-card}.tsx`.
+
+### Decision
+
+Extend. Adding a `Theme` enum and a nullable `theme` field mirrors `date_format` end to end, and
+inherits resolution, source reporting, the global/user split, permissions and locking without new
+concepts.
+
+⚠ Two constraints are documented in the existing source and must be carried:
+
+- `models.py` states persisted nullable fields must be written `Optional[X]`, **not** `X | None`,
+ because of how `StandardNode.guess_field_type` works. Python 3.14 has not lifted this.
+- `types/preferences.py` states enum descriptions must stay on a single line, because
+ `graphql-core`'s SDL printer dedents multi-line descriptions inconsistently across versions and
+ makes the generated `schema/schema.graphql` environment-dependent.
+
+### ⚠ Open governance point
+
+`AGENTS.md` lists "Database schema or migration changes" and "GraphQL schema modifications" under
+**Ask First**. `Preference` is a `StandardNode`, so adding a nullable field is additive — existing
+rows simply lack the property and read as `None`, which is already the "nothing set" case. No data
+migration is expected. The GraphQL schema does change (new enum, new field on the effective-preferences
+type, new mutation input), and `schema/schema.graphql` is generated and CI-validated. Both points are
+flagged for explicit human sign-off before implementation, not assumed.
+
+## R6 — Token migration surface (FR-017, FR-018, SC-004)
+
+### Evidence
+
+`frontend/packages/ui/src/styles/theme.css` defines ~40 semantic custom properties on `:root`, a
+`.dark` block redefining the same names, and an `@theme inline` block bridging each token into
+Tailwind utilities. (Exact counts and line numbers drift as this very PR adds tokens — the
+structure is the durable fact.) The light palette is **warm** — `--background:
+var(--color-stone-100)`, `--foreground: var(--color-stone-800)`, `--card`/`--panel` built from
+`stone`/`gray` stops.
+
+`shared/components/data-viewer/data-viewer.tsx` uses `bg-neutral-800 text-neutral-200` (line 29),
+`border-neutral-700` (line 77) and two `bg-white` containers (lines 58, 89). `neutral` is Tailwind's
+cold grey; `stone` is the warm one. This is precisely the reported tone mismatch, and the two
+`bg-white` panels are a fixed light background in dark mode, which FR-018 forbids.
+
+Counted on the PR #10284 branch, roughly twenty application files still carry hardcoded `dark:`
+variants, concentrated in `entities/diff/` (node-diff, checks, conflicts, badges),
+`entities/path-traversal/`, `entities/proposed-changes/`, `entities/tasks/`, and
+`shared/components/ui/badge.tsx` (twelve occurrences, the single largest).
+
+### Decision
+
+Migrate to tokens, and add an automated guard so SC-004 holds as a standing property rather than a
+one-time cleanup. Without a guard the debt returns with the next feature branch, and SC-004's wording
+("holds as a standing property") would be unenforceable. The concrete guard mechanism — a lint rule
+versus a `betterer` counter — is left to the plan; `betterer` is already wired into CI here.
+
+`shared/components/ui/badge.tsx` is called out separately: at twelve occurrences it likely encodes
+semantic colors (status, severity). **Redesigning semantic palettes is out of scope** — that is the
+separately-tracked "content that carries its own colors" work. The instruction here is narrower and
+easier to get wrong in the opposite direction: migrate it without *degrading* what exists. Where a
+mechanical token swap would flatten two currently-distinct severities into one, leave the distinction
+in place and note it for the separate effort rather than collapsing it.
+
+## R7 — Schema visualizer (FR-016)
+
+### Evidence
+
+`git submodule status frontend/packages/schema-visualizer` reports
+`-f7d3cc5af409e9db7916947e33b887737a626d4d` — the leading `-` means **uninitialised**, and the
+directory is empty in this worktree. The package is consumed as `infrahub-schema-visualizer` from
+`frontend/packages/schema-visualizer`.
+
+`AGENTS.md` is explicit: a submodule pointer must not move to an unpushed commit, because that
+breaks every other checkout. The upstream change must be merged before the pointer bump lands here.
+
+### Decision
+
+Two deliverables in strict order: an upstream pull request against
+`opsmill/infrahub-schema-visualizer` implementing dark support and accepting the embedding
+application's theme; then a pointer bump here. No visualizer styling code lands in this repository.
+
+This is the reason User Story 7 is P3 and last: it is the only item whose completion is gated on a
+merge in another repository, and nothing else depends on it.
+
+⚠ `git submodule update --init frontend/packages/schema-visualizer` is a prerequisite for any work on
+this item, and also removes the two phantom `betterer` findings that an uninitialised submodule
+produces in a fresh worktree.
+
+## R8 — Test and verification impact
+
+Enabling the flag (FR-010) changes what the end-to-end suites see, since they run against the
+repository's own compose configuration — the same place the flag is switched on.
+
+Both suites are affected: the legacy Playwright suite (`frontend/app`, `pnpm test:e2e`) and the
+pytest/testcontainers suite (`tests/e2e`). Any assertion on a specific color, and any screenshot
+comparison, may flip.
+
+**Decision**: end-to-end runs pin the theme explicitly rather than inheriting the build-derived
+default, so the suites remain deterministic and independent of the version they happen to be built
+at. This also keeps them from silently masking a regression in the default-resolution logic, which
+gets its own targeted coverage instead.
+
+⚠ PR #10284's end-to-end checks are already failing and are out of scope. Their failures must not be
+conflated with fallout from this change; the baseline needs to be established from a green run after
+#10284 lands.
+
+Local verification runs the binaries directly rather than through `pnpm` scripts, which abort in this
+environment: `node_modules/.bin/{vitest,biome,tsc,betterer}`.
diff --git a/dev/specs/infp-46-dark-theme-completion/spec.md b/dev/specs/infp-46-dark-theme-completion/spec.md
new file mode 100644
index 00000000000..3839fba0f61
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/spec.md
@@ -0,0 +1,469 @@
+# Feature Specification: Dark Theme Completion
+
+**Feature Branch**: `dark-theme-completion-infp-46`
+
+**Ticket**: [INFP-46](https://opsmill.atlassian.net/browse/INFP-46)
+
+**Created**: 2026-08-17
+
+**Status**: Draft
+
+**Input**: Follow-up work inherited from the dark-theme series (PRs #10247 → #10284). Seven known
+limitations were recorded by the series author; this spec covers all seven.
+
+## Context
+
+A series of eleven merged pull requests tokenized the design system and swept most application
+surfaces onto theme-aware CSS custom properties. A twelfth, [#10284](https://github.com/opsmill/infrahub/pull/10284)
+("Adapt remaining app to dark theme", 151 files), is open and covers the remaining app surfaces.
+
+The result is a dark palette that exists but that **no user can reach**. Dark mode is activated only
+by manually adding a `.dark` class at the top of the cascade, via the development-only
+`@custom-variant dark` declaration in the shared theme stylesheet — which carries an explicit
+`TODO: DELETE` marker. The series author drove it with a local, uncommitted debug button.
+
+This feature closes that gap and clears the seven limitations the author recorded on handover.
+
+### Relationship to PR #10284
+
+Several items below (notably User Story 5) describe debt that #10284 *introduces* — hastily
+dark-themed legacy pages carrying hardcoded variants rather than tokens.
+
+**This work stacks on #10284**: the branch is based on `bab-dark-theme-app` and the pull request
+targets it, not `develop`. That makes the debt US5 migrates actually present in the tree, and keeps
+this review free of #10284's 151 files. When #10284 merges, this branch re-targets `develop`; if
+#10284 is revised, this branch rebases onto it.
+
+Its failing end-to-end checks are explicitly **out of scope** and are not addressed here. ⚠ Stacking
+means those failures are inherited and will appear on this pull request too — they are pre-existing,
+not caused by this work.
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Choose a theme (Priority: P1)
+
+A signed-in user opens their preferences, sees a theme setting alongside the existing date-format and
+timezone settings, and picks between light, dark, and matching their operating system. The choice
+applies immediately, survives a reload, and follows them to another browser or machine because it is
+stored with their account rather than in one browser.
+
+Because the dark palette is known to still contain visual defects, the dark choice is tagged **alpha**
+so a user opting in knows what they are accepting.
+
+**Why this priority**: This is the keystone. Every other story either binds a surface to "the
+selected theme" or adjusts how that theme looks — none of them are meaningful until a selected theme
+exists and is readable by the application.
+
+**Independent Test**: Sign in, change the theme setting, observe the application repaint without a
+reload, reload the page and observe the choice persisted, then sign in on a second browser and
+observe the same choice.
+
+**Acceptance Scenarios**:
+
+1. **Given** a signed-in user whose theme has never been set, **When** they open their preferences,
+ **Then** the theme setting shows the effective value and indicates that it comes from a default
+ rather than from their own choice.
+2. **Given** a user viewing the theme setting, **When** they look at the dark option, **Then** it
+ carries a visible **alpha** marker distinguishing it from the light option.
+3. **Given** a user on the light theme, **When** they select dark, **Then** the application switches
+ to the dark palette without a page reload.
+4. **Given** a user who has selected dark, **When** they reload the page, **Then** the application
+ paints in dark from the first frame, with no visible flash of the light theme.
+5. **Given** a user who has selected dark in one browser, **When** they sign in from a different
+ browser, **Then** the application is dark there too.
+6. **Given** a user who has selected "match system", **When** their operating system switches from
+ light to dark while the page is open, **Then** the application follows without a reload.
+7. **Given** a user who has selected a theme, **When** they clear it back to the inherited default,
+ **Then** the setting reports the value as coming from a default again rather than from their own
+ choice.
+
+---
+
+### User Story 2 - The whole feature sits behind a flag, on for the dev stack (Priority: P1)
+
+The theme feature is gated by an experimental flag. It is off by default everywhere, and turned on in
+the development stack so the team lives in dark continuously and surfaces its remaining visual
+defects through ordinary use, without any engineer configuring anything themselves.
+
+The flag does two jobs while dark is alpha: it decides whether the feature exists at all, and — where
+it exists — it makes dark the default for anyone who has not chosen. With the flag off there is no
+theme setting and the application is light. Both are deliberate: an engineer on a light system must
+still see dark or they are not dogfooding it, and a user on a deployment where the flag is off must
+have no route into the alpha palette at all.
+
+**Why this priority**: This is the stated near-term goal — dogfooding dark for the coming weeks — and
+it is what keeps an unfinished theme away from anyone who has not opted into running it.
+
+Defects found this way are reported over Slack. Naming the destination is what makes "no new defects
+were found" a claim someone can check rather than an absence of evidence.
+
+**Independent Test**: Start the dev stack with no per-engineer setup and observe dark; start with the
+flag off and observe light with no theme setting present; in both, confirm a stored preference is
+never destroyed.
+
+**Acceptance Scenarios**:
+
+1. **Given** a deployment with the flag on and a user with no theme preference, **When** they load the
+ application, **Then** it paints dark **regardless of their system appearance** — including for an
+ engineer whose operating system is light.
+2. **Given** a deployment with the flag off, **When** any user loads the application, **Then** it
+ paints light and **no theme setting is offered**. In particular "match system" is absent, so a
+ user on a dark operating system has no route to the alpha palette.
+3. **Given** the flag is on and a user has selected dark, **When** an operator turns the flag off,
+ **Then** the application renders light **and the stored preference is retained, not deleted** —
+ turning the flag back on restores their choice.
+4. **Given** a deployment with the flag on defaulting to dark, **When** a user explicitly selects
+ light, **Then** their choice is honoured and persists.
+5. **Given** an engineer starting the development stack, **When** they do nothing else, **Then** the
+ application is dark — no per-engineer configuration step exists.
+
+---
+
+### User Story 3 - The GraphQL sandbox follows the theme (Priority: P2)
+
+A user working in the GraphQL sandbox on a dark application sees the sandbox in dark too, rather than
+a bright panel embedded in a dark page.
+
+**Why this priority**: The sandbox is a full-page surface that is currently pinned to light
+regardless of the application theme, making it one of the two most jarring mismatches. It already
+ships a dark theme of its own, so the work is binding rather than building.
+
+**Independent Test**: With the application in dark, navigate to the GraphQL sandbox and confirm it
+renders dark; switch the theme and confirm the sandbox follows.
+
+**Acceptance Scenarios**:
+
+1. **Given** the application is in dark, **When** the user opens the GraphQL sandbox, **Then** the
+ sandbox renders using its dark theme.
+2. **Given** the user is in the GraphQL sandbox, **When** they change the application theme, **Then**
+ the sandbox switches to match.
+3. **Given** the application is in light, **When** the user opens the sandbox, **Then** it renders
+ exactly as it does today.
+
+---
+
+### User Story 4 - Mermaid diagrams follow the theme (Priority: P2)
+
+A user reading a document containing a Mermaid diagram on a dark application sees the diagram
+rendered for a dark background, with legible text and no bright panel behind it.
+
+**Why this priority**: Same class of mismatch as the sandbox, and diagrams appear inside ordinary
+content where a bright block is especially disruptive. Currently only partially dark.
+
+**Independent Test**: With the application in dark, view content containing a Mermaid diagram and
+confirm the diagram and its container are dark and legible; switch the theme and confirm the diagram
+re-renders to match.
+
+**Acceptance Scenarios**:
+
+1. **Given** the application is in dark, **When** a Mermaid diagram renders, **Then** the diagram
+ uses a dark-appropriate palette and its container background matches the surrounding surface.
+2. **Given** a rendered Mermaid diagram, **When** the user changes the application theme, **Then**
+ the diagram reflects the new theme.
+3. **Given** a Mermaid diagram that fails to parse, **When** it renders its error state, **Then**
+ that error state is legible in both themes.
+
+---
+
+### User Story 5 - Application surfaces use theme tokens, not hardcoded colors (Priority: P2)
+
+A user moving between pages on a dark application sees one coherent dark theme, rather than pockets
+of near-black that were bolted on page by page. Pages carried over from the legacy structure — the
+proposed-changes flow, diff and check views, path traversal — look like the rest of the application.
+
+**Why this priority**: This is the largest correctness debt and the most visible source of "almost
+dark" defects. Hardcoded per-page variants also mean every future palette change has to be repeated
+by hand in each of them, so leaving them in place taxes all later work.
+
+**Independent Test**: With the application in dark, walk the proposed-changes flow, a diff view, the
+checks view and path traversal, and confirm each uses the same surfaces, borders and text colors as
+the rest of the application. Separately, confirm no application source file specifies theme-specific
+colors directly.
+
+**Acceptance Scenarios**:
+
+1. **Given** the application is in dark, **When** the user walks the legacy pages listed above,
+ **Then** every surface, border and text color matches the shared palette.
+2. **Given** the application source, **When** it is inspected for per-theme color overrides or raw
+ color literals in application components, **Then** none remain.
+3. **Given** the application is in light, **When** the same pages are compared against their previous
+ appearance, **Then** they are visually unchanged.
+
+---
+
+### User Story 6 - The data viewer matches the theme's tone (Priority: P3)
+
+A user viewing file, artifact or object data sees a viewer whose greys belong to the same family as
+the rest of the dark theme, rather than a colder panel that reads as a foreign element.
+
+**Why this priority**: A genuine inconsistency, but a tonal one — the viewer is already dark, just
+the wrong dark. Lower user impact than surfaces that are still bright.
+
+**Independent Test**: With the application in dark, open the data viewer beside another dark surface
+and confirm the greys belong to the same family.
+
+**Acceptance Scenarios**:
+
+1. **Given** the application is in dark, **When** the data viewer renders, **Then** its background,
+ border and text colors come from the shared palette.
+2. **Given** the data viewer renders any of its content types, **When** each is displayed, **Then**
+ none of them shows a fixed light background in dark mode.
+
+---
+
+### User Story 7 - The schema visualizer supports dark (Priority: P3)
+
+A user exploring the schema visualizer on a dark application sees a dark visualizer, consistent with
+the application that embeds it.
+
+**Why this priority**: Real, but the longest lead time and the lowest coupling — the visualizer lives
+in a separate repository and must be released there before this application can consume it. Deferring
+it does not block any other story.
+
+**Independent Test**: With the application in dark, open the schema visualizer and confirm its canvas,
+nodes, edges and controls are dark and legible.
+
+**Acceptance Scenarios**:
+
+1. **Given** the application is in dark, **When** the user opens the schema visualizer, **Then** its
+ canvas, nodes, edges, labels and controls render in dark and remain legible.
+2. **Given** the application is in light, **When** the user opens the visualizer, **Then** it is
+ visually unchanged from today.
+3. **Given** the visualizer's dark support is released upstream, **When** this application adopts the
+ release, **Then** the adoption is a version change here and carries no visualizer styling code in
+ this repository.
+
+---
+
+### Edge Cases
+
+The first three are one problem with one answer, so they are grouped rather than listed apart.
+
+- **No account-backed answer yet — before sign-in, on first paint, or when the preference cannot be
+ read.** In all three the application must still paint a coherent theme immediately and never land
+ half-styled or flash.
+
+ A single mechanism covers all three: a locally cached copy of the last resolved theme, read
+ synchronously before the first frame. The account-backed preference reconciles on arrival and
+ refreshes the cache. Because the cache holds the *resolved* theme, a returning user — signed in or
+ not — paints correctly from the first frame.
+
+ With nothing cached, the fallback is light. With the flag off that is already the answer, so the
+ first-ever visit is correct. With the flag on it is not: that first visit paints light and corrects
+ to dark once the flag's value arrives. Accepted — it is one frame, on a flag-enabled deployment, on
+ a browser that has never loaded the application before. Removing it would mean the server
+ templating the HTML shell, which is disproportionate.
+
+ ⚠ The fallback is light rather than the operating system's appearance. Consulting the system here
+ would put a dark-OS user into the alpha palette before either the preference or the flag has been
+ read — exactly what FR-011 exists to prevent.
+
+- **The flag is turned off while a user has dark stored.** The application renders light; the stored
+ preference is retained untouched and honoured again if the flag returns. A config change must
+ never destroy user data.
+
+- **System appearance changes while the page is open.** A user who chose match-system switches their
+ operating system's appearance. The application follows without a reload. Cheap to support — the
+ browser exposes this as a subscribable change — so it is in scope rather than deferred.
+
+- **Existing automated tests.** Tests that assert specific colors, or that screenshot the interface,
+ are sensitive to the flag's value. In scope: the suites must pin the theme explicitly rather than
+ inherit whatever the deployment implies.
+
+- **Print and export.** Unchanged; out of scope.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+**Theme selection and persistence**
+
+- **FR-001**: The system MUST offer a theme preference with three choices: light, dark, and match the
+ operating system.
+- **FR-002**: The system MUST persist a user's theme choice against their account, so it applies on
+ any browser or machine where they sign in.
+- **FR-003**: The theme preference MUST be user-scoped only. No organisation-wide theme default is
+ offered in this version — while the feature is flag-gated to the development stack there is no
+ administrator setting a house theme for anyone. This is deferred to the moment the flag is removed,
+ when a real user for it exists.
+- **FR-004**: The system MUST report which layer an effective theme came from — the user's own
+ choice or the built-in default — consistent with how existing preferences report their source. No
+ new permission is introduced.
+- **FR-005**: Users MUST be able to change the theme and see it applied without reloading the page.
+- **FR-006**: The system MUST apply the correct theme on the first painted frame, with no visible
+ flash of the other theme.
+- **FR-007**: When "match system" is selected, the system MUST follow changes to the operating
+ system's appearance while the page is open.
+- **FR-008**: The system MUST mark the dark choice as **alpha** in the interface, so users understand
+ they are opting into something that may still contain visual defects. The handover named this
+ label specifically; "alpha" is the word to render, not a paraphrase of it. Because "match system"
+ can resolve to dark, its description MUST make that consequence clear.
+- **FR-009**: The system MUST render a coherent theme when no preference can be retrieved, falling
+ back to the last locally cached resolution and then to light. The cache MUST be read synchronously
+ before the first frame, and its absence or unavailability MUST NOT prevent the application from
+ loading.
+
+**Feature flag**
+
+- **FR-010**: The theme feature MUST be gated by an experimental flag, following the convention the
+ existing experimental settings already use: off by default, enabled per deployment through
+ configuration. The development stack MUST enable it, so an engineer gets dark by starting the
+ stack and performing no other step.
+- **FR-011**: With the flag off, the system MUST render light and MUST NOT offer a theme setting at
+ all. Offering only "light" and "match system" is not sufficient: a user on a dark operating system
+ would reach the alpha palette through match-system, defeating the flag.
+- **FR-012**: With the flag on, the system MUST default to dark for users with no personal choice,
+ **regardless of their operating system's appearance**. Following the system here would leave every
+ engineer on a light machine out of the dogfooding, which is the flag's whole purpose.
+- **FR-013**: Changing the flag MUST NOT overwrite, reset or delete any user's stored preference. A
+ stored choice that the flag makes unreachable MUST be ignored while the flag is off and honoured
+ again when it returns.
+
+**Embedded and third-party surfaces**
+
+- **FR-014**: The GraphQL sandbox MUST render in the application's active theme, and MUST follow
+ changes to it. It MUST NOT be pinned to a fixed theme.
+- **FR-015**: Mermaid diagrams MUST render using a palette appropriate to the active theme, including
+ their container background and their parse-error state, and MUST reflect a theme change.
+- **FR-016**: The schema visualizer MUST support both themes and follow the embedding application's
+ active theme. Its styling MUST be implemented in its own repository and consumed here as a released
+ version.
+
+**Token discipline**
+
+- **FR-017**: Application components MUST express color through shared theme tokens. Per-theme
+ overrides and raw color literals MUST NOT remain in application components.
+- **FR-018**: The data viewer MUST draw its surfaces, borders and text from the shared palette, and
+ MUST NOT present a fixed light background in any of its content types.
+- **FR-019**: The development-only mechanism that currently makes the dark palette reachable MUST be
+ removed once the theme preference supersedes it.
+
+**Preservation**
+
+- **FR-020**: The light theme MUST remain visually unchanged by this feature.
+- **FR-021**: Text and essential interface elements MUST remain legible against their background in
+ both themes, meeting the contrast level the light theme already achieves.
+
+ This is about text against a surface, not about telling semantic colors apart from each other.
+ Content that carries its own colors — diagrams, syntax highlighting, status and severity palettes,
+ user-supplied content — is **out of scope** and tracked separately; a migration here must not make
+ those worse, but redesigning them is not this feature's job.
+
+### Key Entities
+
+- **Theme preference**: A user's chosen appearance. One of light, dark, or match-system. Stored per
+ account only. Absent by default; absence means "fall back".
+- **Effective theme**: The appearance actually applied for a given user at a given moment. Resolved
+ from the user's choice, then the flag's default; and if the resolved choice is match-system,
+ further resolved against the operating system's current appearance.
+- **Theme feature flag**: A per-deployment switch, off by default and enabled by configuration. While
+ dark is alpha it decides both whether the feature exists and, where it does, that dark is the
+ default for users who have not chosen. It is never stored against a user and never modifies what
+ is stored against one.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: A user can change the theme and see the whole application — including the GraphQL
+ sandbox, Mermaid diagrams and the schema visualizer — reflect the change, without reloading.
+- **SC-002**: On reload, the correct theme is present in the first painted frame; no flash of the
+ opposite theme is observable. This is verified by an automated end-to-end check, not by manual
+ observation alone — it is the requirement most likely to regress silently, and the mechanism that
+ delivers it runs outside the unit-test harness.
+- **SC-003**: A theme chosen on one machine is in effect when the same user signs in on another.
+- **SC-004**: No application component paints a fixed light surface regardless of theme. Verifiable
+ by inspection, and holding as a standing property rather than a one-time cleanup.
+
+ ⚠ **Corrected during implementation.** This originally read "zero per-theme colour overrides",
+ measured by counting `dark:` occurrences. That metric was wrong in both directions. It flagged
+ files that *work* — a hardcoded `dark:` variant renders correctly, it is merely unmaintainable —
+ while completely missing the files that are actually broken, which carry no variant at all. The
+ real defects were 38 unconditional `bg-white` and 21 `bg-gray-*` literals; counting `dark:` would
+ never have found one of them. Some `dark:` uses are also legitimate and have no token equivalent,
+ such as swapping between two different logo assets.
+- **SC-005**: The light theme is unchanged: a comparison of light-theme rendering before and after
+ this feature shows no visual differences.
+- **SC-006**: Every page reachable from the main navigation renders with no bright-on-dark surface
+ when dark is active.
+- **SC-007**: With the flag on, a user with no stored preference sees dark whatever their operating
+ system says; with the flag off, every user sees light and no theme setting exists. Neither state
+ alters a stored preference, and neither consults the operating system.
+- **SC-008**: An engineer gets dark by starting the development stack and taking **zero** further
+ configuration steps. Counted literally: the number of actions between "stack is up" and "interface
+ is dark" is nought.
+- **SC-009**: Text and essential interface elements meet the same contrast level in dark as the light
+ theme already achieves, verified across the pages walked for SC-006 rather than on a sample.
+
+## Assumptions
+
+These were decided during specification rather than left open. Each is a judgement call that a
+reviewer may overturn.
+
+- **Dark is never reached by inference.** Because it is alpha, a user arrives at it only by choosing
+ it — either by selecting dark, or by selecting match-system on a dark operating system. Where the
+ flag is off there is no route in at all; where it is on, the deployment has opted in on the user's
+ behalf. The alpha tag therefore always labels something someone actually chose.
+
+ An intermediate revision defaulted production to the system appearance. It was withdrawn once the
+ consequence was explicit: it would have put dark-OS users into the alpha palette with no choice on
+ their part, which is precisely what the alpha label exists to prevent.
+- **The flag's default ignores the operating system deliberately.** Following the system would leave
+ every engineer on a light machine out of the dogfooding, which is the flag's entire purpose.
+- **Three choices, not two.** Match-system is included rather than deferred: it is the conventional
+ expectation for a theme setting, and adding it later would change the meaning of an already-stored
+ value. It is offered only where the flag is on, and only ever as an explicit choice — never a
+ default.
+- **The existing preference machinery is extended, not replaced.** Theme joins date-format and
+ timezone in the established preference model and inherits its resolution and source-reporting
+ semantics — but is exposed at the user scope only.
+- **The flag follows the existing experimental-settings convention** rather than deriving from the
+ running version. The two experimental settings already in the codebase default to `false` and are
+ enabled per deployment through configuration; this one does the same, and the development stack
+ enables it.
+
+ An earlier revision derived the default from the version's pre-release status. It was withdrawn
+ because "pre-release" catches any beta or release candidate — including one a customer runs in
+ their own environment — which is broader than "the deployments we run". Following the existing
+ convention targets exactly the intended deployments, matches how the codebase already works, and
+ removes a subsystem. The accepted trade: deployments not started from this repository's
+ configuration files are not covered and stay light unless configured.
+- **The flag has no removal date, knowingly.** It is recorded as open-ended rather than tied to a
+ release, because the release cycle for the version this would land in is not yet known. ⚠ The same
+ settings class already contains a dead experimental flag carrying a deprecation notice, so
+ flag-rot here is a realised failure mode rather than a hypothetical one.
+- **This work stacks on PR #10284.** The branch is based on `bab-dark-theme-app` and the pull request
+ targets it, rather than `develop`. #10284's surfaces are the input to User Story 5, and its failing
+ end-to-end checks are out of scope. When #10284 merges, this branch re-targets `develop`.
+- **The schema visualizer is a separate deliverable.** Upstream release precedes adoption here; the
+ adoption in this repository is a dependency version change with no styling code.
+- **Print, export and screenshot output are unchanged.**
+
+## Dependencies
+
+- PR [#10284](https://github.com/opsmill/infrahub/pull/10284) — stacked on, not waited for.
+- The existing account-backed preference system (effective resolution, source reporting), used at the
+ user scope only.
+- The existing experimental-settings mechanism, already surfaced to the frontend before sign-in.
+- The `opsmill/infrahub-schema-visualizer` repository, for User Story 7 only. That story completes on
+ the upstream repository's timeline, not this one, so it is tracked as its own deliverable and does
+ not gate the other six. It remains in scope — the seven-item scope was proposed narrower, queried,
+ and confirmed at all seven by the requester.
+
+## Out of Scope
+
+- The failing end-to-end checks on PR #10284.
+- Any change to the light theme's appearance.
+- **Content that carries its own colors** — diagrams, syntax highlighting, status and severity
+ palettes, user-supplied content. Making these meaningful in both themes is a separate piece of
+ work. This feature must not degrade them, but does not redesign them.
+- **Cross-tab synchronisation.** A second open tab is not required to react to a theme change made in
+ the first; it picks the change up on its next load.
+- **An organisation-wide theme default.** While the feature is flag-gated there is no administrator
+ setting a house theme for anyone. Deferred to the moment the flag is removed, when a real user for
+ it exists. The backend gains it for free either way — the preference mutation's scope argument is
+ shared — so this defers only the interface for it.
+- **A removal date for the flag**, recorded as knowingly open-ended rather than left unstated.
+- Additional themes beyond light and dark (high contrast, custom palettes, per-branch theming).
+- Theming of printed or exported output.
+- Restyling third-party surfaces beyond binding them to the active theme.
diff --git a/dev/specs/infp-46-dark-theme-completion/tasks.md b/dev/specs/infp-46-dark-theme-completion/tasks.md
new file mode 100644
index 00000000000..999e3b88d97
--- /dev/null
+++ b/dev/specs/infp-46-dark-theme-completion/tasks.md
@@ -0,0 +1,213 @@
+# Tasks: Dark Theme Completion
+
+**Input**: Design documents in `dev/specs/infp-46-dark-theme-completion/`
+
+**Prerequisites**: [plan.md](./plan.md), [spec.md](./spec.md), [research.md](./research.md),
+[data-model.md](./data-model.md), [contracts/](./contracts/)
+
+**Tests**: Included. Constitution principle IV (Test Discipline) and `AGENTS.md` both require tests
+for new functionality. Pure-function tasks are written test-first.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: can run in parallel — different files, no dependency on another incomplete task
+- **[Story]**: the user story the task serves
+
+## ⚠ Before starting
+
+Two governance approvals are **required** before Phase 3 (see [plan.md](./plan.md#open-governance-points)).
+`AGENTS.md` lists both as Ask First:
+
+1. **GraphQL schema modification** — new `Theme` enum, `EffectiveTheme` type, field on two types, new
+ mutation argument. Additive and non-breaking, but public schema.
+2. **Persisted model change** — nullable `theme` on the `Preference` `StandardNode`. Expected additive
+ with no data migration; confirm with an owner of the `StandardNode` persistence path.
+
+Phases 1, 2 and 7–9 need neither approval and can proceed meanwhile.
+
+Verification uses the binaries directly — `pnpm` scripts abort in this environment:
+
+```bash
+cd frontend/app && node_modules/.bin/vitest run && node_modules/.bin/biome ci . && node_modules/.bin/tsc --noEmit
+```
+
+---
+
+## Phase 1: Setup
+
+**Purpose**: make the worktree able to build and give the light theme a reference to be compared against.
+
+- [x] T001 Initialise the visualizer submodule: `git submodule update --init frontend/packages/schema-visualizer`. Required for US7, and it clears the two phantom `betterer` findings an uninitialised submodule produces.
+- [x] T002 [P] Install the editable SDK: `uv pip install -e python_sdk`. Fresh worktrees skip this and `infrahub_sdk` imports fail.
+- [x] T003 Base the branch on `origin/bab-dark-theme-app` and open the pull request **against that branch**, not `develop` — this is a stacked PR on #10284. It supplies the surfaces US5 migrates and keeps this review free of #10284's 151 files. Re-target `develop` once #10284 merges; rebase if it is revised. ⚠ #10284's failing e2e checks are inherited and will show on this PR — say so in the description so they are not read as caused by this work.
+- [ ] T004 ⏸ **Deferred to just before Phase 7** (needs a running stack; only US5/US6 depend on it). Capture light-theme reference screenshots of every page US5/US6 touch (proposed changes, a diff view, checks, path traversal, data viewer). FR-020/SC-005 make "light is unchanged" a hard constraint, and it is unprovable later without a baseline taken now.
+
+**Checkpoint**: builds clean; light-theme baseline exists.
+
+---
+
+## Phase 2: Foundational (blocking)
+
+**Purpose**: the theme value every other story consumes. Nothing here needs the backend, so it can
+start immediately and in parallel with governance approval.
+
+⚠ The context lives in `shared/`, the provider in the entity. `shared/` may not import an entity
+(`dev/knowledge/frontend/entities-structure.md`), and there is **no lint guard** — layer rules are
+review-enforced only.
+
+- [x] T005 Create `frontend/app/src/shared/context/theme-context.ts` exporting the `ResolvedTheme` type (`"light" | "dark"`). It must import nothing from `entities/`. ⚠ **Scope corrected during implementation**: the React context const itself moved to Phase 3 (T021a). `knip` runs in CI and fails on any export without a consumer, so a context with neither a producer nor a reader cannot land green on its own — it must arrive with its provider. The *type* lands here because the pure resolver consumes it immediately.
+- [x] T006 [P] Write failing tests for stage-2 resolution in `frontend/app/src/entities/preferences/domain/rules/theme.test.ts`: table-driven over `(choice, systemPrefersDark)` → `"light" | "dark"`, covering all three choices and both system states.
+- [x] T007 Implement `frontend/app/src/entities/preferences/domain/rules/resolve-theme.ts` to pass T006 (named for the house `resolve-*` convention). ⚠ Pure only — `domain/rules` may not touch browser storage or React.
+
+**Checkpoint**: a resolved theme can be held and read; consumers can be written against it.
+
+---
+
+## Phase 3: User Story 1 — Choose a theme (P1) 🎯 MVP
+
+**Goal**: a user picks light / dark / match-system; it applies immediately, persists to their account,
+and paints correctly on first frame.
+
+**Independent Test**: change the setting, watch it apply without reload; reload under heavy network
+throttling and confirm no flash; sign in from a second browser and see the same choice.
+
+### Backend
+
+- [ ] T008 [US1] Add `Theme` StrEnum (`LIGHT`, `DARK`, `SYSTEM`) to `backend/infrahub/core/preferences/constants.py`, beside `DateFormat`.
+- [ ] T009 [US1] Add `theme: Optional[Theme] = None` to `Preference` in `backend/infrahub/core/preferences/models.py`. ⚠ `Optional[Theme]`, never `Theme | None` — `StandardNode.guess_field_type` requires it, as the file's own comment records.
+- [ ] T010 [US1] Add `Theme` and `EffectiveTheme` to `backend/infrahub/graphql/types/preferences.py`; add `theme` to `EffectivePreferencesType` and `RawPreferencesType` per [contracts/graphql-preferences.md](./contracts/graphql-preferences.md). ⚠ Enum descriptions stay on **one line** — the SDL printer dedents multi-line descriptions inconsistently and makes the generated schema environment-dependent.
+- [ ] T011 [US1] Resolve `theme` through the existing user → global → default chain in `backend/infrahub/graphql/queries/preferences.py`.
+- [ ] T012 [US1] Write failing tests for the mutation's three-state argument in `backend/tests/unit/graphql/test_preferences.py`: omitted leaves unchanged, explicit `null` clears, a value sets. ⚠ This is the single easiest thing to get wrong — collapsing "omitted" and "null" makes an override impossible to clear.
+- [ ] T013 [US1] Add the `theme` argument and payload field to `backend/infrahub/graphql/mutations/preferences.py`, honouring `_UNSET` exactly as `date_format` does. Passes T012.
+- [ ] T014 [P] [US1] Test the resolution chain: nothing set → `DEFAULT`/null; user set → `USER`; clearing the user layer returns to `DEFAULT`. The global layer is exercised too — the mutation's `scope` argument reaches it and the chain must keep working — even though no interface writes it in this version.
+- [ ] T015 [P] [US1] Test that a non-`Theme` value is rejected on construction, including on load from the database.
+- [ ] T016 [US1] Regenerate and commit: `uv run invoke schema.generate-graphqlschema`. CI fails on a stale `schema/schema.graphql`.
+
+### Frontend
+
+- [ ] T017 [US1] Add `theme` to `PreferenceValues` and `EffectivePreferences` in `frontend/app/src/entities/preferences/domain/model/preference.ts`.
+- [ ] T018 [US1] Add `theme` to the effective-preferences query and the **user** upsert mutation under `frontend/app/src/entities/preferences/ui/queries/`. ⚠ Not `update-global-preferences.mutation.ts` — leaving `theme` out of that document is what keeps the organisation scope unreachable from the interface without needing backend changes.
+- [ ] T019 [US1] Regenerate frontend types: `cd frontend/app && pnpm codegen`.
+- [ ] T020 [US1] Write failing tests for `frontend/app/src/entities/preferences/ui/theme-provider.test.tsx`: fills the shared context from the effective preference; falls back to the flag's default when the query fails; reacts to a `prefers-color-scheme` change while mounted.
+- [ ] T021a [US1] Add the `ThemeContext` const and its reader hook to `frontend/app/src/shared/context/theme-context.ts` (moved from T005 — see the note there). Model it on the sibling `shared/context/date-preferences-context.tsx`. Land it in the same commit as T021 so no export exists without a consumer.
+- [ ] T021 [US1] Implement `frontend/app/src/entities/preferences/ui/theme-provider.tsx` — fills `shared/context/theme-context`, applies the class to `document.documentElement`, writes the `localStorage` mirror, subscribes to `prefers-color-scheme`. Mirror `DatePreferencesProvider`'s shape. Passes T020. No `storage` listener — cross-tab sync is out of scope.
+- [ ] T022 [US1] Mount the provider in `frontend/app/src/app/app.tsx` alongside `DatePreferencesProvider`.
+- [ ] T023 [US1] Add the inline pre-paint script to `frontend/app/index.html` ``, **before** the module script. Precedence: mirrored resolved theme → mirrored `system` choice resolved against `prefers-color-scheme` → light. ⚠ The empty-cache fallback is **light**, not `prefers-color-scheme` — consulting the system there would put a dark-OS user into the alpha palette before any preference has been read. `prefers-color-scheme` is read only when the *user* has chosen match-system. ⚠ It blocks rendering and runs before everything: wrap storage access in `try`/`catch` (`localStorage` throws when storage is disabled, e.g. Safari private browsing) and validate the stored string against the known set before using it as a class name.
+- [ ] T024 [US1] Add the theme field to `frontend/app/src/entities/preferences/ui/preference-fields.tsx` as a `Combobox` matching the existing fields, keeping `"Automatic (inherited)"` as the empty label. Dark carries a visible **"alpha"** tag — the handover named that word specifically, so render it rather than a synonym; "match system" says it can resolve to the alpha palette (FR-008).
+- [ ] T025 [P] [US1] Surface the field in `preferences-form.tsx` and `user-preferences-card.tsx`, updating their existing tests. ⚠ **Not** `global-preferences-form.tsx` — theme is user-scoped only in this version. The backend gains the global scope for free (the mutation's `scope` argument is shared), so this defers only the interface.
+- [ ] T026 [US1] End-to-end test for first-paint correctness (FR-006 / SC-002): with a stored dark preference and the preference request delayed, assert the document element carries the dark class before the app has hydrated. Add a cold-cache case — no mirror, emulated **dark** browser preference — asserting the first paint is **light**, since a defaulted user must not reach the alpha palette by inference. ⚠ The pre-paint script sits outside the module graph so Vitest cannot reach it; this is its **only** automated coverage.
+- [ ] T027 [US1] Remove `@custom-variant dark` and its `TODO: DELETE` from `frontend/packages/ui/src/styles/theme.css` (FR-019). ⚠ **Last task in this phase** — it is what all current dark rendering depends on; removing it earlier leaves the tree with no way to reach dark at all.
+
+**Checkpoint**: US1 ships standalone. Dark is reachable, persistent and flash-free.
+
+---
+
+## Phase 4: User Story 2 — Non-production default (P1)
+
+**Goal**: non-production deployments default to dark so the team dogfoods it without per-engineer setup.
+
+**Independent Test**: load as a user with no stored preference on a pre-release build → dark; on a
+release build → light; a personal choice beats both and is never overwritten.
+
+- [x] T028 [US2] Add `dark_theme: bool = False` to `ExperimentalFeaturesSettings` in `backend/infrahub/config.py`, beside `graphql_enums`. A plain bool — no tri-state is needed, since the flag carries no derived value. ⚠ Not `installation_type`, which is community-vs-enterprise and a tempting false lead on the same payload.
+- [x] T029 [US2] Regenerate and commit: `uv run invoke schema.generate-jsonschema`, then `cd frontend/app && pnpm codegen`. No new endpoint or payload field — `experimental_features` is already on the unauthenticated `/api/config`.
+- [x] T030 [US2] Enable it in `development/docker-compose.yml`: `INFRAHUB_EXPERIMENTAL_DARK_THEME: ${INFRAHUB_EXPERIMENTAL_DARK_THEME:-true}`. ⚠ Defaulting to `true` — unlike its two neighbours — is precisely what delivers SC-008. The env var still overrides for an engineer who wants light.
+- [x] T031 [US2] Add the flag to `development/docker-compose.yml`, defaulted `true`. **Reversed during implementation:** the root `docker-compose.yml` gets the flag too, defaulted `false`, matching its two experimental siblings there. The original plan was to withhold it from the root file, but that file's env block is *generated* from `Settings` by `release.gen-config-env`, and CI's `validate-docker-compose-env-vars` fails on any drift. The generator's only per-setting exclusion is the `INFRAHUB_DEV` prefix skip, which would mean moving `dark_theme` into `DevelopmentSettings` — a class `/api/config` does not publish, so the gating rule could only read it by widening an unauthenticated endpoint to expose development-only settings. Not worth it. What the original decision protected is unaffected: production stays light because the default is `false` and the frontend gates on the flag. The only thing given up is that an operator *can* opt in from the root compose file — the same posture as `INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS` and `INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX`.
+- [ ] T032 [US2] Gate the theme field on the flag: with it off, render light and **omit the field entirely**. ⚠ Not a light-only picker — offering match-system would let a dark-OS user reach the alpha palette straight through the gate.
+- [ ] T033 [US2] Default a user with no stored preference to dark when the flag is on, **ignoring the operating system**. Extend T020's tests to assert a defaulted user's theme does not change when the emulated system appearance flips.
+- [ ] T034 [P] [US2] Test that turning the flag off **retains** a stored `DARK` preference — renders light, leaves the stored value intact, and honours it again when the flag returns. ⚠ A config change must never destroy user data (FR-013).
+- [ ] T035 [US2] Pin the theme explicitly in both end-to-end suites (`frontend/app` Playwright and `tests/e2e` pytest) so they stop inheriting the flag's value. ⚠ #10284's e2e checks are already failing and are out of scope — baseline only from a green run after it lands, or its failures will be misread as fallout from this change.
+
+**Checkpoint**: the dogfooding loop is live.
+
+---
+
+## Phase 5: User Story 3 — GraphQL sandbox (P2)
+
+- [x] T036 [US3] Replace `forcedTheme="light"` in `frontend/app/src/pages/graphql/index.tsx` with the resolved theme from the shared context. ⚠ Pass `"light"`/`"dark"` only — never `"system"`, or GraphiQL runs its own `prefers-color-scheme` detection and can disagree with the application.
+- [ ] T037 [US3] Test that the sandbox receives the resolved value and follows a theme change. ⚠ The relied-upon behaviour (reactive `forcedTheme`, and picker-hiding when set) is not documented public API — it was verified against `graphiql@5.2.4`'s bundled source, so a test is what protects the binding across upgrades. *Partially covered*: `use-resolved-theme.test.tsx` now guards the hook the page reads from; the GraphiQL binding itself (that `forcedTheme` reaches the sandbox and reacts) remains untested — GraphiQL is too heavy for the browser-mode suite, so this wants an e2e assertion on the sandbox page.
+
+---
+
+## Phase 6: User Story 4 — Mermaid diagrams (P2)
+
+- [x] T038 [US4] Derive `mermaidConfig.theme` from the resolved theme in `frontend/app/src/shared/components/editor/markdown/markdown-with-mermaid.tsx`, mapping to Mermaid's `"dark"` / `"default"`. ⚠ `rehypePlugins` is currently a module-level constant; making it theme-dependent **must** memoise on the resolved theme alone. A new array identity per render re-runs the rehype pipeline continuously — the diagram flickers and a CPU core pins.
+- [x] T039 [US4] Replace the hardcoded `bg-white` on the pan/zoom container in `frontend/app/src/shared/components/editor/markdown/mermaid-diagram.tsx` with a surface token. This is the bright panel behind an otherwise-correct dark diagram.
+- [x] T040 [P] [US4] Tokenise the `mermaid-error` fallback styling so the parse-error state is legible in both themes (FR-015).
+- [x] T041 [US4] Test that a theme change re-renders the diagram. **Narrowed during implementation:** the companion property — a stable plugin array across renders at a fixed theme — has no meaningful test here. The React Compiler memoises the array whether or not the `useMemo` is written by hand, so an assertion on it passes identically with the memo deleted; a test that cannot fail is worse than none. The property is held by compilation, and the re-render half above is what actually guards the user-visible behaviour.
+
+---
+
+## Phase 7: User Story 5 — Legacy pages onto tokens (P2)
+
+**Goal**: no application component carries per-theme overrides or raw color literals.
+
+⚠ Verify the light theme against the T004 baseline after **every batch**, not once at the end. This
+is the constraint a token swap breaks most easily and the most expensive to bisect late.
+
+- [ ] T042 [P] [US5] Migrate `entities/diff/ui/` — `node-diff/utils.tsx`, `node-diff/node.tsx`, `checks/validator.tsx`, `checks/check.tsx`, `checks/data-conflict.tsx`, `diff-badge.tsx`.
+- [ ] T043 [P] [US5] Migrate `entities/path-traversal/ui/` — `path-results-list.tsx`, `infra-node.tsx`, `path-traversal-page.tsx`.
+- [ ] T044 [P] [US5] Migrate `entities/proposed-changes/ui/diff-summary/diff-summary-tag-group.tsx`, `entities/tasks/ui/task-display.tsx`, `entities/branches/ui/branch-working-notice.tsx`, `entities/schema/ui/styled.tsx`, `entities/user-profile/ui/account-token-create-action.tsx`.
+- [ ] T045 [P] [US5] Migrate `shared/components/` — `modals/modal-confirm.tsx`, `table/style.tsx`, `table/sticky-cell-shadow.tsx`, `ui/infrahub-logo.tsx`, `ui/link-pill.tsx`.
+- [x] T046 [US5] ✅ **No work needed.** Migrate `shared/components/ui/badge.tsx` **separately and last**. ⚠ Verified during implementation: all twelve occurrences are semantic colours, which are out of scope. The file needs no change. Redesigning semantic palettes is **out of scope** (tracked separately); the rule is do not *degrade* them. Where a mechanical swap would flatten two currently-distinct severities into one, keep the distinction and note it for that separate effort.
+- [ ] T047 [US5] Add the automated guard that makes SC-004 a standing property rather than a one-time cleanup. `betterer` is already wired into CI and is the lower-friction option; a lint rule is stricter. Without a guard the debt returns with the next feature branch.
+- [ ] T048 [US5] Verify: `git grep -c "dark:" -- 'frontend/app/src/**/*.tsx'` returns nothing. ⚠ Use plain `git grep` — `rtk` reformats output and an empty piped result is not proof.
+
+---
+
+## Phase 8: User Story 6 — Data viewer (P3)
+
+- [x] T049 [US6] Replace `bg-neutral-800 text-neutral-200` (line 29) and `border-neutral-700` (line 77) in `frontend/app/src/shared/components/data-viewer/data-viewer.tsx` with palette tokens. `neutral` is Tailwind's cold grey; the theme is built on warm `stone` — that difference is the reported tone mismatch.
+- [ ] T050 [US6] Replace the two `bg-white` containers (lines 58, 89) with tokens — a fixed light background in dark mode, which FR-018 forbids.
+- [ ] T051 [P] [US6] Walk every content type the viewer handles and confirm none retains a fixed light background.
+
+---
+
+## Phase 9: User Story 7 — Schema visualizer (P3)
+
+⚠ Completes on the upstream repository's timeline. Tracked separately so it does not gate the other
+six; the work stays in scope.
+
+- [ ] T052 [US7] Open a pull request on `opsmill/infrahub-schema-visualizer` adding dark support: canvas, nodes, edges, labels and controls, with the theme accepted from the embedding application rather than detected independently.
+- [ ] T053 [US7] Get it merged and released upstream.
+- [ ] T054 [US7] Bump the submodule pointer here and pass the resolved theme into the visualizer. ⚠ Never point at an unpushed commit — it breaks every other checkout.
+- [ ] T055 [US7] Confirm no visualizer styling code landed in this repository (FR-016).
+
+---
+
+## Phase 10: Cross-cutting
+
+- [x] T056 Contrast audit (FR-021 / SC-009) across the pages walked for SC-006, not a sample — text and essential interface elements against their surfaces. ⚠ Scope boundary: this is legibility against a background, **not** semantic palettes (diagram, syntax-highlighting, status and severity colors), which are tracked separately. Record anything noticed there for that effort rather than fixing it here.
+- [x] T057 [P] Add a changelog fragment under `changelog/`. This series used `ci/skip-changelog` for pure restyling, but a user-facing theme setting is a genuine feature and warrants an entry.
+- [x] T058 [P] Document the theme preference in the user-facing docs under `docs/`, including that dark is pre-release.
+- [x] T059 Run `/pre-ci` before pushing — it runs the locally-executable CI checks including generated-file and generated-doc validation, which this feature touches in three places.
+
+---
+
+## Dependencies
+
+```text
+Phase 1 (setup)
+ └─▶ Phase 2 (shared context + resolution) ← blocks every consumer
+ ├─▶ Phase 3 (US1) ← governance approval required
+ │ └─▶ Phase 4 (US2)
+ ├─▶ Phase 5 (US3) ┐
+ ├─▶ Phase 6 (US4) ├─ independent of each other
+ └─▶ Phase 8 (US6) ┘
+
+Phase 7 (US5) ── needs PR #10284 merged (or T003's rebase)
+Phase 9 (US7) ── independent; gated on the upstream repository
+Phase 10 ── after the phases it audits
+```
+
+**Critical path**: T001 → T005/T007 → T008–T027 (US1) → T028–T035 (US2).
+
+**Parallelisable once Phase 2 lands**: US3, US4, US6 and the US5 migration batches are all
+independent of one another. US7 can start at any time.
+
+## Task count
+
+59 tasks: 4 setup, 3 foundational, 20 US1, 8 US2, 2 US3, 4 US4, 7 US5, 3 US6, 4 US7, 4 cross-cutting.
+
+The US2 count is unchanged but its content is not: the version-derived resolver, its tests and the
+config field were replaced by the flag, its compose wiring, and the flag-off behaviour.
diff --git a/development/docker-compose.yml b/development/docker-compose.yml
index 6fadf11bef6..f33b4bfe3fc 100644
--- a/development/docker-compose.yml
+++ b/development/docker-compose.yml
@@ -71,6 +71,9 @@ x-infrahub-config: &infrahub_config
INFRAHUB_DIFF_UPDATE_AFTER_MERGE: ${INFRAHUB_DIFF_UPDATE_AFTER_MERGE:-true}
INFRAHUB_SELECTIVE_EXECUTION_AFTER_MERGE: ${INFRAHUB_SELECTIVE_EXECUTION_AFTER_MERGE:-true}
INFRAHUB_DOCS_INDEX_PATH: ${INFRAHUB_DOCS_INDEX_PATH:-/opt/infrahub/docs/build/search-index.json}
+ # Defaults to true here, and only here: the team dogfoods dark on development stacks while it is
+ # alpha. The root compose file deliberately has no passthrough, so a deployment cannot pick it up.
+ INFRAHUB_EXPERIMENTAL_DARK_THEME: ${INFRAHUB_EXPERIMENTAL_DARK_THEME:-true}
INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS: ${INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS:-false}
INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX: ${INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX:-false}
INFRAHUB_GIT_APPEND_GIT_SUFFIX:
diff --git a/docker-compose.yml b/docker-compose.yml
index 62d92738aff..a6b3cf0ef6d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -93,6 +93,7 @@ x-infrahub-config: &infrahub_config
INFRAHUB_DELETE_BRANCH_AFTER_MERGE: ${INFRAHUB_DELETE_BRANCH_AFTER_MERGE:-false}
INFRAHUB_DIFF_UPDATE_AFTER_MERGE: ${INFRAHUB_DIFF_UPDATE_AFTER_MERGE:-true}
INFRAHUB_DOCS_INDEX_PATH: ${INFRAHUB_DOCS_INDEX_PATH:-/opt/infrahub/docs/build/search-index.json}
+ INFRAHUB_EXPERIMENTAL_DARK_THEME: ${INFRAHUB_EXPERIMENTAL_DARK_THEME:-false}
INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS: ${INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS:-false}
INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX: ${INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX:-false}
INFRAHUB_GIT_APPEND_GIT_SUFFIX:
diff --git a/docs/docs/faq/faq.mdx b/docs/docs/faq/faq.mdx
index 826c8257a3c..74291b12f0d 100644
--- a/docs/docs/faq/faq.mdx
+++ b/docs/docs/faq/faq.mdx
@@ -141,6 +141,12 @@ Infrahub exposes a metrics endpoint by default, and the Infrahub Exporter provid
In production, the most commonly monitored signals are API request latency, task manager queue depth, database connection pool usage, and disk utilisation.
+### Does the web interface have a dark theme?
+
+Yes, as a pre-release feature. The dark theme is gated behind the `INFRAHUB_EXPERIMENTAL_DARK_THEME` setting (see the [configuration reference](../reference/configuration)): with the setting enabled, the interface starts in dark and the account menu at the bottom of the sidebar offers a switch between light and dark, marked *alpha*. The choice is remembered per browser. With the setting disabled — the default — the interface is light-only.
+
+While the theme is in alpha, some surfaces may still render with incorrect colours. Development deployments started from the repository's compose file enable it by default so those get found; production deployments stay light unless an operator opts in.
+
### How does Infrahub handle authentication and access control?
Infrahub supports OAuth2 / OIDC single sign-on (see the [SSO guide](../deploy-manage/user-management/sso/overview) and token-based API authentication (see [managing API tokens](../deploy-manage/user-management/managing-api-tokens)).
diff --git a/docs/docs/reference/configuration.mdx b/docs/docs/reference/configuration.mdx
index bfa61eda1a1..40326a76581 100644
--- a/docs/docs/reference/configuration.mdx
+++ b/docs/docs/reference/configuration.mdx
@@ -383,6 +383,7 @@ LDAP authentication configuration.
| Name | Description | Type | Default |
|------|-------------|------|---------|
| `INFRAHUB_EXPERIMENTAL_GRAPHQL_ENUMS` | None | boolean | False |
+| `INFRAHUB_EXPERIMENTAL_DARK_THEME` | Offer the dark theme in the web interface. Alpha: some surfaces still render incorrectly. | boolean | False |
| `INFRAHUB_EXPERIMENTAL_VALUE_DB_INDEX` | None | boolean | False |
## Log forwarding
diff --git a/frontend/app/.betterer.results b/frontend/app/.betterer.results
index 1487f990cc6..ba1a367b5de 100644
--- a/frontend/app/.betterer.results
+++ b/frontend/app/.betterer.results
@@ -17,21 +17,21 @@ exports[`fix ts error`] = {
"src/entities/branches/ui/branch-create-form.tsx:4075623856": [
[34, 33, 13, "tsc: Argument of type \'BranchListItem\' is not assignable to parameter of type \'Branch\'.\\n Type \'BranchListItem\' is missing 4 properties from type \'Branch\'", "2717214769"]
],
- "src/entities/diff/ui/artifact-diff/artifact-content-diff.tsx:863579160": [
+ "src/entities/diff/ui/artifact-diff/artifact-content-diff.tsx:3984866554": [
[7, 39, 9, "tsc: Could not find a declaration file for module \'unidiff\'. \'../../../../../../node_modules/.pnpm/unidiff@1.0.4/node_modules/unidiff/index.js\' implicitly has an \'any\' type.\\n Try \`npm i --save-dev @types/unidiff\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'unidiff\';\`", "1870574010"],
[29, 13, 23, "tsc: No overload matches this call.\\n Overload 1 of 3, \'(message: Buffer | string, options?: Sha1AsStringOptions | undefined): string\', gave the following error.\\n Argument of type \'number\' is not assignable to parameter of type \'Buffer | string\'.\\n Overload 2 of 3, \'(message: Buffer | string, options?: Sha1AsBytesOptions | undefined): Uint8Array\', gave the following error.\\n Argument of type \'number\' is not assignable to parameter of type \'string | Buffer\'.\\n Overload 3 of 3, \'(message: string | Buffer, options?: Sha1Options | Uint8Array | undefined): string\', gave the following error.\\n Argument of type \'number\' is not assignable to parameter of type \'string | Buffer\'.", "1877915893"],
[329, 17, 11, "tsc: \'fileContent\' is possibly \'undefined\'.", "1561581386"],
[331, 20, 11, "tsc: \'fileContent\' is possibly \'undefined\'.", "1561581386"],
[333, 30, 11, "tsc: \'fileContent\' is possibly \'undefined\'.", "1561581386"]
],
- "src/entities/diff/ui/checks/check.tsx:4217899158": [
- [102, 4, 9, "tsc: Property \'conflicts\' does not exist on type \'null; artifact_id: { value: string | null; commit: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conflicts: { value: unknown; } | null; conflicts: { value: unknown; } | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; display_label: string | null; display_label: string | null; display_label: string | null; display_label: string | null; display_label: string | null; display_label: string | null; files: { value: unknown; } | null; keep_branch: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; storage_id: { value: string | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | { __typename: \\"CoreArtifactCheck\\"; id: string | { __typename: \\"CoreDataCheck\\"; id: string | { __typename: \\"CoreFileCheck\\"; id: string | { __typename: \\"CoreGeneratorCheck\\"; id: string | { __typename: \\"CoreSchemaCheck\\"; id: string | { __typename: \\"CoreStandardCheck\\"; id: string\'.", "1029368512"],
- [165, 61, 6, "tsc: Property \'length\' does not exist on type \'{}\'.", "1433765721"],
- [166, 32, 9, "tsc: Type \'{ value: unknown; }\' is not assignable to type \'Maybe\'.\\n Type \'{ value: unknown; }\' is missing 3 properties from type \'AttributeInterface\'", "1029368512"],
- [169, 63, 6, "tsc: Property \'length\' does not exist on type \'{}\'.", "1433765721"],
- [170, 34, 9, "tsc: Type \'{ value: unknown; }\' is not assignable to type \'Maybe\'.\\n Type \'{ value: unknown; }\' is missing 3 properties from type \'AttributeInterface\'", "1029368512"]
+ "src/entities/diff/ui/checks/check.tsx:950742999": [
+ [108, 4, 9, "tsc: Property \'conflicts\' does not exist on type \'null; artifact_id: { value: string | null; commit: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conclusion: { value: string | null; conflicts: { value: unknown; } | null; conflicts: { value: unknown; } | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; created_at: { value: string | null; display_label: string | null; display_label: string | null; display_label: string | null; display_label: string | null; display_label: string | null; display_label: string | null; files: { value: unknown; } | null; keep_branch: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; kind: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; message: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; name: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; origin: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; severity: { value: string | null; storage_id: { value: string | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | null; } | { __typename: \\"CoreArtifactCheck\\"; id: string | { __typename: \\"CoreDataCheck\\"; id: string | { __typename: \\"CoreFileCheck\\"; id: string | { __typename: \\"CoreGeneratorCheck\\"; id: string | { __typename: \\"CoreSchemaCheck\\"; id: string | { __typename: \\"CoreStandardCheck\\"; id: string\'.", "1029368512"],
+ [171, 61, 6, "tsc: Property \'length\' does not exist on type \'{}\'.", "1433765721"],
+ [172, 32, 9, "tsc: Type \'{ value: unknown; }\' is not assignable to type \'Maybe\'.\\n Type \'{ value: unknown; }\' is missing 3 properties from type \'AttributeInterface\'", "1029368512"],
+ [175, 63, 6, "tsc: Property \'length\' does not exist on type \'{}\'.", "1433765721"],
+ [176, 34, 9, "tsc: Type \'{ value: unknown; }\' is not assignable to type \'Maybe\'.\\n Type \'{ value: unknown; }\' is missing 3 properties from type \'AttributeInterface\'", "1029368512"]
],
- "src/entities/diff/ui/file-diff/file-content-diff.tsx:4266470414": [
+ "src/entities/diff/ui/file-diff/file-content-diff.tsx:1428611458": [
[23, 39, 9, "tsc: Could not find a declaration file for module \'unidiff\'. \'../../../../../../node_modules/.pnpm/unidiff@1.0.4/node_modules/unidiff/index.js\' implicitly has an \'any\' type.\\n Try \`npm i --save-dev @types/unidiff\` if it exists or add a new declaration (.d.ts) file containing \`declare module \'unidiff\';\`", "1870574010"],
[42, 13, 23, "tsc: No overload matches this call.\\n Overload 1 of 3, \'(message: Buffer | string, options?: Sha1AsStringOptions | undefined): string\', gave the following error.\\n Argument of type \'number\' is not assignable to parameter of type \'Buffer | string\'.\\n Overload 2 of 3, \'(message: Buffer | string, options?: Sha1AsBytesOptions | undefined): Uint8Array\', gave the following error.\\n Argument of type \'number\' is not assignable to parameter of type \'string | Buffer\'.\\n Overload 3 of 3, \'(message: string | Buffer, options?: Sha1Options | Uint8Array | undefined): string\', gave the following error.\\n Argument of type \'number\' is not assignable to parameter of type \'string | Buffer\'.", "1877915893"],
[352, 19, 11, "tsc: \'fileContent\' is possibly \'undefined\'.", "1561581386"],
@@ -47,32 +47,29 @@ exports[`fix ts error`] = {
[49, 22, 17, "tsc: Property \'diff_branch_label\' does not exist on type \'DiffConflict\'. Did you mean \'diff_branch_value\'?", "1869793242"],
[83, 29, 12, "tsc: Cannot find name \'DataConflict\'.", "3642611779"]
],
- "src/entities/diff/ui/node-diff/node.tsx:1988485766": [
+ "src/entities/diff/ui/node-diff/node.tsx:3075633910": [
[94, 21, 13, "tsc: Binding element \'property_type\' implicitly has an \'any\' type.", "324947141"],
[102, 20, 13, "tsc: Type \'Element | null | string\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "1656119487"],
[106, 20, 8, "tsc: Type \'Element | null | string\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "288015442"],
[142, 22, 13, "tsc: Type \'Element | null | string\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "1656119487"],
[146, 22, 8, "tsc: Type \'Element | null | string\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "288015442"]
],
- "src/entities/events/ui/filters/global-filter.tsx:663673153": [
+ "src/entities/events/ui/filters/global-filter.tsx:2521722326": [
[53, 45, 5, "tsc: Type \'ReactNode\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "173467459"]
],
- "src/entities/events/ui/filters/global-kind-filter.tsx:2478865141": [
+ "src/entities/events/ui/filters/global-kind-filter.tsx:749058434": [
[38, 45, 5, "tsc: Type \'ReactNode\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "173467459"]
],
"src/entities/ipam/ip-addresses/ui/ip-address-table.tsx:2853785502": [
[48, 8, 7, "tsc: Type \'ColumnDef[]\' is not assignable to type \'ColumnDef[]\'.\\n Type \'ColumnDef\' is not assignable to type \'ColumnDef\'.\\n Type \'ColumnDefBase & StringHeaderIdentifier\' is not assignable to type \'ColumnDef\'.\\n Type \'ColumnDefBase & StringHeaderIdentifier\' is not assignable to type \'AccessorFnColumnDefBase | IpAddressAvailableNode, unknown> & IdIdentifier & StringHeaderIdentifier\' but required in type \'AccessorFnColumnDefBase\'.", "3718923584"]
],
- "src/entities/navigation/ui/search-anywhere/search-anywhere-dialog.tsx:4258717933": [
- [12, 7, 5, "tsc: Type \'\\"dialog\\" | undefined; | undefined; id?: string | { children: Element; role?: \\"alertdialog\\"\'aria-label\'?: string | undefined;\'aria-labelledby\'?: string | undefined;\'aria-describedby\'?: string | undefined;\'aria-details\'\\"no\\" | ?: string | null | undefined; className: string; } | undefined; dir?: string | undefined; hidden?: boolean | undefined; inert?: boolean | undefined; lang?: string | undefined; onAnimationEnd?: AnimationEventHandler | undefined; onAnimationEndCapture?: AnimationEventHandler | undefined; onAnimationIteration?: AnimationEventHandler | undefined; onAnimationIterationCapture?: AnimationEventHandler | undefined; onAnimationStart?: AnimationEventHandler | undefined; onAnimationStartCapture?: AnimationEventHandler | undefined; onAuxClick?: MouseEventHandler | undefined; onAuxClickCapture?: MouseEventHandler | undefined; onClick?: MouseEventHandler | undefined; onClickCapture?: MouseEventHandler | undefined; onContextMenu?: MouseEventHandler | undefined; onContextMenuCapture?: MouseEventHandler | undefined; onDoubleClick?: MouseEventHandler | undefined; onDoubleClickCapture?: MouseEventHandler | undefined; onGotPointerCapture?: PointerEventHandler | undefined; onGotPointerCaptureCapture?: PointerEventHandler | undefined; onLostPointerCapture?: PointerEventHandler | undefined; onLostPointerCaptureCapture?: PointerEventHandler | undefined; onMouseDown?: MouseEventHandler | undefined; onMouseDownCapture?: MouseEventHandler | undefined; onMouseEnter?: MouseEventHandler | undefined; onMouseLeave?: MouseEventHandler | undefined; onMouseMove?: MouseEventHandler | undefined; onMouseMoveCapture?: MouseEventHandler | undefined; onMouseOut?: MouseEventHandler | undefined; onMouseOutCapture?: MouseEventHandler | undefined; onMouseOver?: MouseEventHandler | undefined; onMouseOverCapture?: MouseEventHandler | undefined; onMouseUp?: MouseEventHandler | undefined; onMouseUpCapture?: MouseEventHandler | undefined; onPointerCancel?: PointerEventHandler | undefined; onPointerCancelCapture?: PointerEventHandler | undefined; onPointerDown?: PointerEventHandler | undefined; onPointerDownCapture?: PointerEventHandler | undefined; onPointerEnter?: PointerEventHandler | undefined; onPointerLeave?: PointerEventHandler | undefined; onPointerMove?: PointerEventHandler | undefined; onPointerMoveCapture?: PointerEventHandler | undefined; onPointerOut?: PointerEventHandler | undefined; onPointerOutCapture?: PointerEventHandler | undefined; onPointerOver?: PointerEventHandler | undefined; onPointerOverCapture?: PointerEventHandler | undefined; onPointerUp?: PointerEventHandler | undefined; onPointerUpCapture?: PointerEventHandler | undefined; onScroll?: UIEventHandler | undefined; onScrollCapture?: UIEventHandler | undefined; onTouchCancel?: TouchEventHandler | undefined; onTouchCancelCapture?: TouchEventHandler | undefined; onTouchEnd?: TouchEventHandler | undefined; onTouchEndCapture?: TouchEventHandler | undefined; onTouchMove?: TouchEventHandler | undefined; onTouchMoveCapture?: TouchEventHandler | undefined; onTouchStart?: TouchEventHandler | undefined; onTouchStartCapture?: TouchEventHandler | undefined; onTransitionCancel?: TransitionEventHandler | undefined; onTransitionCancelCapture?: TransitionEventHandler | undefined; onTransitionEnd?: TransitionEventHandler | undefined; onTransitionEndCapture?: TransitionEventHandler | undefined; onTransitionRun?: TransitionEventHandler | undefined; onTransitionRunCapture?: TransitionEventHandler | undefined; onTransitionStart?: TransitionEventHandler | undefined; onTransitionStartCapture?: TransitionEventHandler | undefined; onWheel?: WheelEventHandler | undefined; onWheelCapture?: WheelEventHandler | undefined; render?: DOMRenderFunction<\\"section\\", undefined> | undefined; slot?: string | undefined; style?: CSSProperties | undefined; translate?: \\"yes\\"\' is not assignable to type \'ModalOverlayProps\'.\\n Types of property \'render\' are incompatible.\\n Type \'DOMRenderFunction<\\"section\\", undefined> | undefined\' is not assignable to type \'DOMRenderFunction<\\"div\\", ModalRenderProps> | undefined\'.\\n Type \'DOMRenderFunction<\\"section\\", undefined>\' is not assignable to type \'DOMRenderFunction<\\"div\\", ModalRenderProps>\'.\\n Type \'\\"div\\"\' is not assignable to type \'\\"section\\"\'.", "210010926"]
+ "src/entities/navigation/ui/search-anywhere/search-nodes.tsx:1387899725": [
+ [125, 62, 4, "tsc: Property \'node\' does not exist on type \'NodeAttributeWithMetadata | NodeRelationshipManyWithMetadata | NodeRelationshipOneWithMetadata | string | string[]\'.\\n Property \'node\' does not exist on type \'string\'.", "2087865285"],
+ [134, 16, 5, "tsc: Type \'null | string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "183222373"],
+ [135, 29, 4, "tsc: Property \'kind\' does not exist on type \'\\"Attribute\\" | \\"Bandwidth\\" | \\"Boolean\\" | \\"Checkbox\\" | \\"Color\\" | \\"Component\\" | \\"DateTime\\" | \\"Dropdown\\" | \\"Email\\" | \\"File\\" | \\"Group\\" | \\"HashedPassword\\" | \\"Hierarchy\\" | \\"ID\\" | \\"IPAddress\\" | \\"IPHost\\" | \\"IPNetwork\\" | \\"JSON\\" | \\"MacAddress\\" | \\"Parent\\" | \\"Password\\" | \\"Profile\\" | \\"Template\\"; label?: string | \\"TextArea\\"; enum?: unknown[] | \\"URL\\"; enum?: unknown[] | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; on_delete?: \\"no-action\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; read_only: boolean; deprecation?: string | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"cascade\\" | \\"extra\\"; isRelationship: boolean; paginated: boolean; } | \\"extra\\"; parameters?: Record | \\"extra\\"; parameters?: { end_range: number; start_range: number; number_pool_id?: string | \\"extra\\"; parameters?: { min_value?: number | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"inbound\\"; hierarchical?: string | \\"many\\"; min_count: number; max_count: number; common_parent?: string | \\"outbound\\" | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | undefined; allow_override: \\"none\\" | undefined; cardinality: \\"one\\" | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; common_relatives?: string[] | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; excluded_values?: string | undefined; identifier?: string | undefined; inherited: boolean; direction: \\"bidirectional\\" | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_value?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; name: string; kind: \\"Any\\" | undefined; name: string; kind: \\"List\\"; enum?: unknown[] | undefined; name: string; kind: \\"Number\\"; enum?: unknown[] | undefined; name: string; kind: \\"NumberPool\\"; enum?: unknown[] | undefined; name: string; kind: \\"Text\\" | undefined; name: string; peer: string; kind: \\"Generic\\" | undefined; optional: boolean; branch?: \\"local\\" | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; state: \\"present\\" | undefined; } | undefined; } | undefined; } | undefined; } | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { label: string; name: string; }\'.\\n Property \'kind\' does not exist on type \'{ label: string; name: string; }\'.", "2088042925"],
+ [136, 16, 5, "tsc: Type \'NodeAttributeWithMetadata | NodeCorePropertyValue | NodeRelationshipWithMetadata\' is not assignable to type \'boolean | null; label: string; color: string; } | null; } | number | { edges: { node: NodeCore; }[]; } | { node: NodeCore; } | { value: string | { value: string\'.\\n Type \'undefined\' is not assignable to type \'boolean | null; label: string; color: string; } | null; } | number | { edges: { node: NodeCore; }[]; } | { node: NodeCore; } | { value: string | { value: string\'.", "189936718"]
],
- "src/entities/navigation/ui/search-anywhere/search-nodes.tsx:3997834688": [
- [127, 62, 4, "tsc: Property \'node\' does not exist on type \'NodeAttributeWithMetadata | NodeRelationshipManyWithMetadata | NodeRelationshipOneWithMetadata | string | string[]\'.\\n Property \'node\' does not exist on type \'string\'.", "2087865285"],
- [136, 16, 5, "tsc: Type \'null | string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "183222373"],
- [137, 29, 4, "tsc: Property \'kind\' does not exist on type \'\\"Attribute\\" | \\"Bandwidth\\" | \\"Boolean\\" | \\"Checkbox\\" | \\"Color\\" | \\"Component\\" | \\"DateTime\\" | \\"Dropdown\\" | \\"Email\\" | \\"File\\" | \\"Group\\" | \\"HashedPassword\\" | \\"Hierarchy\\" | \\"ID\\" | \\"IPAddress\\" | \\"IPHost\\" | \\"IPNetwork\\" | \\"JSON\\" | \\"MacAddress\\" | \\"Parent\\" | \\"Password\\" | \\"Profile\\" | \\"Template\\"; label?: string | \\"TextArea\\"; enum?: unknown[] | \\"URL\\"; enum?: unknown[] | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; on_delete?: \\"no-action\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; read_only: boolean; deprecation?: string | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"cascade\\" | \\"extra\\"; isRelationship: boolean; paginated: boolean; } | \\"extra\\"; parameters?: Record | \\"extra\\"; parameters?: { end_range: number; start_range: number; number_pool_id?: string | \\"extra\\"; parameters?: { min_value?: number | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"inbound\\"; hierarchical?: string | \\"many\\"; min_count: number; max_count: number; common_parent?: string | \\"outbound\\" | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | undefined; allow_override: \\"none\\" | undefined; cardinality: \\"one\\" | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; common_relatives?: string[] | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; excluded_values?: string | undefined; identifier?: string | undefined; inherited: boolean; direction: \\"bidirectional\\" | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; isAttribute: boolean; } | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_value?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; name: string; kind: \\"Any\\" | undefined; name: string; kind: \\"List\\"; enum?: unknown[] | undefined; name: string; kind: \\"Number\\"; enum?: unknown[] | undefined; name: string; kind: \\"NumberPool\\"; enum?: unknown[] | undefined; name: string; kind: \\"Text\\" | undefined; name: string; peer: string; kind: \\"Generic\\" | undefined; optional: boolean; branch?: \\"local\\" | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; state: \\"present\\" | undefined; } | undefined; } | undefined; } | undefined; } | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { label: string; name: string; }\'.\\n Property \'kind\' does not exist on type \'{ label: string; name: string; }\'.", "2088042925"],
- [138, 16, 5, "tsc: Type \'NodeAttributeWithMetadata | NodeCorePropertyValue | NodeRelationshipWithMetadata\' is not assignable to type \'boolean | null; label: string; color: string; } | null; } | number | { edges: { node: NodeCore; }[]; } | { node: NodeCore; } | { value: string | { value: string\'.\\n Type \'undefined\' is not assignable to type \'boolean | null; label: string; color: string; } | null; } | number | { edges: { node: NodeCore; }[]; } | { node: NodeCore; } | { value: string | { value: string\'.", "189936718"]
- ],
- "src/entities/nodes/convert/ui/convert-form.tsx:2330310837": [
+ "src/entities/nodes/convert/ui/convert-form.tsx:3998917862": [
[42, 6, 8, "tsc: Type \'{}\' is not assignable to type \'Record\'.\\n Index signature for type \'string\' is missing in type \'{}\'.", "1301887866"]
],
"src/entities/nodes/object/ui/filters/dynamic-filter-input.tsx:2989982206": [
@@ -101,7 +98,7 @@ exports[`fix ts error`] = {
"src/entities/nodes/object/ui/object-relationship-list.tsx:629247395": [
[38, 54, 5, "tsc: Property \'items\' does not exist on type \'NodeObject[]\'.", "179721187"]
],
- "src/entities/nodes/object/ui/object-template/object-template-form.tsx:3479770126": [
+ "src/entities/nodes/object/ui/object-template/object-template-form.tsx:2992493322": [
[86, 44, 5, "tsc: Property \'edges\' does not exist on type \'NodeAttribute | number | string | string[] | string[]\'.\\n Property \'edges\' does not exist on type \'string\'.", "165200885"]
],
"src/entities/nodes/relationships/ui/queries/get-default-parent.query.ts:1805247483": [
@@ -117,7 +114,7 @@ exports[`fix ts error`] = {
"src/entities/proposed-changes/ui/action-button/pc-review-button.tsx:69324143": [
[39, 22, 5, "tsc: Type \'NoInfer | undefined\' is not assignable to type \'PcActionsContextType\'.\\n Type \'undefined\' is not assignable to type \'PcActionsContextType\'.", "189936718"]
],
- "src/entities/proposed-changes/ui/proposed-change-details.tsx:3917007424": [
+ "src/entities/proposed-changes/ui/proposed-change-details.tsx:323668549": [
[70, 59, 5, "tsc: Argument of type \'null | string | undefined\' is not assignable to parameter of type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "195031250"],
[202, 18, 4, "tsc: Type \'unknown\' is not assignable to type \'Date | null | number | string | undefined\'.", "2087377937"]
],
@@ -129,7 +126,7 @@ exports[`fix ts error`] = {
"src/entities/repository/ui/repository-objects-manager.tsx:3064139168": [
[40, 6, 18, "tsc: Type \'\\"Attribute\\" | \\"Attribute\\" | \\"Attribute\\" | \\"Attribute\\" | \\"Bandwidth\\" | \\"Bandwidth\\" | \\"Bandwidth\\" | \\"Bandwidth\\" | \\"Boolean\\" | \\"Boolean\\" | \\"Boolean\\" | \\"Boolean\\" | \\"Checkbox\\" | \\"Checkbox\\" | \\"Checkbox\\" | \\"Checkbox\\" | \\"Color\\" | \\"Color\\" | \\"Color\\" | \\"Color\\" | \\"Component\\" | \\"Component\\" | \\"Component\\" | \\"Component\\" | \\"DateTime\\" | \\"DateTime\\" | \\"DateTime\\" | \\"DateTime\\" | \\"Dropdown\\" | \\"Dropdown\\" | \\"Dropdown\\" | \\"Dropdown\\" | \\"Email\\" | \\"Email\\" | \\"Email\\" | \\"Email\\" | \\"File\\" | \\"File\\" | \\"File\\" | \\"File\\" | \\"Group\\" | \\"Group\\" | \\"Group\\" | \\"Group\\" | \\"HashedPassword\\" | \\"HashedPassword\\" | \\"HashedPassword\\" | \\"HashedPassword\\" | \\"Hierarchy\\" | \\"Hierarchy\\" | \\"Hierarchy\\" | \\"Hierarchy\\" | \\"ID\\" | \\"ID\\" | \\"ID\\" | \\"ID\\" | \\"IPAddress\\" | \\"IPAddress\\" | \\"IPAddress\\" | \\"IPAddress\\" | \\"IPHost\\" | \\"IPHost\\" | \\"IPHost\\" | \\"IPHost\\" | \\"IPNetwork\\" | \\"IPNetwork\\" | \\"IPNetwork\\" | \\"IPNetwork\\" | \\"JSON\\" | \\"JSON\\" | \\"JSON\\" | \\"JSON\\" | \\"MacAddress\\" | \\"MacAddress\\" | \\"MacAddress\\" | \\"MacAddress\\" | \\"Parent\\" | \\"Parent\\" | \\"Parent\\" | \\"Parent\\" | \\"Password\\" | \\"Password\\" | \\"Password\\" | \\"Password\\" | \\"Profile\\" | \\"Profile\\" | \\"Profile\\" | \\"Profile\\" | \\"Template\\"; label?: string | \\"Template\\"; label?: string | \\"Template\\"; label?: string | \\"Template\\"; label?: string | \\"TextArea\\"; enum?: unknown[] | \\"TextArea\\"; enum?: unknown[] | \\"TextArea\\"; enum?: unknown[] | \\"TextArea\\"; enum?: unknown[] | \\"URL\\"; enum?: unknown[] | \\"URL\\"; enum?: unknown[] | \\"URL\\"; enum?: unknown[] | \\"URL\\"; enum?: unknown[] | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; allow_override: \\"none\\" | \\"absent\\"; attributes?: ({ id?: string | \\"absent\\"; attributes?: ({ id?: string | \\"absent\\"; attributes?: ({ id?: string | \\"absent\\"; attributes?: ({ id?: string | \\"absent\\"; on_delete?: \\"no-action\\" | \\"absent\\"; on_delete?: \\"no-action\\" | \\"absent\\"; on_delete?: \\"no-action\\" | \\"absent\\"; on_delete?: \\"no-action\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\" | \\"agnostic\\"; default_filter?: string | \\"agnostic\\"; default_filter?: string | \\"agnostic\\"; default_filter?: string | \\"agnostic\\"; default_filter?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; deprecation?: string | \\"any\\"; read_only: boolean; deprecation?: string | \\"any\\"; read_only: boolean; deprecation?: string | \\"any\\"; read_only: boolean; deprecation?: string | \\"any\\"; read_only: boolean; deprecation?: string | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"aware\\" | \\"cascade\\" | \\"cascade\\" | \\"cascade\\" | \\"cascade\\" | \\"extra\\"; parameters?: Record | \\"extra\\"; parameters?: Record | \\"extra\\"; parameters?: Record | \\"extra\\"; parameters?: Record | \\"extra\\"; parameters?: { end_range: number; start_range: number; number_pool_id?: string | \\"extra\\"; parameters?: { end_range: number; start_range: number; number_pool_id?: string | \\"extra\\"; parameters?: { end_range: number; start_range: number; number_pool_id?: string | \\"extra\\"; parameters?: { end_range: number; start_range: number; number_pool_id?: string | \\"extra\\"; parameters?: { min_value?: number | \\"extra\\"; parameters?: { min_value?: number | \\"extra\\"; parameters?: { min_value?: number | \\"extra\\"; parameters?: { min_value?: number | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; parameters?: { regex?: string | \\"extra\\"; }[] | \\"extra\\"; }[] | \\"extra\\"; }[] | \\"extra\\"; }[] | \\"inbound\\"; hierarchical?: string | \\"inbound\\"; hierarchical?: string | \\"inbound\\"; hierarchical?: string | \\"inbound\\"; hierarchical?: string | \\"many\\"; min_count: number; max_count: number; common_parent?: string | \\"many\\"; min_count: number; max_count: number; common_parent?: string | \\"many\\"; min_count: number; max_count: number; common_parent?: string | \\"many\\"; min_count: number; max_count: number; common_parent?: string | \\"outbound\\" | \\"outbound\\" | \\"outbound\\" | \\"outbound\\" | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | null | undefined; allow_override: \\"none\\" | undefined; allow_override: \\"none\\" | undefined; allow_override: \\"none\\" | undefined; allow_override: \\"none\\" | undefined; branch: \\"local\\" | undefined; branch: \\"local\\" | undefined; branch: \\"local\\" | undefined; branch: \\"local\\" | undefined; cardinality: \\"one\\" | undefined; cardinality: \\"one\\" | undefined; cardinality: \\"one\\" | undefined; cardinality: \\"one\\" | undefined; children?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; choices?: { name: string; description?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; color?: string | undefined; common_relatives?: string[] | undefined; common_relatives?: string[] | undefined; common_relatives?: string[] | undefined; common_relatives?: string[] | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; computed_attribute?: { kind: \\"Jinja2\\"; jinja2_template: string; } | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; description?: string | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display: \\"default\\" | undefined; display_label?: string | undefined; display_label?: string | undefined; display_label?: string | undefined; display_label?: string | undefined; display_labels?: string[] | undefined; display_labels?: string[] | undefined; display_labels?: string[] | undefined; display_labels?: string[] | undefined; documentation?: string | undefined; documentation?: string | undefined; documentation?: string | undefined; documentation?: string | undefined; excluded_values?: string | undefined; excluded_values?: string | undefined; excluded_values?: string | undefined; excluded_values?: string | undefined; generate_profile: boolean; generate_template: boolean; hierarchy?: string | undefined; hash?: string | undefined; hash?: string | undefined; hash?: string | undefined; hash?: string | undefined; hierarchical: boolean; generate_profile: boolean; used_by?: string[] | undefined; human_friendly_id?: string[] | undefined; human_friendly_id?: string[] | undefined; human_friendly_id?: string[] | undefined; human_friendly_id?: string[] | undefined; icon?: string | undefined; icon?: string | undefined; icon?: string | undefined; icon?: string | undefined; identifier?: string | undefined; identifier?: string | undefined; identifier?: string | undefined; identifier?: string | undefined; include_in_menu?: boolean | undefined; include_in_menu?: boolean | undefined; include_in_menu?: boolean | undefined; include_in_menu?: boolean | undefined; inherit_from?: string[] | undefined; inherit_from?: string[] | undefined; inherit_from?: string[] | undefined; inherited: boolean; direction: \\"bidirectional\\" | undefined; inherited: boolean; direction: \\"bidirectional\\" | undefined; inherited: boolean; direction: \\"bidirectional\\" | undefined; inherited: boolean; direction: \\"bidirectional\\" | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; label?: string | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_length?: number | undefined; max_value?: number | undefined; max_value?: number | undefined; max_value?: number | undefined; max_value?: number | undefined; menu_placement?: string | undefined; menu_placement?: string | undefined; menu_placement?: string | undefined; menu_placement?: string | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; min_length?: number | undefined; name: string; kind: \\"Any\\" | undefined; name: string; kind: \\"Any\\" | undefined; name: string; kind: \\"Any\\" | undefined; name: string; kind: \\"Any\\" | undefined; name: string; kind: \\"List\\"; enum?: unknown[] | undefined; name: string; kind: \\"List\\"; enum?: unknown[] | undefined; name: string; kind: \\"List\\"; enum?: unknown[] | undefined; name: string; kind: \\"List\\"; enum?: unknown[] | undefined; name: string; kind: \\"Number\\"; enum?: unknown[] | undefined; name: string; kind: \\"Number\\"; enum?: unknown[] | undefined; name: string; kind: \\"Number\\"; enum?: unknown[] | undefined; name: string; kind: \\"Number\\"; enum?: unknown[] | undefined; name: string; kind: \\"NumberPool\\"; enum?: unknown[] | undefined; name: string; kind: \\"NumberPool\\"; enum?: unknown[] | undefined; name: string; kind: \\"NumberPool\\"; enum?: unknown[] | undefined; name: string; kind: \\"NumberPool\\"; enum?: unknown[] | undefined; name: string; kind: \\"Text\\" | undefined; name: string; kind: \\"Text\\" | undefined; name: string; kind: \\"Text\\" | undefined; name: string; kind: \\"Text\\" | undefined; name: string; namespace: string; description?: string | undefined; name: string; namespace: string; description?: string | undefined; name: string; namespace: string; description?: string | undefined; name: string; namespace: string; description?: string | undefined; name: string; peer: string; kind: \\"Generic\\" | undefined; name: string; peer: string; kind: \\"Generic\\" | undefined; name: string; peer: string; kind: \\"Generic\\" | undefined; name: string; peer: string; kind: \\"Generic\\" | undefined; optional: boolean; branch?: \\"local\\" | undefined; optional: boolean; branch?: \\"local\\" | undefined; optional: boolean; branch?: \\"local\\" | undefined; optional: boolean; branch?: \\"local\\" | undefined; order_by?: string[] | undefined; order_by?: string[] | undefined; order_by?: string[] | undefined; order_by?: string[] | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; order_weight?: number | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; ordered: boolean; default_value?: unknown; inherited: boolean; state: \\"present\\" | undefined; parent?: string | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; read_only: boolean; unique: boolean; optional: boolean; branch?: \\"local\\" | undefined; readonly kind: string; } | undefined; readonly kind: string; } | undefined; readonly kind: string; } | undefined; readonly kind: string; } | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; regex?: string | undefined; relationships?: { id?: string | undefined; relationships?: { id?: string | undefined; relationships?: { id?: string | undefined; relationships?: { id?: string | undefined; restricted_namespaces?: string[] | undefined; state: \\"present\\" | undefined; state: \\"present\\" | undefined; state: \\"present\\" | undefined; state: \\"present\\" | undefined; state: \\"present\\" | undefined; state: \\"present\\" | undefined; state: \\"present\\" | undefined; state: \\"present\\" | undefined; uniqueness_constraints?: string[][] | undefined; uniqueness_constraints?: string[][] | undefined; uniqueness_constraints?: string[][] | undefined; uniqueness_constraints?: string[][] | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; } | undefined; })[] | undefined; })[] | undefined; })[] | undefined; })[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | undefined; }[] | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { id?: string | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"TransformPython\\"; transform: string; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; } | { kind: \\"User\\"; }\' is not assignable to type \'ModelSchema\'.\\n Type \'null\' is not assignable to type \'ModelSchema\'.", "3283107600"]
],
- "src/entities/resource-manager/ui/number-pool-form.tsx:3337327002": [
+ "src/entities/resource-manager/ui/number-pool-form.tsx:3955593524": [
[50, 39, 13, "tsc: Argument of type \'NodeFieldsWithMetadata | undefined\' is not assignable to parameter of type \'Record | undefined\'.\\n Type \'NodeFieldsWithMetadata\' is not assignable to type \'Record\'.\\n \'string\' index signatures are incompatible.\\n Type \'NodeAttributeWithMetadata | NodeCorePropertyValue | NodeRelationshipWithMetadata\' is not assignable to type \'NodeAttributeWithMetadata\'.\\n Type \'undefined\' is not assignable to type \'NodeAttributeWithMetadata\'.", "3940586649"],
[51, 53, 13, "tsc: Argument of type \'NodeFieldsWithMetadata | undefined\' is not assignable to parameter of type \'Record | undefined\'.\\n Type \'NodeFieldsWithMetadata\' is not assignable to type \'Record\'.\\n \'string\' index signatures are incompatible.\\n Type \'NodeAttributeWithMetadata | NodeCorePropertyValue | NodeRelationshipWithMetadata\' is not assignable to type \'NodeAttributeWithMetadata\'.\\n Type \'undefined\' is not assignable to type \'NodeAttributeWithMetadata\'.", "3940586649"],
[52, 39, 13, "tsc: Argument of type \'NodeFieldsWithMetadata | undefined\' is not assignable to parameter of type \'Record | undefined\'.\\n Type \'NodeFieldsWithMetadata\' is not assignable to type \'Record\'.\\n \'string\' index signatures are incompatible.\\n Type \'NodeAttributeWithMetadata | NodeCorePropertyValue | NodeRelationshipWithMetadata\' is not assignable to type \'NodeAttributeWithMetadata\'.\\n Type \'undefined\' is not assignable to type \'NodeAttributeWithMetadata\'.", "3940586649"],
@@ -188,11 +185,11 @@ exports[`fix ts error`] = {
"src/entities/schema/ui/computed-attribute-display.tsx:4204959992": [
[38, 14, 8, "tsc: Type \'{ title: string; fileName: string; data: string; }\' is not assignable to type \'IntrinsicAttributes & DataViewerProps\'.\\n Property \'fileName\' does not exist on type \'IntrinsicAttributes & DataViewerProps\'.", "3193632964"]
],
- "src/entities/tasks/ui/task-display.tsx:186971154": [
- [43, 16, 4, "tsc: Binding element \'task\' implicitly has an \'any\' type.", "2087951912"],
- [50, 10, 22, "tsc: Element implicitly has an \'any\' type because expression of type \'any\' can\'t be used to index type \'{ SCHEDULED: string; PENDING: string; RUNNING: string; PAUSED: string; CANCELLING: string; COMPLETED: string; CANCELLED: string; FAILED: string; CRASHED: string; }\'.", "1950518905"],
- [65, 39, 4, "tsc: Parameter \'edge\' implicitly has an \'any\' type.", "2087414470"],
- [65, 45, 5, "tsc: Parameter \'index\' implicitly has an \'any\' type.", "178792571"]
+ "src/entities/tasks/ui/task-display.tsx:4156427209": [
+ [46, 16, 4, "tsc: Binding element \'task\' implicitly has an \'any\' type.", "2087951912"],
+ [53, 10, 22, "tsc: Element implicitly has an \'any\' type because expression of type \'any\' can\'t be used to index type \'{ SCHEDULED: string; PENDING: string; RUNNING: string; PAUSED: string; CANCELLING: string; COMPLETED: string; CANCELLED: string; FAILED: string; CRASHED: string; }\'.", "1950518905"],
+ [68, 39, 4, "tsc: Parameter \'edge\' implicitly has an \'any\' type.", "2087414470"],
+ [68, 45, 5, "tsc: Parameter \'index\' implicitly has an \'any\' type.", "178792571"]
],
"src/entities/tasks/ui/task-items.tsx:3434935166": [
[179, 35, 4, "tsc: Type \'null; }; state: { display: any; }; related_nodes: { display: Element; }; progress: { display: number | null; }; updated_at: { display: Element; }; }; }[] | null; }; workflow: { display: string | { link: string; values: { title: { display: string; }; branch: { display: string\' is not assignable to type \'tRow[]\'.\\n Type \'null; }; state: { display: any; }; related_nodes: { display: JSX.Element; }; progress: { display: number | null; }; updated_at: { display: JSX.Element; }; }; } | null; }; workflow: { display: string | { link: string; values: { title: { display: string; }; branch: { display: string\' is not assignable to type \'tRow\'.\\n Types of property \'values\' are incompatible.\\n Type \'null; }; state: { display: any; }; related_nodes: { display: JSX.Element; }; progress: { display: number | null; }; updated_at: { display: JSX.Element; }; } | null; }; workflow: { display: string | { title: { display: string; }; branch: { display: string\' is not assignable to type \'Record\'.\\n Property \'title\' is incompatible with index signature.\\n Type \'{ display: string; }\' is not assignable to type \'string | number | tRowValue\'.\\n Property \'value\' is missing in type \'{ display: string; }\' but required in type \'tRowValue\'.", "2088305148"]
@@ -245,11 +242,8 @@ exports[`fix ts error`] = {
"src/pages/resource-manager/resource-pool-details.tsx:1673813627": [
[157, 72, 4, "tsc: Property \'node\' does not exist on type \'NodeAttributeWithMetadata | NodeRelationshipManyWithMetadata | NodeRelationshipOneWithMetadata | string | string[]\'.\\n Property \'node\' does not exist on type \'string\'.", "2087865285"]
],
- "src/shared/components/display/badge-circle.tsx:2957256666": [
- [6, 12, 18, "tsc: This syntax is not allowed when \'erasableSyntaxOnly\' is enabled.", "460855417"]
- ],
- "src/shared/components/editor/json/json-editor.tsx:3454594744": [
- [49, 49, 20, "tsc: Argument of type \'Grammar | undefined\' is not assignable to parameter of type \'Grammar\'.\\n Type \'undefined\' is not assignable to type \'Grammar\'.", "2776531305"]
+ "src/shared/components/editor/json/json-editor.tsx:1301116350": [
+ [45, 49, 20, "tsc: Argument of type \'Grammar | undefined\' is not assignable to parameter of type \'Grammar\'.\\n Type \'undefined\' is not assignable to type \'Grammar\'.", "2776531305"]
],
"src/shared/components/errors/error-boundary-app.tsx:3236611445": [
[5, 24, 5, "tsc: Type \'unknown\' is not assignable to type \'Error\'.", "165548477"]
@@ -311,10 +305,10 @@ exports[`fix ts error`] = {
[56, 30, 8, "tsc: Argument of type \'null | { id: string; } | { id: string; }[]\' is not assignable to parameter of type \'NodeCore | NodeCore[] | boolean | null | number | string | string[]\'.\\n Type \'{ id: string; }\' is not assignable to type \'NodeCore | NodeCore[] | boolean | null | number | string | string[]\'.\\n Property \'__typename\' is missing in type \'{ id: string; }\' but required in type \'NodeCore\'.", "288015442"],
[89, 30, 8, "tsc: Argument of type \'null | { id: string; } | { id: string; }[]\' is not assignable to parameter of type \'NodeCore | NodeCore[] | boolean | null | number | string | string[]\'.\\n Type \'{ id: string; }\' is not assignable to type \'NodeCore | NodeCore[] | boolean | null | number | string | string[]\'.\\n Property \'__typename\' is missing in type \'{ id: string; }\' but required in type \'NodeCore\'.", "288015442"]
],
- "src/shared/components/inputs/dropdown.tsx:228728578": [
+ "src/shared/components/inputs/dropdown.tsx:1988763455": [
[202, 24, 14, "tsc: \'formData.value\' is possibly \'undefined\'.", "945600486"]
],
- "src/shared/components/inputs/enum.tsx:3431642447": [
+ "src/shared/components/inputs/enum.tsx:351438738": [
[131, 33, 13, "tsc: \'formData.enum\' is possibly \'undefined\'.", "1980161662"]
],
"src/shared/components/inputs/list.test.tsx:3096980043": [
@@ -328,11 +322,11 @@ exports[`fix ts error`] = {
[102, 54, 2, "tsc: Property \'id\' does not exist on type \'Node | PoolValue\'.\\n Property \'id\' does not exist on type \'PoolValue\'.", "5861160"],
[150, 23, 5, "tsc: Argument of type \'NodeCore\' is not assignable to parameter of type \'Node | PoolValue | null\'.\\n Type \'NodeCore\' is not assignable to type \'Node\'.\\n Types of property \'display_label\' are incompatible.\\n Type \'null | string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "189936718"]
],
- "src/shared/components/table/table.tsx:3721991410": [
+ "src/shared/components/table/table.tsx:3565150288": [
[67, 40, 23, "tsc: Argument of type \'number | string | tRowValue | undefined\' is not assignable to parameter of type \'number | string | tRowValue\'.\\n Type \'undefined\' is not assignable to type \'number | string | tRowValue\'.", "1426455104"],
[73, 40, 23, "tsc: Argument of type \'number | string | tRowValue | undefined\' is not assignable to parameter of type \'number | string | tRowValue\'.\\n Type \'undefined\' is not assignable to type \'number | string | tRowValue\'.", "1426455104"]
],
- "src/shared/components/ui/alert.tsx:85410580": [
+ "src/shared/components/ui/alert.tsx:1540020858": [
[15, 12, 11, "tsc: This syntax is not allowed when \'erasableSyntaxOnly\' is enabled.", "2602525471"]
]
}`
diff --git a/frontend/app/AGENTS.md b/frontend/app/AGENTS.md
index c2f8cc305c4..01780233c82 100644
--- a/frontend/app/AGENTS.md
+++ b/frontend/app/AGENTS.md
@@ -52,6 +52,7 @@ cd frontend/app && pnpm test # vitest (browser mode)
- `dev/knowledge/frontend/entities-structure.md` - Entity layer pattern (api/domain/ui), GraphQL fetching, backend authority
- `dev/knowledge/frontend/shared-components.md` - **Reuse-first inventory** — look here before building anything generic
- `dev/knowledge/frontend/design-system.md` - `@infrahub/ui` package (Button, Card, Modal, Spinner)
+- `dev/knowledge/frontend/theming.md` - **Read before touching colours** — theme tokens, the dark class, how to change a colour in one theme only
- `dev/knowledge/frontend/file-components.md` - DataViewer and file handling components
- `dev/knowledge/frontend/auth-methods.md` - Auth method registry, picker, token persistence boundaries
- `dev/knowledge/frontend/branches.md` - Read before writing code that depends on which branch is current, or on the default branch — the default branch name is deployment-configurable
diff --git a/frontend/app/index.html b/frontend/app/index.html
index b7ebd850bc5..d7f02c2bbad 100644
--- a/frontend/app/index.html
+++ b/frontend/app/index.html
@@ -9,6 +9,24 @@
Infrahub
+
diff --git a/frontend/app/package.json b/frontend/app/package.json
index ac255155902..f81dee8258f 100644
--- a/frontend/app/package.json
+++ b/frontend/app/package.json
@@ -74,6 +74,7 @@
"jotai": "^2.20.2",
"json-to-graphql-query": "^2.3.0",
"lucide-react": "catalog:",
+ "mermaid": "^11.0.0",
"monaco-editor": "0.52.2",
"monaco-graphql": "^1.8.0",
"nuqs": "^2.9.5",
diff --git a/frontend/app/src/app/app.tsx b/frontend/app/src/app/app.tsx
index 64509dc5c52..2b770f13eae 100644
--- a/frontend/app/src/app/app.tsx
+++ b/frontend/app/src/app/app.tsx
@@ -15,10 +15,12 @@ import { store } from "@/shared/stores";
import { AuthProvider } from "@/entities/authentication/ui/auth-provider";
import { ConfigProvider } from "@/entities/config/ui/config-provider";
+import { ThemeProvider } from "@/entities/config/ui/theme-provider";
import { DatePreferencesProvider } from "@/entities/preferences/ui/date-preferences-provider";
import "@/app/styles/index.css";
import "react-toastify/dist/ReactToastify.css";
+import "@/app/styles/toastify-overrides.css";
addCollection(mdiIcons);
@@ -30,9 +32,11 @@ export function App() {
-
-
-
+
+
+
+
+
diff --git a/frontend/app/src/app/styles/markdown.css b/frontend/app/src/app/styles/markdown.css
index 068f7dd612c..158b56681db 100644
--- a/frontend/app/src/app/styles/markdown.css
+++ b/frontend/app/src/app/styles/markdown.css
@@ -191,7 +191,7 @@
}
.markdown .mermaid-error {
- background-color: #fef2f2;
- color: #b91c1c;
+ background-color: var(--danger-surface);
+ color: var(--danger);
white-space: pre-wrap;
}
diff --git a/frontend/app/src/app/styles/toastify-overrides.css b/frontend/app/src/app/styles/toastify-overrides.css
new file mode 100644
index 00000000000..bd0c60d4b5f
--- /dev/null
+++ b/frontend/app/src/app/styles/toastify-overrides.css
@@ -0,0 +1,16 @@
+/* react-toastify's default "light" theme paints a hardcoded white card, so toasts stayed a light
+ island in dark mode. It themes through its own custom properties, so re-pointing the light
+ theme's two surface variables at our tokens is enough: the tokens flip under `.dark`, and the
+ toast follows without a `theme` prop or any JSX. Icon colours (success/error/…) are semantic
+ and deliberately left alone. */
+
+:root {
+ --toastify-color-light: var(--content);
+ --toastify-text-color-light: var(--foreground);
+}
+
+/* The close button is the one colour outside the variable system: hardcoded near-black, which
+ disappears against a dark card. */
+.Toastify__close-button--light {
+ color: var(--foreground);
+}
diff --git a/frontend/app/src/assets/icons/tasks-status.svg b/frontend/app/src/assets/icons/tasks-status.svg
index a044d980a16..4ff6b22bebe 100644
--- a/frontend/app/src/assets/icons/tasks-status.svg
+++ b/frontend/app/src/assets/icons/tasks-status.svg
@@ -1,9 +1,9 @@
diff --git a/frontend/app/src/entities/authentication/ui/login-method-picker.tsx b/frontend/app/src/entities/authentication/ui/login-method-picker.tsx
index e3f1c5320ee..9e72b9ac465 100644
--- a/frontend/app/src/entities/authentication/ui/login-method-picker.tsx
+++ b/frontend/app/src/entities/authentication/ui/login-method-picker.tsx
@@ -22,7 +22,7 @@ export const LoginMethodPicker = () => {
const [active, setActive] = useLastUsedMethod(methods, preferredDefault(methods));
if (methods.length === 0 || !active) {
- return