aboutsummaryrefslogtreecommitdiff
path: root/templates
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-08 23:38:54 +0530
committerBobby <[email protected]>2026-03-08 23:38:54 +0530
commit94d5561e7cc39eb2909bdc36d4ef4972cd21e56d (patch)
tree98d9792dda80f76f185ab2eb37c1de005be9ea0f /templates
parent1136af49815be77a0aca151f3b8ec7348bf4b4c8 (diff)
downloaddove-94d5561e7cc39eb2909bdc36d4ef4972cd21e56d.tar.xz
dove-94d5561e7cc39eb2909bdc36d4ef4972cd21e56d.zip
Refactor DNS and SMTP configurations; add system DNS management
- Updated DNS server address configuration to use BindAddress and DnsPort. - Enhanced email submission to utilize BindAddress for SMTP server address. - Improved error messages for unknown recipient domains. - Introduced a new OrderedMap structure for route management. - Added system DNS management functions for Linux, Darwin, and Windows platforms. - Created new dashboard services for DNS configuration and overview. - Updated UI to include Proxy Rules section and improved descriptions. - Added new utility functions for handling DNS configurations.
Diffstat (limited to 'templates')
-rw-r--r--templates/auth/login.django2
-rw-r--r--templates/dashboard/htmx/overview.htmx.django511
-rw-r--r--templates/domains/editdomain.django5
-rw-r--r--templates/domains/htmx/detail.htmx.django288
-rw-r--r--templates/domains/htmx/editdomain.htmx.django48
-rw-r--r--templates/domains/htmx/index.htmx.django20
-rw-r--r--templates/partials/sidebar.django18
7 files changed, 701 insertions, 191 deletions
diff --git a/templates/auth/login.django b/templates/auth/login.django
index babff22..b40f345 100644
--- a/templates/auth/login.django
+++ b/templates/auth/login.django
@@ -10,7 +10,7 @@
</svg>
</div>
<h1 class="text-3xl font-bold gradient-text tracking-tight">Dove</h1>
- <p class="mt-2 text-sm text-zinc-500">Local SMTP server for peaceful email testing</p>
+ <p class="mt-2 text-sm text-zinc-500">Local Infrastructure Service for Peaceful Development Experience.</p>
</div>
{% if ErrorMessage %}
diff --git a/templates/dashboard/htmx/overview.htmx.django b/templates/dashboard/htmx/overview.htmx.django
index 8e1529e..95c4926 100644
--- a/templates/dashboard/htmx/overview.htmx.django
+++ b/templates/dashboard/htmx/overview.htmx.django
@@ -1,57 +1,488 @@
-<div class="slide-up space-y-8">
- <div class="grid grid-cols-3 gap-5">
- <div class="glass rounded-xl p-5 glow-border">
- <div class="flex items-center gap-3 mb-4">
- <div class="flex items-center justify-center w-9 h-9 rounded-lg bg-accent-500/10">
- <svg class="w-4.5 h-4.5 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" />
- </svg>
+<style>
+ @keyframes pulseGlow {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.4; }
+ }
+ .pulse-dot {
+ animation: pulseGlow 2s ease-in-out infinite;
+ }
+ @keyframes barGrow {
+ from { width: 0; }
+ }
+ .animate-bar {
+ animation: barGrow 0.8s ease-out forwards;
+ }
+ .dove-scroll {
+ max-height: 220px;
+ overflow-y: auto;
+ }
+ .dove-scroll::-webkit-scrollbar {
+ width: 4px;
+ }
+ .dove-scroll::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ .dove-scroll::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 2px;
+ }
+ .dove-scroll::-webkit-scrollbar-thumb:hover {
+ background: rgba(255, 255, 255, 0.15);
+ }
+ @keyframes drawLine {
+ from { stroke-dashoffset: var(--line-length); }
+ to { stroke-dashoffset: 0; }
+ }
+ @keyframes fadeGradient {
+ from { opacity: 0; }
+ to { opacity: 1; }
+ }
+ .sparkline-draw {
+ animation: drawLine 1s ease-out forwards;
+ }
+ .sparkline-fill-fade {
+ opacity: 0;
+ animation: fadeGradient 0.6s ease-out 0.4s forwards;
+ }
+ @keyframes statusBarGrow {
+ from { width: 0; }
+ }
+ .status-bar-segment {
+ animation: statusBarGrow 0.6s ease-out forwards;
+ }
+</style>
+
+<div class="slide-up space-y-4">
+
+ <div class="grid grid-cols-2 gap-4">
+
+ <div class="glass rounded-xl glow-border overflow-hidden" data-email-card>
+ <div class="flex items-center justify-between px-5 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">Email</h3>
+ <div class="flex items-center gap-1 text-[10px] text-zinc-600">
+ <button class="px-1.5 py-0.5 rounded bg-white/[0.06] text-zinc-400 transition-colors duration-150" data-email-range="24h" data-email-active>24h</button>
+ <button class="px-1.5 py-0.5 rounded hover:bg-white/[0.04] transition-colors duration-150" data-email-range="7d">7d</button>
+ <button class="px-1.5 py-0.5 rounded hover:bg-white/[0.04] transition-colors duration-150" data-email-range="30d">30d</button>
+ </div>
+ </div>
+ <div class="px-5 pt-3 pb-1">
+ <svg viewBox="0 0 200 40" class="w-full h-20" preserveAspectRatio="none" data-email-svg>
+ <defs>
+ <linearGradient id="emailGradient" x1="0" y1="0" x2="0" y2="1">
+ <stop offset="0%" stop-color="#34d399" stop-opacity="0.15" />
+ <stop offset="100%" stop-color="#34d399" stop-opacity="0" />
+ </linearGradient>
+ </defs>
+ <path d="{{ email.Sparkline24h.FillPath }}" fill="url(#emailGradient)" class="sparkline-fill-fade" data-email-fill />
+ <polyline points="{{ email.Sparkline24h.LinePath }}" fill="none" stroke="#34d399" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="sparkline-draw" data-email-line />
+ </svg>
+ </div>
+ <div class="grid grid-cols-4 gap-4 px-5 py-3 border-t border-white/[0.04]">
+ <div>
+ <p class="text-[10px] text-zinc-600">Mailboxes</p>
+ <p class="text-sm text-zinc-200 font-mono tabular-nums">{{ email.TotalMailboxes }}</p>
+ </div>
+ <div>
+ <p class="text-[10px] text-zinc-600">Delivered</p>
+ <p class="text-sm text-zinc-200 font-mono tabular-nums">{{ email.TotalDelivered }}</p>
+ </div>
+ <div>
+ <p class="text-[10px] text-zinc-600">Bounced</p>
+ <p class="text-sm text-zinc-200 font-mono tabular-nums">{{ email.TotalBounced }}</p>
+ </div>
+ <div>
+ <p class="text-[10px] text-zinc-600">Bounce rate</p>
+ <p class="text-sm text-zinc-200 font-mono tabular-nums">{{ email.BounceRate }}%</p>
+ </div>
+ </div>
+ </div>
+
+ <div class="glass rounded-xl glow-border overflow-hidden flex flex-col" data-domains-card>
+ <div class="flex items-center justify-between px-5 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">Domains</h3>
+ {% if domains.Domains %}
+ <div class="dropdown dropdown-compact" data-dropdown data-domain-dropdown>
+ <input type="hidden" value="{{ domains.Selected }}" data-dropdown-value data-domain-selected>
+ <button type="button" data-dropdown-trigger class="flex items-center gap-1.5 px-2 py-0.5 rounded bg-white/[0.06] border border-white/[0.06] text-[10px] text-zinc-400 transition-colors duration-150 hover:border-white/[0.1]">
+ <span class="truncate max-w-[120px]" data-dropdown-label>{{ domains.Selected }}</span>
+ <svg class="w-2.5 h-2.5 text-zinc-600 shrink-0 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
+ <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
+ </svg>
+ </button>
+ <div class="dropdown-menu" data-dropdown-menu>
+ <div class="dropdown-options" data-dropdown-options>
+ {% for domain in domains.Domains %}
+ <button type="button" class="dropdown-option" data-dropdown-option data-value="{{ domain.Name }}" data-label="{{ domain.Name }}">
+ <p>{{ domain.Name }}</p>
+ </button>
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ {% if domains.Domains %}
+ <div class="flex-1 flex flex-col justify-center">
+ {% for domain in domains.Domains %}
+ <div class="px-5 py-3 {% if domain.Name != domains.Selected %}hidden{% endif %}" data-domain-graph="{{ domain.Name }}">
+ <div class="flex items-center gap-2 mb-3">
+ <div class="w-full h-2 rounded-full bg-white/[0.04] overflow-hidden flex">
+ <div class="h-full bg-emerald-500/70 rounded-l-full status-bar-segment" style="width: {{ domain.PercentOk }}%"></div>
+ <div class="h-full bg-amber-500/70 status-bar-segment" style="width: {{ domain.PercentWarn }}%"></div>
+ <div class="h-full bg-red-500/70 rounded-r-full status-bar-segment" style="width: {{ domain.PercentErr }}%"></div>
+ </div>
+ </div>
+ <div class="grid grid-cols-3 gap-4 text-center">
+ <div>
+ <span class="inline-flex items-center gap-1 text-[10px] mb-1">
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500/70"></span>
+ <span class="text-zinc-500">2xx</span>
+ <span class="text-zinc-300 font-mono tabular-nums">{{ domain.PercentOk }}%</span>
+ </span>
+ <p class="text-sm text-emerald-400 font-mono tabular-nums">{{ domain.StatusOk }}</p>
+ </div>
+ <div>
+ <span class="inline-flex items-center gap-1 text-[10px] mb-1">
+ <span class="w-1.5 h-1.5 rounded-full bg-amber-500/70"></span>
+ <span class="text-zinc-500">3xx</span>
+ <span class="text-zinc-300 font-mono tabular-nums">{{ domain.PercentWarn }}%</span>
+ </span>
+ <p class="text-sm text-amber-400 font-mono tabular-nums">{{ domain.StatusWarn }}</p>
+ </div>
+ <div>
+ <span class="inline-flex items-center gap-1 text-[10px] mb-1">
+ <span class="w-1.5 h-1.5 rounded-full bg-red-500/70"></span>
+ <span class="text-zinc-500">4xx+</span>
+ <span class="text-zinc-300 font-mono tabular-nums">{{ domain.PercentErr }}%</span>
+ </span>
+ <p class="text-sm text-red-400 font-mono tabular-nums">{{ domain.StatusErr }}</p>
+ </div>
+ </div>
</div>
- <span class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Mailboxes</span>
+ {% endfor %}
+ </div>
+ {% else %}
+ <div class="flex-1 flex items-center justify-center">
+ <p class="text-xs text-zinc-600">No domains registered</p>
+ </div>
+ {% endif %}
+ </div>
+
+ </div>
+
+ <div class="grid grid-cols-3 gap-4">
+
+ <div class="glass rounded-xl glow-border overflow-hidden">
+ <div class="px-5 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">Reverse Proxy</h3>
+ </div>
+ <div class="divide-y divide-white/[0.04]">
+ {% for service in reverse_proxy.Services %}
+ <div class="flex items-center justify-between px-5 py-2.5">
+ <div class="flex items-center gap-3">
+ {% if service.Up %}
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)] pulse-dot"></span>
+ {% else %}
+ <span class="w-1.5 h-1.5 rounded-full bg-red-400 shadow-[0_0_6px_rgba(248,113,113,0.4)] pulse-dot"></span>
+ {% endif %}
+ <span class="text-xs {% if service.Up %}text-zinc-300{% else %}text-red-400/70{% endif %}">{{ service.Name }}</span>
+ </div>
+ <span class="text-xs text-zinc-600 font-mono">{{ service.Target }}</span>
+ </div>
+ {% endfor %}
</div>
- <p class="text-3xl font-bold text-zinc-100 tracking-tight">{{ MailboxCount }}</p>
- <p class="mt-1 text-xs text-zinc-600">Active inboxes</p>
</div>
- <div class="glass rounded-xl p-5 glow-border">
- <div class="flex items-center gap-3 mb-4">
- <div class="flex items-center justify-center w-9 h-9 rounded-lg bg-accent-500/10">
- <svg class="w-4.5 h-4.5 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
- </svg>
+ <div class="glass rounded-xl glow-border overflow-hidden">
+ <div class="flex items-center justify-between px-5 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">Object Storage</h3>
+ <span class="text-[10px] text-zinc-600 font-mono">{{ storage.TotalUsage }}</span>
+ </div>
+ <div class="divide-y divide-white/[0.04]">
+ {% for bucket in storage.Buckets %}
+ <div class="px-5 py-2.5">
+ <div class="flex items-center justify-between mb-1.5">
+ <span class="text-xs text-zinc-300">{{ bucket.Name }}</span>
+ <span class="text-[10px] text-zinc-600 font-mono">{{ bucket.UsageLabel }}</span>
+ </div>
+ <div class="w-full h-1 rounded-full bg-white/[0.04]">
+ <div class="h-full rounded-full bg-cyan-500/60 animate-bar" style="width: {{ bucket.Percentage }}%"></div>
+ </div>
</div>
- <span class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Emails</span>
+ {% endfor %}
</div>
- <p class="text-3xl font-bold text-zinc-100 tracking-tight">{{ EmailCount }}</p>
- <p class="mt-1 text-xs text-zinc-600">Total received</p>
</div>
- <div class="glass rounded-xl p-5 glow-border">
- <div class="flex items-center gap-3 mb-4">
- <div class="flex items-center justify-center w-9 h-9 rounded-lg bg-emerald-500/10">
- <svg class="w-4.5 h-4.5 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M9.348 14.652a3.75 3.75 0 0 1 0-5.304m5.304 0a3.75 3.75 0 0 1 0 5.304m-7.425 2.121a6.75 6.75 0 0 1 0-9.546m9.546 0a6.75 6.75 0 0 1 0 9.546M5.106 18.894c-3.808-3.807-3.808-9.98 0-13.788m13.788 0c3.808 3.807 3.808 9.98 0 13.788M12 12h.008v.008H12V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
- </svg>
+ <div class="glass rounded-xl glow-border overflow-hidden">
+ <div class="flex items-center justify-between px-5 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">Cron Jobs</h3>
+ <span class="text-[10px] text-zinc-600">{{ cron_jobs.FailCount }} failed</span>
+ </div>
+ {% if cron_jobs.Failures %}
+ <div class="divide-y divide-white/[0.04]">
+ {% for failure in cron_jobs.Failures %}
+ <div class="px-5 py-2.5">
+ <div class="flex items-center justify-between mb-0.5">
+ <span class="text-xs text-zinc-300">{{ failure.JobName }}</span>
+ <span class="text-[10px] text-zinc-600">{{ failure.FailedAt }}</span>
+ </div>
+ <p class="text-[10px] text-red-400/70 truncate">{{ failure.ErrorText }}</p>
</div>
- <span class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Server</span>
+ {% endfor %}
+ </div>
+ {% else %}
+ <div class="flex items-center justify-center py-8">
+ <p class="text-xs text-emerald-400/60">All jobs running successfully</p>
</div>
- <p class="text-3xl font-bold text-emerald-400 tracking-tight">Online</p>
- <p class="mt-1 text-xs text-zinc-600">SMTP listening on {{ SMTPAddress }}</p>
+ {% endif %}
</div>
+
</div>
- <div class="glass rounded-xl glow-border">
- <div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
- <h2 class="text-sm font-medium text-zinc-200">Recent Emails</h2>
+ <div class="grid grid-cols-[3fr_1fr] gap-4">
+
+ <div class="glass rounded-xl glow-border overflow-hidden">
+ <div class="flex items-center justify-between px-5 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">Dove</h3>
+ <div class="flex items-center gap-4 text-[10px]">
+ {% for group, count in dove.StatusGroups %}
+ <span class="{% if group == "2xx" %}text-emerald-400{% elif group == "3xx" %}text-amber-400{% elif group == "4xx" %}text-orange-400{% else %}text-red-400{% endif %} font-mono tabular-nums">{{ group }}: {{ count }}</span>
+ {% endfor %}
+ </div>
+ </div>
+ <div class="dove-scroll">
+ <table class="w-full text-xs">
+ <thead class="sticky top-0 bg-surface-900/95 backdrop-blur-sm">
+ <tr class="border-b border-white/[0.04]">
+ <th class="text-left text-zinc-600 font-medium px-5 py-2">Method</th>
+ <th class="text-left text-zinc-600 font-medium px-5 py-2">Route</th>
+ <th class="text-left text-zinc-600 font-medium px-5 py-2">Status</th>
+ <th class="text-right text-zinc-600 font-medium px-5 py-2">Hits</th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-white/[0.04]">
+ {% for entry in dove.RecentRoutes %}
+ <tr>
+ <td class="px-5 py-1.5">
+ <span class="inline-flex px-1.5 py-0.5 rounded text-[10px] font-medium
+ {% if entry.Method == "GET" %}bg-blue-500/10 text-blue-400
+ {% elif entry.Method == "POST" %}bg-emerald-500/10 text-emerald-400
+ {% elif entry.Method == "PUT" %}bg-amber-500/10 text-amber-400
+ {% elif entry.Method == "DELETE" %}bg-red-500/10 text-red-400
+ {% else %}bg-zinc-500/10 text-zinc-400{% endif %}">{{ entry.Method }}</span>
+ </td>
+ <td class="px-5 py-1.5 text-zinc-400 font-mono">{{ entry.Route }}</td>
+ <td class="px-5 py-1.5">
+ <span class="font-mono tabular-nums
+ {% if entry.StatusCode >= 200 and entry.StatusCode < 300 %}text-emerald-400
+ {% elif entry.StatusCode >= 300 and entry.StatusCode < 400 %}text-amber-400
+ {% elif entry.StatusCode >= 400 and entry.StatusCode < 500 %}text-orange-400
+ {% else %}text-red-400{% endif %}">{{ entry.StatusCode }}</span>
+ </td>
+ <td class="px-5 py-1.5 text-right text-zinc-500 font-mono tabular-nums">{{ entry.Count }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
</div>
- <div class="flex flex-col items-center justify-center py-16 text-center">
- <div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-surface-800 mb-4">
- <svg class="w-6 h-6 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
- </svg>
+
+ <div class="glass rounded-xl glow-border overflow-hidden flex flex-col">
+ <div class="flex items-center justify-between px-4 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">System DNS</h3>
+ </div>
+ <div class="flex flex-col items-center justify-center flex-1 py-6 gap-3 px-4">
+ {% if dns.Configured %}
+ <div class="flex items-center gap-2">
+ <span class="w-2 h-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.5)] pulse-dot"></span>
+ <span class="text-xs text-emerald-400 font-medium">Configured</span>
+ </div>
+ <p class="text-[10px] text-zinc-500 text-center leading-relaxed">Resolving through Dove on {{ dns.Platform }}.</p>
+ <span class="text-[10px] text-zinc-600 font-mono">{{ dns.Address }}</span>
+ <form method="POST" action="/dashboard/dns/disable">
+ <button type="submit" class="mt-1 px-3 py-1 rounded-md text-[11px] font-medium text-red-400 bg-red-500/10 border border-red-500/20 transition-all duration-150 hover:bg-red-500/20 hover:border-red-500/30 cursor-pointer">Disable</button>
+ </form>
+ {% else %}
+ <div class="flex items-center gap-2">
+ <span class="w-2 h-2 rounded-full bg-zinc-600"></span>
+ <span class="text-xs text-zinc-400 font-medium">Not configured</span>
+ </div>
+ <p class="text-[10px] text-zinc-500 text-center leading-relaxed">Dove's DNS is not set as a system nameserver.</p>
+ <span class="text-[10px] text-zinc-600 font-mono">{{ dns.Address }}</span>
+ <form method="POST" action="/dashboard/dns/configure">
+ <button type="submit" class="mt-1 px-3 py-1 rounded-md text-[11px] font-medium text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 transition-all duration-150 hover:bg-emerald-500/20 hover:border-emerald-500/30 cursor-pointer">Configure</button>
+ </form>
+ {% endif %}
</div>
- <p class="text-sm text-zinc-400">No emails yet</p>
- <p class="mt-1 text-xs text-zinc-600">Send an email to any address and it will appear here</p>
</div>
+
+ </div>
+
+ <div class="glass rounded-xl glow-border overflow-hidden">
+ <div class="px-5 py-3 border-b border-white/[0.04]">
+ <h3 class="text-xs font-medium text-zinc-400">Listeners</h3>
+ </div>
+ <table class="w-full text-xs">
+ <thead>
+ <tr class="border-b border-white/[0.04]">
+ <th class="text-left text-zinc-500 font-medium px-5 py-2.5">Name</th>
+ <th class="text-left text-zinc-500 font-medium px-5 py-2.5">Address</th>
+ <th class="text-left text-zinc-500 font-medium px-5 py-2.5">Protocol</th>
+ <th class="text-right text-zinc-500 font-medium px-5 py-2.5">Status</th>
+ </tr>
+ </thead>
+ <tbody class="divide-y divide-white/[0.04]">
+ {% for service in services %}
+ <tr>
+ <td class="px-5 py-2 text-zinc-300 font-medium font-mono">{{ service.Name }}</td>
+ <td class="px-5 py-2 text-zinc-500 font-mono">{{ bind_address }}:{{ service.Port }}</td>
+ <td class="px-5 py-2 text-zinc-500">{{ service.Protocol }}</td>
+ <td class="px-5 py-2 text-right">
+ {% if service.Active %}
+ <span class="inline-flex items-center gap-1.5">
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)] pulse-dot"></span>
+ <span class="text-emerald-400">Running</span>
+ </span>
+ {% else %}
+ <span class="inline-flex items-center gap-1.5">
+ <span class="w-1.5 h-1.5 rounded-full bg-red-400/50 pulse-dot"></span>
+ <span class="text-red-400/50">Inactive</span>
+ </span>
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
</div>
-</div> \ No newline at end of file
+
+</div>
+
+<script>
+(function() {
+ var emailLine = document.querySelector('[data-email-line]');
+ var emailFill = document.querySelector('[data-email-fill]');
+
+ if (emailLine) {
+ var lineLength = emailLine.getTotalLength();
+ emailLine.style.strokeDasharray = lineLength;
+ emailLine.style.strokeDashoffset = lineLength;
+ emailLine.style.setProperty('--line-length', lineLength);
+ emailLine.classList.add('sparkline-draw');
+
+ emailLine.addEventListener('animationend', function() {
+ emailLine.style.strokeDasharray = '';
+ emailLine.style.strokeDashoffset = '';
+ emailLine.classList.remove('sparkline-draw');
+ }, { once: true });
+ }
+
+ var emailData = {
+ '24h': { line: '{{ email.Sparkline24h.LinePath }}', fill: '{{ email.Sparkline24h.FillPath }}' },
+ '7d': { line: '{{ email.Sparkline7d.LinePath }}', fill: '{{ email.Sparkline7d.FillPath }}' },
+ '30d': { line: '{{ email.Sparkline30d.LinePath }}', fill: '{{ email.Sparkline30d.FillPath }}' }
+ };
+
+ var emailButtons = document.querySelectorAll('[data-email-range]');
+ var animating = false;
+
+ function parsePoints(pointString) {
+ return pointString.split(' ').map(function(pair) {
+ var coordinates = pair.split(',');
+ return { x: parseFloat(coordinates[0]), y: parseFloat(coordinates[1]) };
+ });
+ }
+
+ function interpolatePoints(fromPoints, toPoints, progress) {
+ var result = [];
+ var length = fromPoints.length;
+ for (var index = 0; index < length; index++) {
+ var interpolatedX = fromPoints[index].x + (toPoints[index].x - fromPoints[index].x) * progress;
+ var interpolatedY = fromPoints[index].y + (toPoints[index].y - fromPoints[index].y) * progress;
+ result.push(interpolatedX.toFixed(1) + ',' + interpolatedY.toFixed(1));
+ }
+ return result.join(' ');
+ }
+
+ function buildFillPath(pointString) {
+ return 'M2.0,40.0 L' + pointString + ' L198.0,40.0 Z';
+ }
+
+ function animateSparkline(targetRange, duration) {
+ if (animating) return;
+ animating = true;
+
+ var currentLinePoints = parsePoints(emailLine.getAttribute('points'));
+ var targetLinePoints = parsePoints(emailData[targetRange].line);
+ var startTime = null;
+
+ function step(timestamp) {
+ if (!startTime) startTime = timestamp;
+ var elapsed = timestamp - startTime;
+ var progress = Math.min(elapsed / duration, 1);
+ var eased = 1 - Math.pow(1 - progress, 3);
+
+ var interpolatedLine = interpolatePoints(currentLinePoints, targetLinePoints, eased);
+ emailLine.setAttribute('points', interpolatedLine);
+ emailFill.setAttribute('d', buildFillPath(interpolatedLine));
+
+ if (progress < 1) {
+ requestAnimationFrame(step);
+ } else {
+ emailLine.setAttribute('points', emailData[targetRange].line);
+ emailFill.setAttribute('d', emailData[targetRange].fill);
+ animating = false;
+ }
+ }
+
+ requestAnimationFrame(step);
+ }
+
+ emailButtons.forEach(function(button) {
+ button.addEventListener('click', function() {
+ var range = button.getAttribute('data-email-range');
+
+ emailButtons.forEach(function(otherButton) {
+ otherButton.classList.remove('bg-white/[0.06]', 'text-zinc-400');
+ otherButton.removeAttribute('data-email-active');
+ });
+ button.classList.add('bg-white/[0.06]', 'text-zinc-400');
+ button.setAttribute('data-email-active', '');
+
+ animateSparkline(range, 400);
+ });
+ });
+
+ var domainDropdown = document.querySelector('[data-domain-dropdown]');
+ if (domainDropdown) {
+ var domainHidden = domainDropdown.querySelector('[data-domain-selected]');
+ var domainOptions = domainDropdown.querySelectorAll('[data-dropdown-option]');
+
+ domainOptions.forEach(function(option) {
+ option.addEventListener('click', function() {
+ setTimeout(function() {
+ var selectedDomain = domainHidden.value;
+ var allGraphs = document.querySelectorAll('[data-domain-graph]');
+ var allStats = document.querySelectorAll('[data-domain-stats]');
+
+ allGraphs.forEach(function(graph) {
+ if (graph.getAttribute('data-domain-graph') === selectedDomain) {
+ graph.classList.remove('hidden');
+ } else {
+ graph.classList.add('hidden');
+ }
+ });
+
+ allStats.forEach(function(stats) {
+ if (stats.getAttribute('data-domain-stats') === selectedDomain) {
+ stats.classList.remove('hidden');
+ } else {
+ stats.classList.add('hidden');
+ }
+ });
+ }, 10);
+ });
+ });
+ }
+})();
+</script>
diff --git a/templates/domains/editdomain.django b/templates/domains/editdomain.django
deleted file mode 100644
index 7b10b37..0000000
--- a/templates/domains/editdomain.django
+++ /dev/null
@@ -1,5 +0,0 @@
-{% extends "layouts/dashboard.django" %}
-
-{% block dashboard %}
-{% include "domains/htmx/editdomain.htmx.django" %}
-{% endblock %}
diff --git a/templates/domains/htmx/detail.htmx.django b/templates/domains/htmx/detail.htmx.django
index eaa3806..05b2ccf 100644
--- a/templates/domains/htmx/detail.htmx.django
+++ b/templates/domains/htmx/detail.htmx.django
@@ -82,10 +82,6 @@
<label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Priority</label>
<input type="number" name="priority" value="10" min="0" class="input-field text-xs">
</div>
- <div data-port-map-field>
- <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Port Map</label>
- <input type="number" name="port_map" value="0" min="0" class="input-field text-xs">
- </div>
<div data-srv-weight style="display: none;">
<label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Weight</label>
<input type="number" name="weight" value="0" min="0" class="input-field text-xs">
@@ -121,19 +117,19 @@
</div>
{% if records %}
- <div class="divide-y divide-white/[0.04]">
- {% for record in records %}
- <div class="flex items-center justify-between px-5 py-3 group" data-record-row="{{ record.Type }}-{{ record.ID }}">
- <div class="flex items-center gap-3 min-w-0" data-record-display>
- {% if record.IsManaged %}
- <div class="flex items-center justify-center w-6 h-6 rounded bg-amber-500/10 shrink-0">
- <svg class="w-3.5 h-3.5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
- </svg>
- </div>
- {% endif %}
- <div class="flex items-center gap-2 min-w-0">
- <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0
+ <div>
+ <div class="grid px-5 py-2.5 border-b border-white/[0.06]" style="grid-template-columns: 4.5rem 1fr 2fr 5rem 6rem;">
+ <span class="text-[10px] font-medium text-zinc-600 uppercase tracking-wider">Type</span>
+ <span class="text-[10px] font-medium text-zinc-600 uppercase tracking-wider">Name</span>
+ <span class="text-[10px] font-medium text-zinc-600 uppercase tracking-wider">Content</span>
+ <span class="text-[10px] font-medium text-zinc-600 uppercase tracking-wider">TTL</span>
+ <span class="text-[10px] font-medium text-zinc-600 uppercase tracking-wider text-right">Actions</span>
+ </div>
+ <div class="divide-y divide-white/[0.04]" data-records-list>
+ {% for record in records %}
+ <div class="group" data-record-row="{{ record.Type }}-{{ record.ID }}">
+ <div class="grid items-center px-5 py-3" style="grid-template-columns: 4.5rem 1fr 2fr 5rem 6rem;">
+ <span class="inline-flex items-center w-fit px-1.5 py-0.5 rounded text-[10px] font-medium
{% if record.Type == "A" %}bg-blue-500/10 text-blue-400
{% elif record.Type == "AAAA" %}bg-blue-500/10 text-blue-300
{% elif record.Type == "CNAME" %}bg-purple-500/10 text-purple-400
@@ -141,27 +137,22 @@
{% elif record.Type == "TXT" %}bg-green-500/10 text-green-400
{% elif record.Type == "SRV" %}bg-pink-500/10 text-pink-400
{% endif %}">{{ record.Type }}</span>
- <p class="text-sm text-zinc-200 shrink-0">{{ record.Name }}</p>
- <svg class="w-3 h-3 text-zinc-700 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
- </svg>
- <p class="text-sm text-zinc-400 truncate">{{ record.Value }}</p>
- {% if record.Priority > 0 %}
- <span class="text-xs text-zinc-600 shrink-0">pri {{ record.Priority }}</span>
- {% endif %}
- <span class="text-xs text-zinc-700 shrink-0">{{ record.TTL }}s</span>
+ <p class="text-sm text-zinc-200 truncate pr-3">{{ record.Name }}</p>
+ <div class="flex items-center gap-2 min-w-0 pr-3">
+ <p class="text-sm text-zinc-400 truncate">{{ record.Value }}</p>
+ {% if record.Priority > 0 %}
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] bg-surface-800 text-zinc-500 shrink-0">pri {{ record.Priority }}</span>
+ {% endif %}
+ </div>
+ <span class="text-xs text-zinc-600">{{ record.TTL }}s</span>
+ <div class="flex items-center justify-end gap-3">
+ <button type="button" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150" data-edit-record data-record-type="{{ record.Type }}" data-record-id="{{ record.ID }}" data-record-name="{{ record.Name }}" data-record-value="{{ record.Value }}" data-record-ttl="{{ record.TTL }}" data-record-priority="{{ record.Priority }}">Edit</button>
+ <button data-confirm-trigger data-confirm-title="Delete {{ record.Type }} record" data-confirm-message="This DNS record will be permanently removed. This action cannot be undone." data-confirm-action="/domains/records/{{ record.Type }}/{{ record.ID }}?domain_id={{ domain.ID }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button>
+ </div>
</div>
</div>
- <div class="flex items-center gap-3 shrink-0 ml-4" data-record-actions>
- {% if record.IsManaged %}
- <span class="text-xs text-amber-500/60">System managed</span>
- {% else %}
- <button type="button" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150 opacity-0 group-hover:opacity-100" data-edit-record data-record-type="{{ record.Type }}" data-record-id="{{ record.ID }}" data-record-name="{{ record.Name }}" data-record-value="{{ record.Value }}" data-record-ttl="{{ record.TTL }}" data-record-priority="{{ record.Priority }}">Edit</button>
- <button data-confirm-trigger data-confirm-title="Delete {{ record.Type }} record" data-confirm-message="This DNS record will be permanently removed. This action cannot be undone." data-confirm-action="/domains/records/{{ record.Type }}/{{ record.ID }}?domain_id={{ domain.ID }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150 opacity-0 group-hover:opacity-100">Delete</button>
- {% endif %}
- </div>
+ {% endfor %}
</div>
- {% endfor %}
</div>
{% else %}
<div class="flex flex-col items-center justify-center py-16 text-center">
@@ -206,7 +197,6 @@
var typeDropdown = document.querySelector('[data-record-type-dropdown]');
var typeHiddenInput = typeDropdown.querySelector('[data-dropdown-value]');
var priorityField = document.querySelector('[data-priority-field]');
- var portMapField = document.querySelector('[data-port-map-field]');
var srvWeight = document.querySelector('[data-srv-weight]');
var srvPort = document.querySelector('[data-srv-port]');
var srvProtocol = document.querySelector('[data-srv-protocol]');
@@ -222,12 +212,12 @@
});
var fieldConfig = {
- 'A': { label: 'Address (IPv4)', placeholder: '127.0.0.1', priority: false, portMap: true, srv: false },
- 'AAAA': { label: 'Address (IPv6)', placeholder: '::1', priority: false, portMap: false, srv: false },
- 'CNAME': { label: 'Target', placeholder: 'other.example.dove.', priority: false, portMap: false, srv: false },
- 'MX': { label: 'Mail Server', placeholder: 'mail.example.dove.', priority: true, portMap: false, srv: false },
- 'TXT': { label: 'Content', placeholder: 'v=spf1 include:...', priority: false, portMap: false, srv: false },
- 'SRV': { label: 'Target Host', placeholder: 'service.example.dove.', priority: true, portMap: false, srv: true }
+ 'A': { label: 'Address (IPv4)', placeholder: '127.0.0.1', priority: false, srv: false },
+ 'AAAA': { label: 'Address (IPv6)', placeholder: '::1', priority: false, srv: false },
+ 'CNAME': { label: 'Target', placeholder: 'other.example.dove.', priority: false, srv: false },
+ 'MX': { label: 'Mail Server', placeholder: 'mail.example.dove.', priority: true, srv: false },
+ 'TXT': { label: 'Content', placeholder: 'v=spf1 include:...', priority: false, srv: false },
+ 'SRV': { label: 'Target Host', placeholder: 'service.example.dove.', priority: true, srv: true }
};
function updateFields() {
@@ -238,7 +228,6 @@
valueLabel.textContent = config.label;
valueInput.placeholder = config.placeholder;
priorityField.style.display = config.priority ? '' : 'none';
- portMapField.style.display = config.portMap ? '' : 'none';
srvWeight.style.display = config.srv ? '' : 'none';
srvPort.style.display = config.srv ? '' : 'none';
srvProtocol.style.display = config.srv ? '' : 'none';
@@ -253,73 +242,197 @@
updateFields();
- var ttlOptions = [['1', 'Immediate'], ['300', '5 min'], ['900', '15 min'], ['3600', '1 hour'], ['14400', '4 hours'], ['86400', '1 day']];
+ var ttlOptions = [
+ { value: '1', label: 'Immediate', desc: '1 second' },
+ { value: '300', label: '5 minutes', desc: '300 seconds' },
+ { value: '900', label: '15 minutes', desc: '900 seconds' },
+ { value: '3600', label: '1 hour', desc: '3600 seconds' },
+ { value: '14400', label: '4 hours', desc: '14400 seconds' },
+ { value: '86400', label: '1 day', desc: '86400 seconds' }
+ ];
+
+ var editFieldConfig = {
+ 'A': { label: 'Address (IPv4)', placeholder: '127.0.0.1', priority: false },
+ 'AAAA': { label: 'Address (IPv6)', placeholder: '::1', priority: false },
+ 'CNAME': { label: 'Target', placeholder: 'other.example.dove.', priority: false },
+ 'MX': { label: 'Mail Server', placeholder: 'mail.example.dove.', priority: true },
+ 'TXT': { label: 'Content', placeholder: 'v=spf1 include:...', priority: false },
+ 'SRV': { label: 'Target Host', placeholder: 'service.example.dove.', priority: true }
+ };
+
+ var chevronSvg = '<svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 ml-1 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /></svg>';
+
+ function buildTtlDropdown(currentTtl) {
+ var wrapper = document.createElement('div');
+ wrapper.className = 'dropdown';
+ wrapper.setAttribute('data-dropdown', '');
+
+ var currentLabel = 'Immediate';
+ for (var i = 0; i < ttlOptions.length; i++) {
+ if (ttlOptions[i].value === currentTtl) {
+ currentLabel = ttlOptions[i].label;
+ break;
+ }
+ }
+
+ var hidden = document.createElement('input');
+ hidden.type = 'hidden';
+ hidden.name = 'ttl';
+ hidden.value = currentTtl;
+ hidden.setAttribute('data-dropdown-value', '');
+ wrapper.appendChild(hidden);
+
+ var trigger = document.createElement('button');
+ trigger.type = 'button';
+ trigger.setAttribute('data-dropdown-trigger', '');
+ trigger.className = 'input-field text-xs text-left flex items-center justify-between';
+
+ var labelSpan = document.createElement('span');
+ labelSpan.className = 'truncate';
+ labelSpan.setAttribute('data-dropdown-label', '');
+ labelSpan.textContent = currentLabel;
+ trigger.appendChild(labelSpan);
+ trigger.insertAdjacentHTML('beforeend', chevronSvg);
+ wrapper.appendChild(trigger);
+
+ var menuDiv = document.createElement('div');
+ menuDiv.className = 'dropdown-menu';
+ menuDiv.setAttribute('data-dropdown-menu', '');
+
+ var optionsDiv = document.createElement('div');
+ optionsDiv.className = 'dropdown-options';
+ optionsDiv.setAttribute('data-dropdown-options', '');
+
+ for (var j = 0; j < ttlOptions.length; j++) {
+ var optBtn = document.createElement('button');
+ optBtn.type = 'button';
+ optBtn.className = 'dropdown-option';
+ optBtn.setAttribute('data-dropdown-option', '');
+ optBtn.setAttribute('data-value', ttlOptions[j].value);
+ optBtn.setAttribute('data-label', ttlOptions[j].label);
+
+ var optLabel = document.createElement('p');
+ optLabel.className = 'text-sm text-zinc-200';
+ optLabel.textContent = ttlOptions[j].label;
+ optBtn.appendChild(optLabel);
+
+ var optDesc = document.createElement('p');
+ optDesc.className = 'text-xs text-zinc-500';
+ optDesc.textContent = ttlOptions[j].desc;
+ optBtn.appendChild(optDesc);
+
+ optionsDiv.appendChild(optBtn);
+ }
+
+ menuDiv.appendChild(optionsDiv);
+ wrapper.appendChild(menuDiv);
+ return wrapper;
+ }
function createEditForm(recordType, recordId, recordName, recordValue, recordTtl, recordPriority) {
+ var config = editFieldConfig[recordType] || editFieldConfig['A'];
+
var form = document.createElement('form');
form.setAttribute('hx-put', '/domains/records/' + recordType + '/' + recordId + '?domain_id={{ domain.ID }}');
form.setAttribute('hx-swap', 'none');
form.setAttribute('data-inline-edit', '');
- form.className = 'flex items-center gap-3 w-full';
+ form.className = 'px-5 py-4 space-y-3';
- var typeBadge = document.createElement('span');
- typeBadge.className = 'inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 bg-surface-800 text-zinc-400';
+ var row1 = document.createElement('div');
+ row1.className = 'grid grid-cols-5 gap-3';
+
+ var typeCol = document.createElement('div');
+ var typeLabel = document.createElement('label');
+ typeLabel.className = 'block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5';
+ typeLabel.textContent = 'Type';
+ typeCol.appendChild(typeLabel);
+ var typeBadge = document.createElement('div');
+ typeBadge.className = 'input-field text-xs flex items-center opacity-60 cursor-not-allowed';
typeBadge.textContent = recordType;
- form.appendChild(typeBadge);
+ typeCol.appendChild(typeBadge);
+ row1.appendChild(typeCol);
+ var nameCol = document.createElement('div');
+ var nameLabel = document.createElement('label');
+ nameLabel.className = 'block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5';
+ nameLabel.textContent = 'Name';
+ nameCol.appendChild(nameLabel);
var nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.name = 'name';
nameInput.value = recordName;
- nameInput.className = 'input-field text-xs w-20';
+ nameInput.autocomplete = 'off';
nameInput.placeholder = '@';
- form.appendChild(nameInput);
-
- var valueInput = document.createElement('input');
- valueInput.type = 'text';
- valueInput.name = 'value';
- valueInput.value = recordValue;
- valueInput.required = true;
- valueInput.className = 'input-field text-xs flex-1';
- valueInput.placeholder = 'Value';
- form.appendChild(valueInput);
-
- var hasPriority = parseInt(recordPriority, 10) > 0;
+ nameInput.className = 'input-field text-xs';
+ nameCol.appendChild(nameInput);
+ row1.appendChild(nameCol);
+
+ var valueCol = document.createElement('div');
+ valueCol.className = 'col-span-2';
+ var valueLbl = document.createElement('label');
+ valueLbl.className = 'block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5';
+ valueLbl.textContent = config.label;
+ valueCol.appendChild(valueLbl);
+ var valInput = document.createElement('input');
+ valInput.type = 'text';
+ valInput.name = 'value';
+ valInput.value = recordValue;
+ valInput.required = true;
+ valInput.autocomplete = 'off';
+ valInput.placeholder = config.placeholder;
+ valInput.className = 'input-field text-xs';
+ valueCol.appendChild(valInput);
+ row1.appendChild(valueCol);
+
+ var ttlCol = document.createElement('div');
+ var ttlLabel = document.createElement('label');
+ ttlLabel.className = 'block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5';
+ ttlLabel.textContent = 'TTL';
+ ttlCol.appendChild(ttlLabel);
+ ttlCol.appendChild(buildTtlDropdown(recordTtl));
+ row1.appendChild(ttlCol);
+
+ form.appendChild(row1);
+
+ var hasPriority = config.priority && parseInt(recordPriority, 10) > 0;
if (hasPriority) {
- var priorityInput = document.createElement('input');
- priorityInput.type = 'number';
- priorityInput.name = 'priority';
- priorityInput.value = recordPriority;
- priorityInput.min = '0';
- priorityInput.className = 'input-field text-xs w-16';
- priorityInput.placeholder = 'Pri';
- form.appendChild(priorityInput);
- }
+ var row2 = document.createElement('div');
+ row2.className = 'grid grid-cols-5 gap-3';
- var ttlSelect = document.createElement('select');
- ttlSelect.name = 'ttl';
- ttlSelect.className = 'input-field text-xs w-24';
- for (var i = 0; i < ttlOptions.length; i++) {
- var opt = document.createElement('option');
- opt.value = ttlOptions[i][0];
- opt.textContent = ttlOptions[i][1];
- if (recordTtl === ttlOptions[i][0]) opt.selected = true;
- ttlSelect.appendChild(opt);
+ var priCol = document.createElement('div');
+ var priLabel = document.createElement('label');
+ priLabel.className = 'block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5';
+ priLabel.textContent = 'Priority';
+ priCol.appendChild(priLabel);
+ var priInput = document.createElement('input');
+ priInput.type = 'number';
+ priInput.name = 'priority';
+ priInput.value = recordPriority;
+ priInput.min = '0';
+ priInput.className = 'input-field text-xs';
+ priCol.appendChild(priInput);
+ row2.appendChild(priCol);
+
+ form.appendChild(row2);
}
- form.appendChild(ttlSelect);
+
+ var row3 = document.createElement('div');
+ row3.className = 'flex items-center gap-3 pt-1';
var saveButton = document.createElement('button');
saveButton.type = 'submit';
saveButton.className = 'btn-small';
- saveButton.textContent = 'Save';
- form.appendChild(saveButton);
+ saveButton.textContent = 'Save Record';
+ row3.appendChild(saveButton);
var cancelLink = document.createElement('a');
cancelLink.href = 'javascript:void(0)';
- cancelLink.className = 'text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150 shrink-0';
+ cancelLink.className = 'text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150';
cancelLink.textContent = 'Cancel';
cancelLink.setAttribute('data-cancel-edit', '');
- form.appendChild(cancelLink);
+ row3.appendChild(cancelLink);
+
+ form.appendChild(row3);
return form;
}
@@ -339,13 +452,18 @@
if (!row || row.querySelector('[data-inline-edit]')) return;
var originalContent = row.innerHTML;
+ var originalClass = row.className;
+ row.className = 'border-b border-white/[0.04]';
while (row.firstChild) row.removeChild(row.firstChild);
var form = createEditForm(recordType, recordId, recordName, recordValue, recordTtl, recordPriority);
row.appendChild(form);
+ initDropdowns();
+
form.querySelector('[data-cancel-edit]').addEventListener('click', function() {
+ row.className = originalClass;
row.innerHTML = originalContent;
htmx.process(row);
});
@@ -353,7 +471,7 @@
htmx.process(row);
}
- var recordsList = document.querySelector('.divide-y');
+ var recordsList = document.querySelector('[data-records-list]');
if (recordsList) {
recordsList.addEventListener('click', handleEditClick);
}
diff --git a/templates/domains/htmx/editdomain.htmx.django b/templates/domains/htmx/editdomain.htmx.django
deleted file mode 100644
index 7969999..0000000
--- a/templates/domains/htmx/editdomain.htmx.django
+++ /dev/null
@@ -1,48 +0,0 @@
-<div class="slide-up flex items-start justify-center pt-12">
- <div class="glass rounded-xl glow-border w-full max-w-lg">
- <div class="px-5 py-4 border-b border-white/[0.04]">
- <h2 class="text-sm font-medium text-zinc-200">Edit Domain</h2>
- </div>
- <div class="p-5">
- {% url "domains.manage.update" id=domain.ID as update_path %}
- <form hx-put="{{ update_path }}" hx-swap="none" class="space-y-4">
- <div>
- <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">Domain Name</label>
- <input type="text" name="name" value="{{ domain.Name }}" required autocomplete="off" class="input-field">
- </div>
- <div>
- <label class="block text-xs font-medium text-zinc-400 mb-1.5 ml-1">TLD</label>
- <div class="dropdown" data-dropdown>
- <input type="hidden" name="tld_name" value="{{ domain.TLD.Name }}" data-dropdown-value>
- <button type="button" data-dropdown-trigger class="input-field text-left flex items-center justify-between">
- <span class="truncate" data-dropdown-label>.{{ domain.TLD.Name }}</span>
- <svg class="w-4 h-4 text-zinc-500 shrink-0 ml-2 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
- </svg>
- </button>
- <div class="dropdown-menu" data-dropdown-menu>
- <div class="p-2 border-b border-white/[0.04]">
- <input type="text" placeholder="Search TLDs..." class="dropdown-search" data-dropdown-search>
- </div>
- <div class="dropdown-options" data-dropdown-options>
- {% for tld in tlds %}
- <button type="button" class="dropdown-option" data-dropdown-option data-value="{{ tld.Name }}" data-label=".{{ tld.Name }}">
- <p class="text-sm text-zinc-200">.{{ tld.Name }}</p>
- </button>
- {% endfor %}
- </div>
- <div class="dropdown-empty hidden" data-dropdown-empty>
- <p class="text-xs text-zinc-500 text-center py-3">No TLDs found</p>
- </div>
- </div>
- </div>
- </div>
- <div class="flex items-center gap-3 pt-2">
- <button type="submit" class="btn-primary">Save Changes</button>
- {% url "domains.manage" as domains_path %}
- <a href="{{ domains_path }}" hx-get="{{ domains_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150">Cancel</a>
- </div>
- </form>
- </div>
- </div>
-</div> \ No newline at end of file
diff --git a/templates/domains/htmx/index.htmx.django b/templates/domains/htmx/index.htmx.django
index 3b2ef96..62272a5 100644
--- a/templates/domains/htmx/index.htmx.django
+++ b/templates/domains/htmx/index.htmx.django
@@ -4,7 +4,7 @@
<h2 class="text-sm font-medium text-zinc-200">Domain Manager</h2>
</div>
<div class="px-5 py-4">
- <p class="text-xs text-zinc-500 leading-relaxed">Dove runs a local DNS server that resolves your registered TLDs and domains. Each domain gets DNS records (A, AAAA, CNAME, MX, TXT, SRV) with port mapping support, so you can route <span class="text-zinc-400">myapp.dove</span> to <span class="text-zinc-400">127.0.0.1:3000</span>. Queries for unregistered domains are forwarded to your system's upstream DNS, so your regular internet browsing is not disrupted. Point your system's DNS resolver at Dove's DNS address to start resolving local domains.</p>
+ <p class="text-xs text-zinc-500 leading-relaxed">Dove runs a local DNS server that resolves your registered TLDs and domains. Each domain gets DNS records (A, AAAA, CNAME, MX, TXT, SRV) that resolve locally. Proxy rules let you route domains like <span class="text-zinc-400">myapp.dove</span> to local services on any port. Queries for unregistered domains are forwarded to your system's upstream DNS, so your regular internet browsing is not disrupted. Point your system's DNS resolver at <span class="text-zinc-400">127.0.0.1</span> to start resolving local domains.</p>
</div>
</div>
@@ -35,19 +35,31 @@
<p class="text-xs text-zinc-500 leading-relaxed">Register domains under your TLDs. Each domain resolves locally and can be used for mail, service routing, and DNS records.</p>
</a>
+ <a href="{{ manage_path }}" hx-get="{{ manage_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="glass rounded-xl p-5 glow-border hover:bg-white/[0.02] transition-colors duration-150 group">
+ <div class="flex items-center gap-3 mb-3">
+ <div class="flex items-center justify-center w-9 h-9 rounded-lg bg-accent-500/10">
+ <svg class="w-4.5 h-4.5 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" />
+ </svg>
+ </div>
+ <h3 class="text-sm font-medium text-zinc-200 group-hover:text-zinc-100">DNS Records</h3>
+ </div>
+ <p class="text-xs text-zinc-500 leading-relaxed">Manage A, AAAA, CNAME, MX, TXT, and SRV records per domain. Default records are seeded automatically when a domain is created. Open any domain to manage its records.</p>
+ </a>
+
<div class="glass rounded-xl p-5 glow-border opacity-60">
<div class="flex items-center gap-3 mb-3">
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-zinc-500/10">
<svg class="w-4.5 h-4.5 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.625 4.5h12.75a1.875 1.875 0 0 1 0 3.75H5.625a1.875 1.875 0 0 1 0-3.75Z" />
+ <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
</svg>
</div>
<div class="flex items-center gap-2">
- <h3 class="text-sm font-medium text-zinc-400">DNS Records</h3>
+ <h3 class="text-sm font-medium text-zinc-400">Proxy Rules</h3>
<span class="text-[10px] text-zinc-600 bg-surface-800 px-1.5 py-0.5 rounded">Coming soon</span>
</div>
</div>
- <p class="text-xs text-zinc-600 leading-relaxed">Manage A, AAAA, CNAME, MX, TXT, and SRV records per domain. A records support port mapping for routing domains to local services. Wildcard subdomain records for multi-tenant apps.</p>
+ <p class="text-xs text-zinc-600 leading-relaxed">Route traffic from your domains to local services. Pick any domain, subdomain, or wildcard pattern and forward HTTP requests to a port on your machine.</p>
</div>
<div class="glass rounded-xl p-5 glow-border opacity-60">
diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django
index 01dd602..7568241 100644
--- a/templates/partials/sidebar.django
+++ b/templates/partials/sidebar.django
@@ -50,6 +50,12 @@
</svg>
Domains
</a>
+ <a href="#" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150 opacity-50 pointer-events-none">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
+ </svg>
+ Proxy Rules
+ </a>
</div>
</div>
@@ -88,18 +94,14 @@
</div>
</nav>
+ {% if AuthEnabled %}
<div class="mt-auto p-3 border-t border-white/[0.04]">
- <div class="flex items-center gap-2 px-3 py-2 text-xs text-zinc-600">
- <span class="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
- SMTP listening
- </div>
- {% if AuthEnabled %}
- <a href="/auth/logout" class="flex items-center gap-3 px-3 py-2 rounded-lg text-xs text-zinc-500 hover:text-zinc-300 hover:bg-white/[0.04] transition-colors duration-150">
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <a href="/auth/logout" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150">
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
</svg>
Logout
</a>
- {% endif %}
</div>
+ {% endif %}
</aside> \ No newline at end of file