fix: autosave, cell name overflow, length validation, apply-and-verify tests

Autosave on Apply (was broken):
- App.jsx called useDraftConfig() in the same component that rendered
  DraftConfigProvider — a component cannot consume context it provides.
  Fixed by splitting into AppCore (consumes context, all logic) and App
  (thin shell that wraps AppCore in DraftConfigProvider).  The hook now
  runs inside the provider and hasDirty()/flushAll() work correctly.

Cell name / domain length validation (255-char DNS standard):
- api/app.py: reject cell_name or domain > 255 chars or empty with 400
- api/app.py: reject ip_range without CIDR prefix (bare IPs shift all VIPs)
- webui/src/pages/Settings.jsx: cellNameError + domainError computed values
  block saveIdentity and show inline error; maxLength={255} on inputs
- tests/test_identity_validation.py: 8 unit tests for the new validation

Cell name overflow on all pages:
- Dashboard.jsx: add min-w-0 to flex child div + truncate + title on cell_name
- CellNetwork.jsx: min-w-0 + truncate + title on cell_name, domain, endpoint,
  vpn_subnet in invite cards and connected-cells list

Apply-and-verify integration tests:
- tests/integration/test_apply_propagation.py: TestPendingState (no restarts)
  and TestApplyAndVerify (triggers real container restart + health poll)
  covering the full save → apply → wait → verify propagation lifecycle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 05:29:09 -04:00
parent 3ce45a8911
commit 4215e03ac6
7 changed files with 504 additions and 26 deletions
+14 -4
View File
@@ -457,8 +457,16 @@ function Settings() {
? 'Must be within an RFC-1918 range: 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16'
: null;
const cellNameError = identity.cell_name && identity.cell_name.length > 255
? 'Cell name must be 255 characters or fewer'
: (!identity.cell_name ? 'Cell name is required' : null);
const domainError = identity.domain && identity.domain.length > 255
? 'Domain must be 255 characters or fewer'
: (!identity.domain ? 'Domain is required' : null);
const saveIdentity = async () => {
if (ipRangeError) return;
if (ipRangeError || cellNameError || domainError) return;
setIdentitySaving(true);
try {
const res = await cellAPI.updateConfig(identity);
@@ -622,18 +630,20 @@ function Settings() {
{/* Cell Identity */}
<Section icon={Server} title="Cell Identity">
<div className="space-y-3">
<Field label="Cell Name">
<Field label="Cell Name" error={cellNameError}>
<TextInput
value={identity.cell_name}
onChange={(v) => { setIdentity((i) => ({ ...i, cell_name: v })); setIdentityDirty(true); draftConfig?.setDirty('identity', true); }}
placeholder="mycell"
maxLength={255}
/>
</Field>
<Field label="Domain">
<Field label="Domain" error={domainError}>
<TextInput
value={identity.domain}
onChange={(v) => { setIdentity((i) => ({ ...i, domain: v })); setIdentityDirty(true); draftConfig?.setDirty('identity', true); }}
placeholder="cell.local"
maxLength={255}
/>
</Field>
<Field label="IP Range" hint="Docker bridge subnet" error={ipRangeError}>
@@ -647,7 +657,7 @@ function Settings() {
<div className="flex justify-end mt-4">
<button
onClick={saveIdentity}
disabled={!identityDirty || identitySaving || !!ipRangeError}
disabled={!identityDirty || identitySaving || !!ipRangeError || !!cellNameError || !!domainError}
className="btn-primary flex items-center gap-2 text-sm disabled:opacity-50"
>
{identitySaving ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}