Feature Specification: Configuration form

Created: 2026-09-23

Status: Implemented

Input: Reconstructed from the code of panel/ at 0bf4ecd36, the documentation of the panel, api/specification.md, the specifications of nix-module-form (pull request nix-module-form#27), pull requests #1017, #1022, #1056, #1073, #1102, #1115, #1117, #1188, #1200, #1219, #1329, #1343, #1791 and #1795, and the design notes of the maintainers.

User Scenarios & Testing (mandatory)

User Story 1 - Edit a configuration in a form made from the schema (Priority: P1)

The browser gets the JSON Schema of the configuration from api, and shows a form that nix-module-form makes from it. The operator edits the form. The panel stores the value that the form gives. The panel does not know the options.

Why this priority: the options change with each version of core. The form must follow without a change in the panel.

Independent Test: a VM test where the operator edits a deployment in the form and saves it, and a unit test that proves that the panel server does not serve or read the schema.

Acceptance Scenarios:

  1. Given a deployment without a module source pin, When the form opens, Then the browser gets the schema of the default source from api.
  2. Given a deployment with a module source pin, When the form opens, Then the browser asks api for the schema of that source with a bearer token, and waits while api builds it.
  3. Given a deployment that has no value, When the form opens, Then the form starts from the defaults of the schema, with the seed document that the hosting provider configures laid over them.
  4. Given an error of api, When the form cannot open, Then the page shows the message of api as text, disables Deploy and Validate, and keeps Save.

User Story 2 - See what changed since the last deploy (Priority: P1)

The form marks the fields that differ from the configuration that runs now. Deploy is disabled when nothing changed.

Why this priority: the operator must know what a deploy will change.

Independent Test: a unit test of the deployment form that proves that the baseline is the deployed configuration from api, not the submitted one.

Acceptance Scenarios:

  1. Given a deployed configuration, When the operator changes a field, Then the form marks the field, and Deploy is enabled.
  2. Given a submitted configuration that failed, When the form opens, Then the baseline is the last deployed configuration, and the operator can deploy again.
  3. Given a deployment that was never deployed, or that api does not know, When the form opens, Then the form has no baseline.

User Story 3 - Keep old values after a schema change (Priority: P2)

When the options change, the form moves old values to their new names with the rename table of api, and shows the schema changes against the deployed version. Values that cannot move stay under their old names, and the page says so.

Independent Test: a VM test where a value with an old name opens under the new name.

Acceptance Scenarios:

  1. Given a value with a renamed option, When the form opens, Then the value shows under the new name.
  2. Given a value that cannot move, When the form opens, Then a banner lists the old and new names.

User Story 4 - Edit one section (Priority: P3)

From the instances overview, the operator opens the section that configures one instance. api gives the sections of the configuration and their places, and the schema and the renames of one section. The page shows the form of that section only, and saves the value of the section back into the whole configuration. The panel does not find the place of a section, and does not apply renames to paths itself.

Independent Test: a unit test of the section page.

Acceptance Scenarios:

  1. Given a saved deployment, When the operator opens a section, Then the page shows the form of that section, with the label that api gives.
  2. Given a configuration that has no place for the value of the section, When the operator opens the section, Then the page tells the operator to save the deployment first.
  3. Given a section that api does not declare, When the operator opens it, Then the page shows the refusal of api.
  4. Given a deployed configuration, When the operator opens a section, Then the diff compares with the value at the deployed place of the section that api gives.
  5. Given an earlier address of a section page, When the operator opens it, Then the panel redirects to the current address.

Edge Cases

  • The schema build for a module source pin does not end in a bounded time: the form stops and shows an error.
  • Validation finds errors: Validate lists them. Save still stores the draft.
  • The operator types a secret and uses Save, not Deploy: the panel stores the draft without the secret, so the secret is lost. Deploy keeps it.
  • A draft that a panel stored before a property became writeOnly still holds its value. The next Save or Deploy removes it from the draft, and Deploy moves it to OpenBao.
  • The operator changes the branch of a union: the form keeps the values that both branches share.

Requirements (mandatory)

Functional Requirements

  • FR-001: The panel MUST get the schema, the rename table and the facts about a configuration from api.
  • FR-002: The panel MUST render the configuration with nix-module-form from the schema, and MUST NOT have a form that names options.
  • FR-003: The panel MUST store and submit the value without change, except the renames that the rename table of api gives, and the secrets of FR-008.
  • FR-004: The diff baseline MUST be the configuration that api reports as deployed.
  • FR-005: The panel MUST keep Save available when the schema or the form fails.
  • FR-006: The panel MUST show messages of api and of the schema as text, not as markup.
  • FR-007: The panel MUST start a value that was never saved from the root default of the schema, with its configured seed document laid over it, and MUST NOT read the option names in the seed.
  • FR-008: An end-to-end check MUST hold the configuration of core that it needs in panel/checks/fixture/deployment-values.nix only. The unit tests MUST use a neutral fixture schema.
  • FR-009: The panel MUST NOT store the value of a property that the schema marks writeOnly, a secret that the operator gives. Deploy submits the full value to api, which keeps the secret in OpenBao. Save and Deploy store the draft without it: the browser removes it (static/deployment_secrets.js), and the server of the panel removes it again with the schema that api serves for the module source of the draft (panel/secrets.py), so that a POST that does not come from the form also stores no secret. The form shows such a property as a password field, empty in a stored draft. secretsSet says which secrets api has. A secret that the draft held a value for also leaves the secretsClear list of its application in the stored draft, as it does in api, so that a later Deploy does not clear the new value.

Key Entities

  • Schema: the JSON Schema that api serves for a module source, optionally for one section.
  • Rename table: the renames of options that api serves for a module source, optionally for one section.
  • Section: a part of the configuration that a form edits on its own, as api gives it: an identifier, a label, its place in the configuration, its place in the deployed configuration, and whether it changed.
  • Baseline: the deployed configuration and module source, from api.
  • Adapter: the panel side of nix-module-form.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: A change of the options in core needs no change in the panel.
  • SC-002: The server code, the browser code, the templates, the scripts and the tests of the panel contain no option names of the configuration, outside of panel/checks/fixture/deployment-values.nix.

Assumptions

  • api serves the sections of a configuration, and the schema and the renames of one section, with the options of that section as the root.

Known Deviations

  • When api does not give the schema, the server of the panel stores the draft as it is, so that Save stays available (FR-005). A draft that the panel stores then can hold a secret, until the next Save or Deploy with the schema.
  • Save without Deploy does not keep a secret that the operator typed: the panel removes it from the draft, and only Deploy gives it to api. The operator types it again before Deploy.