fix: Add token editing, DDNS record management, wildcard display

Dashboard:
- Cloudflare token can be changed when already configured (was only
  editable when missing/invalid)
- DDNS records can be added and removed inline (was read-only)

Services:
- Wildcard services display as *.domain instead of domain with a
  separate subdomains badge — clearer at a glance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 22:45:17 +00:00
parent 9ce9999d5c
commit 7d703832ab
2 changed files with 77 additions and 10 deletions

View File

@@ -7,7 +7,7 @@ import { Alert, AlertDescription } from './ui/alert';
import { Badge } from './ui/badge';
import {
LayoutDashboard, Cloud, CheckCircle, XCircle, AlertCircle, Loader2,
RefreshCw, BookOpen, Globe, Shield, Router, Server, RotateCw, Key,
RefreshCw, BookOpen, Globe, Shield, Router, Server, RotateCw, Key, Plus, X,
} from 'lucide-react';
import { useCloudflare } from '../hooks/useCloudflare';
import { useDdns } from '../hooks/useDdns';
@@ -23,7 +23,7 @@ export function DashboardComponent() {
const { verification, isLoading: cfLoading } = useCloudflare();
const { status: ddnsStatus, isLoadingStatus: ddnsLoading, trigger: triggerDdns, isTriggering } = useDdns();
const { status: certStatus } = useCert();
const { config: globalConfig } = useConfig();
const { config: globalConfig, updateConfig } = useConfig();
const queryClient = useQueryClient();
const { data: globalSecrets } = useQuery({
@@ -36,6 +36,8 @@ export function DashboardComponent() {
const [editingToken, setEditingToken] = useState(false);
const [tokenValue, setTokenValue] = useState('');
const [addingDdnsRecord, setAddingDdnsRecord] = useState(false);
const [newDdnsRecord, setNewDdnsRecord] = useState('');
const updateSecretsMutation = useMutation({
mutationFn: (values: Record<string, unknown>) => secretsApi.update(values),
@@ -168,6 +170,29 @@ export function DashboardComponent() {
</div>
</div>
)}
{!editingToken ? (
<Button variant="ghost" size="sm" onClick={() => setEditingToken(true)} className="gap-1 text-muted-foreground">
<Key className="h-3 w-3" />
Change Token
</Button>
) : (
<div className="space-y-2">
<Input
type="password"
placeholder="New Cloudflare API token..."
value={tokenValue}
onChange={(e) => setTokenValue(e.target.value)}
/>
<div className="flex gap-2">
<Button size="sm" onClick={() => updateSecretsMutation.mutate({ cloudflare: { apiToken: tokenValue } })} disabled={!tokenValue}>
Save
</Button>
<Button size="sm" variant="ghost" onClick={() => { setEditingToken(false); setTokenValue(''); }}>
Cancel
</Button>
</div>
</div>
)}
</div>
) : (
<div className="ml-7">
@@ -249,13 +274,13 @@ export function DashboardComponent() {
</div>
</div>
{/* Record list */}
{/* Record list with remove buttons */}
{globalConfig?.cloud?.ddns?.records?.length ? (
<div className="space-y-1">
{globalConfig.cloud.ddns.records.map((r) => {
const rs: DdnsRecordStatus | undefined = ddnsStatus.records?.find(s => s.name === r);
{globalConfig.cloud.ddns.records.map((r: string) => {
const rs: DdnsRecordStatus | undefined = ddnsStatus.records?.find((s: DdnsRecordStatus) => s.name === r);
return (
<div key={r} className="flex items-center gap-2 text-xs">
<div key={r} className="flex items-center gap-2 text-xs group">
{rs?.ok ? (
<CheckCircle className="h-3.5 w-3.5 text-green-500 shrink-0" />
) : rs && !rs.ok ? (
@@ -270,12 +295,57 @@ export function DashboardComponent() {
{rs && !rs.ok && rs.error && (
<span className="text-red-500 truncate" title={rs.error}>{rs.error}</span>
)}
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 ml-auto"
onClick={async () => {
if (!globalConfig) return;
const records = (globalConfig.cloud?.ddns?.records || []).filter((rec: string) => rec !== r);
await updateConfig({
...globalConfig,
cloud: { ...globalConfig.cloud, ddns: { ...globalConfig.cloud?.ddns, records } },
});
}}
>
<X className="h-3 w-3" />
</Button>
</div>
);
})}
</div>
) : null}
{/* Add DDNS record */}
{!addingDdnsRecord ? (
<Button variant="ghost" size="sm" onClick={() => setAddingDdnsRecord(true)} className="gap-1 text-muted-foreground">
<Plus className="h-3 w-3" />
Add Record
</Button>
) : (
<div className="flex gap-2 items-center">
<Input
value={newDdnsRecord}
onChange={(e) => setNewDdnsRecord(e.target.value)}
placeholder="dev.payne.io"
className="h-8 text-xs font-mono flex-1"
/>
<Button size="sm" className="h-8" disabled={!newDdnsRecord} onClick={async () => {
if (!globalConfig || !newDdnsRecord) return;
const records = [...(globalConfig.cloud?.ddns?.records || []), newDdnsRecord];
await updateConfig({
...globalConfig,
cloud: { ...globalConfig.cloud, ddns: { ...globalConfig.cloud?.ddns, records } },
});
setNewDdnsRecord('');
setAddingDdnsRecord(false);
}}>Add</Button>
<Button size="sm" variant="ghost" className="h-8" onClick={() => { setAddingDdnsRecord(false); setNewDdnsRecord(''); }}>
<X className="h-3 w-3" />
</Button>
</div>
)}
<Button
variant="outline"
size="sm"