mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat(nodes): per-node default domain with auto service hostnames (#65)
node_agents.default_domain gives each agent a base domain. Services
placed on it (explicit pin or spread) with no explicit service_domains
get <name>.<default_domain> — resolved in serviceDomainNames so edge
routing, wake placeholders, and public_url all see the same names.
Spread services union every eligible node's base domain; explicit
domains always win.
Surfaces: PUT /agents/:id {default_domain} (hostname-validated), CLI
nodes update --domain, MCP arg docs, OpenAPI + regenerated types,
node-detail inline editor.
Also marks the already-shipped backup-import roadmap item.
Generated with [Devin](https://devin.ai)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
a4bb7ad559
commit
1409186a1f
17 files changed
+294
-41
No files matched your search
@@ -48,6 +48,7 @@ export function NodeDetailPage() {
|
||||
const [confirmVolumes, setConfirmVolumes] = useState(false);
|
||||
const [confirmDrain, setConfirmDrain] = useState(false);
|
||||
const [tagDraft, setTagDraft] = useState<string | null>(null);
|
||||
const [domainDraft, setDomainDraft] = useState<string | null>(null);
|
||||
|
||||
const agentQuery = useQuery({
|
||||
queryKey: ['node-agent', id],
|
||||
@@ -80,8 +81,8 @@ export function NodeDetailPage() {
|
||||
queryClient.invalidateQueries({ queryKey: ['usage-agents'] });
|
||||
};
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (body: { name?: string; auto_prune?: boolean; tags?: string[] }) => updateAgent(id, body),
|
||||
onSuccess: () => { setRename(null); setTagDraft(null); invalidate(); },
|
||||
mutationFn: (body: { name?: string; auto_prune?: boolean; tags?: string[]; default_domain?: string }) => updateAgent(id, body),
|
||||
onSuccess: () => { setRename(null); setTagDraft(null); setDomainDraft(null); invalidate(); },
|
||||
});
|
||||
const pruneMutation = useMutation({
|
||||
mutationFn: (volumes: boolean) => pruneAgent(id, { volumes }),
|
||||
@@ -218,6 +219,44 @@ export function NodeDetailPage() {
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className="v-mono mt-1 text-[11px] text-[var(--text-tertiary)] flex items-center gap-1.5 flex-wrap">
|
||||
domain:
|
||||
{domainDraft === null ? (
|
||||
<>
|
||||
<span className={agent.defaultDomain ? 'text-[var(--text-secondary)]' : 'text-[var(--text-muted)]'}>
|
||||
{agent.defaultDomain || 'none'}
|
||||
</span>
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => setDomainDraft(agent.defaultDomain || '')}
|
||||
className="text-[var(--text-muted)] hover:text-[var(--text-primary)]"
|
||||
title="Node base domain — domainless services placed here get <name>.<domain> hostnames"
|
||||
>
|
||||
<Pencil size={11} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
value={domainDraft}
|
||||
onChange={(e) => setDomainDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') updateMutation.mutate({ default_domain: domainDraft.trim().toLowerCase() });
|
||||
if (e.key === 'Escape') setDomainDraft(null);
|
||||
}}
|
||||
autoFocus
|
||||
placeholder="apps.node1.example.com — empty clears"
|
||||
className="rounded-md border border-[var(--border-subtle)] bg-[var(--bg-tertiary)] px-2 py-0.5 text-[11px] text-[var(--text-primary)] w-64"
|
||||
/>
|
||||
<button
|
||||
onClick={() => updateMutation.mutate({ default_domain: domainDraft.trim().toLowerCase() })}
|
||||
className="text-[var(--accent-primary)] hover:underline"
|
||||
>save</button>
|
||||
<button onClick={() => setDomainDraft(null)} className="text-[var(--text-muted)] hover:underline">cancel</button>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`v-mono text-[11px] px-2 py-1 rounded-md border ${online
|
||||
|
||||
Reference in new issue
Block a user