feat(ui): per-service config in the deploy wizard's CONFIGURATION step

Setting a password, banner or TLS material AFTER deployment forces a
container recreate on every change. The deploy wizard now lets the
operator set service config up-front so the initial build has the
right env from the start.

Mechanics:
- Extracted the schema-driven field rendering out of ServiceConfigForm
  into a standalone ServiceConfigFields component (no API/buttons,
  just inputs + onChange).  ServiceConfigForm now delegates to it.
- Wizard step 2 (CONFIGURATION) renders one accordion block per
  selected service; clicking a service reveals its schema-driven
  inputs and a 'N set' badge tracks how many overrides are populated.
  Removing a service (back to step 1) drops its config so the INI
  doesn't carry orphans.
- _buildIni emits one [<prefix>.<svc>] group subsection per service
  with at least one override.  The INI loader's prefix-matcher
  applies it to every ${prefix}-NN decky in the batch, so one block
  covers all clones.
- Multi-line string values (PEM textareas etc.) are escaped as \n
  on the way into INI; downstream consumers re-expand.
This commit is contained in:
2026-04-29 12:08:17 -04:00
parent 97260daf8d
commit d8fa7cc73d
4 changed files with 400 additions and 206 deletions

View File

@@ -141,6 +141,45 @@
}
.info-banner em { color: var(--matrix); font-style: normal; }
/* Per-service config accordion in the deploy wizard's CONFIGURATION
step. Each block is one service slug; clicking the toggle reveals
the schema-driven fields rendered by ServiceConfigFields. */
.wizard-svc-list { display: flex; flex-direction: column; gap: 6px; }
.wizard-svc-block { border: 1px solid var(--border); }
.wizard-svc-toggle {
display: flex; align-items: center; gap: 8px;
width: 100%;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.02);
border: none;
color: inherit;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 1px;
text-transform: uppercase;
cursor: pointer;
text-align: left;
}
.wizard-svc-toggle:hover { background: rgba(255, 255, 255, 0.05); }
.wizard-svc-toggle.open { border-bottom: 1px solid var(--border); }
.wizard-svc-caret { color: var(--violet); width: 10px; }
.wizard-svc-name { color: var(--matrix); flex: 1; }
.wizard-svc-badge {
font-size: 0.6rem;
letter-spacing: 1px;
color: var(--violet);
border: 1px solid var(--violet);
padding: 1px 6px;
border-radius: 999px;
}
.wizard-svc-fields {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: rgba(0, 0, 0, 0.25);
}
/* Status dots */
.status-dot {
display: inline-block;