413 lines
11 KiB
Text
413 lines
11 KiB
Text
package templates
|
|
|
|
import "fmt"
|
|
import "strings"
|
|
import "sort"
|
|
|
|
type IndexProps struct {
|
|
Title string
|
|
IsConfigured bool
|
|
CurrentIP string
|
|
Config ConfigData
|
|
Records []DNSRecord
|
|
UpdateFreqs []UpdateFrequency
|
|
}
|
|
|
|
type ConfigData struct {
|
|
ZoneID string
|
|
Domain string
|
|
UpdatePeriod string
|
|
ApiToken string
|
|
}
|
|
|
|
type DNSRecord struct {
|
|
ID string
|
|
Type string
|
|
Name string
|
|
Content string
|
|
TTL int
|
|
Proxied bool
|
|
IsStatic bool
|
|
CreatedOn string
|
|
}
|
|
|
|
type UpdateFrequency struct {
|
|
Label string
|
|
Value string
|
|
}
|
|
|
|
// Helper type for grouped records
|
|
type IPGroup struct {
|
|
IP string
|
|
Records []DNSRecord
|
|
}
|
|
|
|
// Helper function to group records by IP
|
|
func groupRecordsByIP(records []DNSRecord) []IPGroup {
|
|
groupMap := make(map[string][]DNSRecord)
|
|
for _, r := range records {
|
|
groupMap[r.Content] = append(groupMap[r.Content], r)
|
|
}
|
|
|
|
// Stable order of groups
|
|
ips := make([]string, 0, len(groupMap))
|
|
for ip := range groupMap {
|
|
ips = append(ips, ip)
|
|
}
|
|
sort.Strings(ips)
|
|
|
|
// Convert to slice of IPGroup
|
|
result := make([]IPGroup, 0, len(ips))
|
|
for _, ip := range ips {
|
|
result = append(result, IPGroup{
|
|
IP: ip,
|
|
Records: groupMap[ip],
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
templ Index(props IndexProps) {
|
|
@Layout(props.Title) {
|
|
<div class="container">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-10">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h1>{ props.Title }</h1>
|
|
<div class="current-ip d-flex align-items-center">
|
|
<span class="me-2">Current IP:</span>
|
|
<span id="current-ip" x-init class="fw-bold">{ props.CurrentIP }</span>
|
|
<a
|
|
href="/refresh-ip"
|
|
x-target="current-ip"
|
|
class="btn btn-sm btn-outline-secondary ms-2 d-inline-flex align-items-center"
|
|
@ajax:before="$el.querySelector('.bi-arrow-clockwise').classList.add('spin')"
|
|
@ajax:after="$el.querySelector('.bi-arrow-clockwise').classList.remove('spin')"
|
|
>
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
if !props.IsConfigured {
|
|
@ConfigWarning()
|
|
} else {
|
|
@ConfigStatus(props.Config)
|
|
@DNSRecordsSection(props.Records, props.CurrentIP)
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
templ ConfigWarning() {
|
|
<div class="alert alert-warning config-warning">
|
|
<h4>Configuration Required</h4>
|
|
<p>Please configure your Cloudflare API credentials to manage your DNS records.</p>
|
|
<a href="/config" x-target="contact" @ajax:success="$dispatch('dialog:open')" class="btn btn-primary">
|
|
Configure Now
|
|
</a>
|
|
</div>
|
|
}
|
|
|
|
templ ConfigStatus(config ConfigData) {
|
|
<div class="card mb-4" id="config-status">
|
|
<div
|
|
class="card-header d-flex justify-content-between align-items-center"
|
|
x-init
|
|
x-data="{ editLoading: false, deleteLoading: false }"
|
|
@ajax:success="$dispatch('dialog:open')"
|
|
>
|
|
<h5 class="mb-0">Configuration</h5>
|
|
<a
|
|
href="/config"
|
|
@ajax:before="editLoading = true"
|
|
@ajax:success="$dispatch('dialog:open')"
|
|
@ajax:after="editLoading = false"
|
|
@ajax:error="editLoading = false"
|
|
x-target="contact"
|
|
class="btn btn-sm btn-outline-primary me-2"
|
|
:disabled="editLoading || deleteLoading"
|
|
>
|
|
<template x-if="!editLoading">
|
|
<span class="ms-1">
|
|
Edit
|
|
<i class="bi bi-pencil"></i>
|
|
</span>
|
|
</template>
|
|
<template x-if="editLoading">
|
|
<span class="spinner-border spinner-border-sm"></span>
|
|
</template>
|
|
</a>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<strong>Domain:</strong> <span>{ config.Domain }</span>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<strong>Zone ID:</strong> <span>{ config.ZoneID }</span>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<strong>IP Update Schedule:</strong>
|
|
<span>{ formatUpdateSchedule(config.UpdatePeriod) }</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ DNSRecordsSection(records []DNSRecord, currentIP string) {
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">DNS Records</h5>
|
|
<div x-data="{ updating: false, addingRecord: false }">
|
|
<form
|
|
method="post"
|
|
action="/update-all-records"
|
|
x-target="dns-records-table"
|
|
@ajax:before="confirm('Are you sure you want to update all non-static A records to your current IP?') || $event.preventDefault(); updating = true"
|
|
@ajax:after="updating = false"
|
|
@ajax:error="updating = false"
|
|
style="display: inline;"
|
|
>
|
|
<button
|
|
class="btn btn-sm btn-success me-2"
|
|
type="submit"
|
|
:disabled="updating || addingRecord"
|
|
>
|
|
<template x-if="!updating">
|
|
<span class="d-flex align-items-center">
|
|
<i class="bi bi-arrow-repeat me-1"></i>
|
|
Update All to Current IP
|
|
</span>
|
|
</template>
|
|
<template x-if="updating">
|
|
<span class="d-flex align-items-center">
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
Updating All Records...
|
|
</span>
|
|
</template>
|
|
</button>
|
|
</form>
|
|
<a
|
|
href="/records/new"
|
|
x-target="contact"
|
|
@ajax:before="addingRecord = true; $dispatch('dialog:open')"
|
|
@ajax:after="addingRecord = false"
|
|
@ajax:error="addingRecord = false"
|
|
class="btn btn-primary"
|
|
:disabled="updating || addingRecord"
|
|
>
|
|
<template x-if="!addingRecord">
|
|
<span class="d-flex align-items-center">
|
|
<i class="bi bi-plus-lg me-1"></i>
|
|
Add Record
|
|
</span>
|
|
</template>
|
|
<template x-if="addingRecord">
|
|
<span class="d-flex align-items-center">
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
Loading...
|
|
</span>
|
|
</template>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
@DNSRecordsTable(records, currentIP)
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ DNSRecordsTable(records []DNSRecord, currentIP string) {
|
|
<div id="dns-records-table" class="table-responsive">
|
|
<table class="table table-striped table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Type</th>
|
|
<th>Name</th>
|
|
<th>Content</th>
|
|
<th>TTL</th>
|
|
<th>Proxied</th>
|
|
<th>Static</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
if len(records) == 0 {
|
|
<tr>
|
|
<td colspan="7" class="text-center">No DNS records found</td>
|
|
</tr>
|
|
} else {
|
|
for _, ipGroup := range groupRecordsByIP(records) {
|
|
<tr class="table-secondary">
|
|
<td colspan="7">
|
|
<strong>{ ipGroup.IP }</strong>
|
|
<span class="ms-2 text-muted">
|
|
{ fmt.Sprintf("%d record(s)",
|
|
len(ipGroup.Records)) }
|
|
</span>
|
|
if ipGroup.IP == currentIP {
|
|
<span class="badge bg-success ms-2">Current IP</span>
|
|
}
|
|
</td>
|
|
</tr>
|
|
for _, record := range ipGroup.Records {
|
|
@DNSRecordRow(record, currentIP)
|
|
}
|
|
}
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
templ DNSRecordRow(record DNSRecord, currentIP string) {
|
|
<tr x-data="{ editLoading: false, deleteLoading: false, toggleLoading: false }">
|
|
<td>{ record.Type }</td>
|
|
<td>{ record.Name }</td>
|
|
<td>
|
|
{ record.Content }
|
|
if record.Type == "A" && !record.IsStatic {
|
|
if record.Content == currentIP {
|
|
<span class="badge bg-success update-badge">Current IP</span>
|
|
} else {
|
|
<span class="badge bg-warning update-badge">Outdated IP</span>
|
|
}
|
|
}
|
|
</td>
|
|
<td>
|
|
if record.TTL == 1 {
|
|
Auto
|
|
} else {
|
|
{ fmt.Sprintf("%ds", record.TTL) }
|
|
}
|
|
</td>
|
|
<td>
|
|
if record.Proxied {
|
|
<i class="bi bi-check-lg text-success"></i>
|
|
}
|
|
</td>
|
|
<td>
|
|
if record.Type == "A" {
|
|
<form
|
|
method="post"
|
|
action={ templ.URL(fmt.Sprintf("/records/%s/toggle-static", record.ID)) }
|
|
x-target="dns-records-table"
|
|
@ajax:before="toggleLoading = true"
|
|
@ajax:after="toggleLoading = false"
|
|
@ajax:error="toggleLoading = false"
|
|
style="display: inline;"
|
|
>
|
|
<button
|
|
type="submit"
|
|
class={ "btn btn-sm" , templ.KV("btn-outline-secondary",
|
|
!record.IsStatic), templ.KV("btn-secondary", record.IsStatic) }
|
|
:disabled="editLoading || deleteLoading || toggleLoading"
|
|
title={ getStaticToggleTitle(record.IsStatic) }
|
|
>
|
|
<template x-if="!toggleLoading">
|
|
if record.IsStatic {
|
|
<i class="bi bi-lock-fill"></i>
|
|
} else {
|
|
<i class="bi bi-unlock"></i>
|
|
}
|
|
</template>
|
|
<template x-if="toggleLoading">
|
|
<span class="spinner-border spinner-border-sm"></span>
|
|
</template>
|
|
</button>
|
|
</form>
|
|
} else {
|
|
<span class="text-muted">-</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<a
|
|
href={ templ.URL(fmt.Sprintf("/edit-record/%s", record.ID)) }
|
|
@ajax:before="editLoading = true"
|
|
@ajax:success="$dispatch('dialog:open')"
|
|
@ajax:after="editLoading = false"
|
|
@ajax:error="editLoading = false"
|
|
x-target="contact"
|
|
class="btn btn-sm btn-outline-primary me-2"
|
|
:disabled="editLoading || deleteLoading || toggleLoading"
|
|
>
|
|
<template x-if="!editLoading">
|
|
<i class="bi bi-pencil"></i>
|
|
</template>
|
|
<template x-if="editLoading">
|
|
<span class="spinner-border spinner-border-sm"></span>
|
|
</template>
|
|
</a>
|
|
<form
|
|
method="delete"
|
|
action={ templ.URL(fmt.Sprintf("/records/%s", record.ID)) }
|
|
x-target="closest tr"
|
|
@ajax:before={ fmt.Sprintf(`confirm('Are you sure you want to delete the record for "%s" ?') ||
|
|
$event.preventDefault(); deleteLoading=true`, record.Name) }
|
|
@ajax:after="deleteLoading = false"
|
|
@ajax:error="deleteLoading = false"
|
|
@ajax:success="$el.closest('tr').remove()"
|
|
style="display: inline;"
|
|
>
|
|
<button
|
|
class="btn btn-sm btn-outline-danger"
|
|
type="submit"
|
|
:disabled="editLoading || deleteLoading || toggleLoading"
|
|
>
|
|
<template x-if="!deleteLoading">
|
|
<i class="bi bi-trash"></i>
|
|
</template>
|
|
<template x-if="deleteLoading">
|
|
<span class="spinner-border spinner-border-sm"></span>
|
|
</template>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
}
|
|
|
|
// Helper function to format update schedule
|
|
func formatUpdateSchedule(period string) string {
|
|
switch period {
|
|
case "*/1 * * * *":
|
|
return "Every minute"
|
|
case "*/5 * * * *":
|
|
return "Every 5 minutes"
|
|
case "*/30 * * * *":
|
|
return "Every 30 minutes"
|
|
case "0 * * * *":
|
|
return "Hourly"
|
|
case "0 */6 * * *":
|
|
return "Every 6 hours"
|
|
case "0 0 * * *":
|
|
return "Daily"
|
|
case "":
|
|
return "Manual updates only"
|
|
default:
|
|
return period
|
|
}
|
|
}
|
|
|
|
// Helper function to extract subdomain name
|
|
func getRecordName(fullName, domain string) string {
|
|
if fullName == domain {
|
|
return "@"
|
|
}
|
|
if strings.HasSuffix(fullName, "."+domain) {
|
|
return fullName[:len(fullName)-len(domain)-1]
|
|
}
|
|
return fullName
|
|
}
|
|
|
|
// Helper function for static toggle title
|
|
func getStaticToggleTitle(isStatic bool) string {
|
|
if isStatic {
|
|
return "Make dynamic (will auto-update)"
|
|
}
|
|
return "Make static (won't auto-update)"
|
|
}
|
|
|