From 900781032a758a9d5991a88971fd92349c164be7 Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Tue, 26 May 2026 07:09:57 -0400 Subject: [PATCH] =?UTF-8?q?wizard:=205-step=20redesign=20=E2=80=94=20passw?= =?UTF-8?q?ord,=20domain,=20timezone,=20services,=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domain name is now the cell identity (no separate cell name step). All 5 providers (pic_ngo, cloudflare, duckdns, http01, lan) are first-class options in a single Domain step. pic.ngo availability is checked live via backend proxy to ddns.pic.ngo. Cloudflare and DuckDNS tokens are verified via backend before proceeding. cell_name is derived automatically from the chosen domain. Co-Authored-By: Claude Sonnet 4.6 --- webui/src/pages/Setup.jsx | 1015 ++++++++++++++----------------------- 1 file changed, 367 insertions(+), 648 deletions(-) diff --git a/webui/src/pages/Setup.jsx b/webui/src/pages/Setup.jsx index 8830878..1ec3cb1 100644 --- a/webui/src/pages/Setup.jsx +++ b/webui/src/pages/Setup.jsx @@ -5,51 +5,44 @@ import { setupAPI } from '../services/api'; // ── constants ───────────────────────────────────────────────────────────────── -const TOTAL_STEPS = 7; +const TOTAL_STEPS = 5; const CELL_NAME_RE = /^[a-z][a-z0-9-]{1,30}$/; +const DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i; const DOMAIN_OPTIONS = [ { value: 'pic_ngo', label: 'PIC.NGO subdomain', - description: 'Get a free yourname.pic.ngo address — HTTPS and DDNS managed automatically.', + description: 'Free yourname.pic.ngo address — HTTPS and DDNS fully automatic.', }, - { - value: 'custom', - label: 'Custom domain', - description: 'Use your own domain with Cloudflare, DuckDNS, or standard HTTP challenge.', - }, - { - value: 'lan', - label: 'LAN only', - description: 'No public domain. Accessible only on your local network and via VPN.', - }, -]; - -const CUSTOM_METHOD_OPTIONS = [ { value: 'cloudflare', - label: 'Cloudflare DNS', - description: 'DNS-01 via Cloudflare API. Your domain must use Cloudflare nameservers.', + label: 'Cloudflare DNS-01', + description: 'Your own domain via Cloudflare API. Domain must use Cloudflare nameservers.', }, { value: 'duckdns', label: 'DuckDNS', - description: 'Free subdomain via duckdns.org with automatic DNS-01 challenge.', + description: 'Free *.duckdns.org subdomain with automatic DNS-01 challenge.', }, { value: 'http01', - label: 'HTTP-01 (any registrar)', - description: 'Standard ACME challenge. Port 80 must be publicly reachable.', + label: 'Any domain (HTTP-01)', + description: 'Any domain via standard ACME. Port 80 must be publicly reachable.', + }, + { + value: 'lan', + label: 'Local only', + description: 'No public domain. Accessible only on your local network and via VPN.', }, ]; const OPTIONAL_SERVICES = [ - { key: 'email', label: 'Email', description: 'Postfix + Dovecot IMAP/SMTP server.' }, + { key: 'email', label: 'Email', description: 'Postfix + Dovecot IMAP/SMTP server.' }, { key: 'calendar', label: 'Calendar & Contacts', description: 'CalDAV/CardDAV via Radicale.' }, - { key: 'files', label: 'Files (WebDAV)', description: 'WebDAV file storage accessible from any device.' }, - { key: 'webmail', label: 'Webmail UI', description: 'Browser-based email client (Roundcube).' }, + { key: 'files', label: 'Files (WebDAV)', description: 'WebDAV file storage accessible from any device.' }, + { key: 'webmail', label: 'Webmail UI', description: 'Browser-based email client (Roundcube).' }, ]; const ALWAYS_ON_SERVICES = [ @@ -61,9 +54,8 @@ const ALWAYS_ON_SERVICES = [ // ── helpers ─────────────────────────────────────────────────────────────────── function getAllTimezones() { - try { - return Intl.supportedValuesOf('timeZone'); - } catch { + try { return Intl.supportedValuesOf('timeZone'); } + catch { return [ 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', @@ -91,13 +83,22 @@ function meetsApiRequirements(pw) { return pw.length >= 12 && /[A-Z]/.test(pw) && /[a-z]/.test(pw) && /[0-9]/.test(pw); } -function getDomainMode(domainType, customMethod) { - if (domainType === 'pic_ngo') return 'pic_ngo'; - if (domainType === 'lan') return 'lan'; - return customMethod || 'http01'; +function deriveCellName(domainType, picName, duckSub, customDomain) { + if (domainType === 'pic_ngo') return picName.toLowerCase(); + if (domainType === 'duckdns') return duckSub.toLowerCase(); + if (domainType === 'lan') return 'cell'; + const seg = (customDomain || '').split('.')[0].toLowerCase().replace(/[^a-z0-9-]/g, ''); + return CELL_NAME_RE.test(seg) ? seg : 'cell'; } -// ── sub-components ──────────────────────────────────────────────────────────── +function getDomainName(domainType, picName, duckSub, customDomain) { + if (domainType === 'pic_ngo') return `${picName}.pic.ngo`; + if (domainType === 'duckdns') return `${duckSub}.duckdns.org`; + if (domainType === 'lan') return ''; + return customDomain; +} + +// ── shared UI ───────────────────────────────────────────────────────────────── function StepHeader({ step, title, description }) { return ( @@ -116,17 +117,13 @@ function ProgressBar({ step }) { return (
- Setup progress - {pct}% + Setup progress{pct}%
@@ -137,28 +134,17 @@ function FieldError({ message }) { if (!message) return null; return (

- - {message} + {message}

); } function RadioOption({ value, selected, label, description, onChange }) { return ( -