diff --git a/api/setup_manager.py b/api/setup_manager.py
index fed16f7..788fd28 100644
--- a/api/setup_manager.py
+++ b/api/setup_manager.py
@@ -128,9 +128,12 @@ class SetupManager:
cell_name = payload.get('cell_name', '')
password = payload.get('password', '')
domain_mode = payload.get('domain_mode', '')
+ domain_name = payload.get('domain_name', '')
timezone = payload.get('timezone', '')
services_enabled = payload.get('services_enabled', [])
ddns_provider = payload.get('ddns_provider', 'none')
+ cloudflare_api_token = payload.get('cloudflare_api_token', '')
+ duckdns_token = payload.get('duckdns_token', '')
errors.extend(self.validate_cell_name(cell_name))
errors.extend(self.validate_password(password))
@@ -185,16 +188,19 @@ class SetupManager:
# ── persist identity fields ────────────────────────────────────
self.config_manager.set_identity_field('cell_name', cell_name)
self.config_manager.set_identity_field('domain_mode', domain_mode)
+ if domain_name:
+ self.config_manager.set_identity_field('domain_name', domain_name)
self.config_manager.set_identity_field('timezone', timezone)
self.config_manager.set_identity_field('services_enabled', services_enabled)
self.config_manager.set_identity_field('ddns_provider', ddns_provider)
+ if cloudflare_api_token:
+ self.config_manager.set_identity_field('cloudflare_api_token', cloudflare_api_token)
+ if duckdns_token:
+ self.config_manager.set_identity_field('duckdns_token', duckdns_token)
- # NOTE: DDNS registration is deferred to Phase 3.
- # For now we just store ddns_provider in config.
logger.info(
- 'DDNS registration skipped (Phase 1). '
- 'DDNS registration will happen in Phase 3. '
- f'ddns_provider={ddns_provider!r} stored in identity config.'
+ 'DDNS registration deferred to Phase 3. '
+ f'ddns_provider={ddns_provider!r} domain_name={domain_name!r}'
)
# ── mark setup complete (must be last) ─────────────────────────
diff --git a/webui/src/pages/Setup.jsx b/webui/src/pages/Setup.jsx
index da867b5..f3d792d 100644
--- a/webui/src/pages/Setup.jsx
+++ b/webui/src/pages/Setup.jsx
@@ -1,6 +1,6 @@
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
-import { Eye, EyeOff, CheckCircle, AlertCircle } from 'lucide-react';
+import { Eye, EyeOff, CheckCircle, AlertCircle, Globe } from 'lucide-react';
import { setupAPI } from '../services/api';
// ── constants ─────────────────────────────────────────────────────────────────
@@ -13,27 +13,36 @@ const DOMAIN_OPTIONS = [
{
value: 'pic_ngo',
label: 'PIC.NGO subdomain',
- description: 'Get a free yourname.pic.ngo domain — managed automatically.',
+ description: 'Get a free yourname.pic.ngo address — HTTPS and DDNS managed automatically.',
},
{
value: 'custom',
label: 'Custom domain',
- description: 'Bring your own domain. You will configure DNS records manually.',
+ description: 'Use your own domain with Cloudflare, DuckDNS, or standard HTTP challenge.',
},
{
- value: 'lan_only',
+ value: 'lan',
label: 'LAN only',
description: 'No public domain. Accessible only on your local network and via VPN.',
},
];
-const DDNS_OPTIONS = [
- { value: 'pic_ngo', label: 'pic.ngo (managed)', description: 'Automatic — no setup required.' },
- { value: 'cloudflare', label: 'Cloudflare', description: 'Use Cloudflare DNS with API token.' },
- { value: 'duckdns', label: 'DuckDNS', description: 'Free dynamic DNS via duckdns.org.' },
- { value: 'noip', label: 'No-IP', description: 'Free dynamic DNS via noip.com.' },
- { value: 'freedns', label: 'FreeDNS', description: 'Free DNS via freedns.afraid.org.' },
- { value: 'manual', label: 'Manual / None', description: 'You will handle DNS updates yourself.' },
+const CUSTOM_METHOD_OPTIONS = [
+ {
+ value: 'cloudflare',
+ label: 'Cloudflare DNS',
+ description: 'DNS-01 via Cloudflare API. Your domain must use Cloudflare nameservers.',
+ },
+ {
+ value: 'duckdns',
+ label: 'DuckDNS',
+ description: 'Free subdomain via duckdns.org with automatic DNS-01 challenge.',
+ },
+ {
+ value: 'http01',
+ label: 'HTTP-01 (any registrar)',
+ description: 'Standard ACME challenge. Port 80 must be publicly reachable.',
+ },
];
const OPTIONAL_SERVICES = [
@@ -55,35 +64,37 @@ function getAllTimezones() {
try {
return Intl.supportedValuesOf('timeZone');
} catch {
- // Fallback list for older browsers
return [
- 'UTC',
- 'America/New_York',
- 'America/Chicago',
- 'America/Denver',
- 'America/Los_Angeles',
- 'Europe/London',
- 'Europe/Paris',
- 'Europe/Berlin',
- 'Asia/Tokyo',
- 'Asia/Shanghai',
- 'Australia/Sydney',
+ 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver',
+ 'America/Los_Angeles', 'Europe/London', 'Europe/Paris', 'Europe/Berlin',
+ 'Asia/Tokyo', 'Asia/Shanghai', 'Australia/Sydney',
];
}
}
function passwordStrength(pw) {
- if (!pw) return { label: '', color: '', width: '0%' };
+ if (!pw) return { label: '', color: '', width: '0%', score: 0 };
let score = 0;
if (pw.length >= 12) score++;
if (pw.length >= 16) score++;
if (/[A-Z]/.test(pw)) score++;
+ if (/[a-z]/.test(pw)) score++;
if (/[0-9]/.test(pw)) score++;
if (/[^A-Za-z0-9]/.test(pw)) score++;
- if (score <= 1) return { label: 'Weak', color: 'bg-red-500', width: '20%' };
- if (score === 2) return { label: 'Fair', color: 'bg-yellow-500', width: '40%' };
- if (score === 3) return { label: 'Good', color: 'bg-blue-500', width: '65%' };
- return { label: 'Strong', color: 'bg-green-500', width: '100%' };
+ if (score <= 2) return { label: 'Weak', color: 'bg-red-500', width: '20%', score };
+ if (score === 3) return { label: 'Fair', color: 'bg-yellow-500', width: '45%', score };
+ if (score === 4) return { label: 'Good', color: 'bg-blue-500', width: '70%', score };
+ return { label: 'Strong', color: 'bg-green-500', width: '100%', score };
+}
+
+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';
}
// ── sub-components ────────────────────────────────────────────────────────────
@@ -218,12 +229,14 @@ function Step1CellName({ value, onChange, onNext }) {
}
};
+ const isValid = CELL_NAME_RE.test(value);
+
return (