mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
webui: rebuild NTP & DNS client pages around per-item editing
Replace the single bulk-PUT form + client-side row cloning (which broke when these pages were split out of System settings) with the server-driven list pattern used by Keystore/Users: each server is a row keyed by name that expands to edit, with per-row delete and a dedicated add form. DNS search domains become add/delete rows. Server creates its container on first use, and field-info ⓘ help is sourced from the YANG descriptions. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -4,9 +4,11 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -42,9 +44,11 @@ type cfgNTPJSON struct {
|
||||
}
|
||||
|
||||
type cfgNTPServerJSON struct {
|
||||
Name string `json:"name"`
|
||||
UDP cfgNTPUDPJSON `json:"udp"`
|
||||
Prefer bool `json:"prefer"`
|
||||
Name string `json:"name"`
|
||||
UDP cfgNTPUDPJSON `json:"udp"`
|
||||
AssociationType string `json:"association-type,omitempty"`
|
||||
IBurst bool `json:"iburst,omitempty"`
|
||||
Prefer bool `json:"prefer,omitempty"`
|
||||
}
|
||||
|
||||
type cfgNTPUDPJSON struct {
|
||||
@@ -89,14 +93,17 @@ type cfgSystemPageData struct {
|
||||
|
||||
type cfgNTPPageData struct {
|
||||
PageData
|
||||
Error string
|
||||
NTP cfgNTPJSON
|
||||
Error string
|
||||
NTP cfgNTPJSON
|
||||
AssocTypes []string // association-type enum: server / pool / peer
|
||||
Desc map[string]string // leaf name → YANG description (field-info ⓘ)
|
||||
}
|
||||
|
||||
type cfgDNSPageData struct {
|
||||
PageData
|
||||
Error string
|
||||
DNS cfgDNSJSON
|
||||
Desc map[string]string // leaf name → YANG description (field-info ⓘ)
|
||||
}
|
||||
|
||||
// ─── Handler ─────────────────────────────────────────────────────────────────
|
||||
@@ -232,6 +239,18 @@ func (h *ConfigureSystemHandler) OverviewNTP(w http.ResponseWriter, r *http.Requ
|
||||
data := cfgNTPPageData{
|
||||
PageData: newPageData(w, r, "configure-ntp", "NTP Client"),
|
||||
}
|
||||
data.AssocTypes = []string{"server", "pool", "peer"}
|
||||
if mgr := h.Schema.Manager(); mgr != nil {
|
||||
srv := "/ietf-system:system/ntp/server"
|
||||
data.Desc = map[string]string{
|
||||
"name": schema.DescriptionOf(mgr, srv+"/name"),
|
||||
"address": schema.DescriptionOf(mgr, srv+"/udp/address"),
|
||||
"port": schema.DescriptionOf(mgr, srv+"/udp/port"),
|
||||
"assoc": schema.DescriptionOf(mgr, srv+"/association-type"),
|
||||
"iburst": schema.DescriptionOf(mgr, srv+"/iburst"),
|
||||
"prefer": schema.DescriptionOf(mgr, srv+"/prefer"),
|
||||
}
|
||||
}
|
||||
s, errMsg := h.loadSystem(r)
|
||||
data.Error = errMsg
|
||||
if errMsg == "" {
|
||||
@@ -246,6 +265,15 @@ func (h *ConfigureSystemHandler) OverviewDNS(w http.ResponseWriter, r *http.Requ
|
||||
data := cfgDNSPageData{
|
||||
PageData: newPageData(w, r, "configure-dns", "DNS Client"),
|
||||
}
|
||||
if mgr := h.Schema.Manager(); mgr != nil {
|
||||
dr := "/ietf-system:system/dns-resolver"
|
||||
data.Desc = map[string]string{
|
||||
"search": schema.DescriptionOf(mgr, dr+"/search"),
|
||||
"name": schema.DescriptionOf(mgr, dr+"/server/name"),
|
||||
"address": schema.DescriptionOf(mgr, dr+"/server/udp-and-tcp/address"),
|
||||
"port": schema.DescriptionOf(mgr, dr+"/server/udp-and-tcp/port"),
|
||||
}
|
||||
}
|
||||
s, errMsg := h.loadSystem(r)
|
||||
data.Error = errMsg
|
||||
if errMsg == "" {
|
||||
@@ -315,114 +343,220 @@ func (h *ConfigureSystemHandler) SaveClock(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
// SaveNTP replaces the NTP server list in the candidate datastore.
|
||||
// PUT /configure/system/ntp
|
||||
func (h *ConfigureSystemHandler) SaveNTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// ntpCandPath is the NTP container in the candidate datastore.
|
||||
const ntpCandPath = candidatePath + "/ietf-system:system/ietf-system:ntp"
|
||||
|
||||
servers := parseNTPServers(r)
|
||||
ntp := map[string]any{"enabled": true}
|
||||
if len(servers) > 0 {
|
||||
ntp["server"] = servers
|
||||
}
|
||||
body := map[string]any{"ietf-system:ntp": ntp}
|
||||
if err := h.RC.Put(r.Context(), candidatePath+"/ietf-system:system/ntp", body); err != nil {
|
||||
log.Printf("configure system ntp: %v", err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSaved(w, "NTP saved")
|
||||
func ntpServerCandPath(name string) string {
|
||||
return ntpCandPath + "/server=" + url.PathEscape(name)
|
||||
}
|
||||
|
||||
// SaveDNS replaces the DNS resolver config in the candidate datastore.
|
||||
// PUT /configure/system/dns
|
||||
func (h *ConfigureSystemHandler) SaveDNS(w http.ResponseWriter, r *http.Request) {
|
||||
// ntpServerFromForm builds the RESTCONF body for one server, omitting the
|
||||
// udp transport when no address is given (empty inet:host is invalid) — so a
|
||||
// server can be added by name and have its address filled in via the
|
||||
// expand-to-edit form afterwards.
|
||||
func ntpServerFromForm(r *http.Request, name string) map[string]any {
|
||||
srv := map[string]any{
|
||||
"name": name,
|
||||
"association-type": defaultStr(r.FormValue("association-type"), "server"),
|
||||
"iburst": r.FormValue("iburst") == "true",
|
||||
"prefer": r.FormValue("prefer") == "true",
|
||||
}
|
||||
if addr := strings.TrimSpace(r.FormValue("address")); addr != "" {
|
||||
udp := map[string]any{"address": addr}
|
||||
if p, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("port")), 10, 16); err == nil && p > 0 {
|
||||
udp["port"] = p
|
||||
}
|
||||
srv["udp"] = udp
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
func defaultStr(v, def string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// AddNTPServer adds an NTP server, creating + enabling the NTP container on
|
||||
// first use, then re-renders the page.
|
||||
// POST /configure/system/ntp/servers
|
||||
func (h *ConfigureSystemHandler) AddNTPServer(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
search := parseSearchList(r)
|
||||
servers := parseDNSServers(r)
|
||||
|
||||
// Omit empty lists entirely — sending null for a YANG list is invalid.
|
||||
dnsResolver := map[string]any{}
|
||||
if len(search) > 0 {
|
||||
dnsResolver["search"] = search
|
||||
}
|
||||
if len(servers) > 0 {
|
||||
dnsResolver["server"] = servers
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
if name == "" {
|
||||
renderSaveError(w, fmt.Errorf("server name is required"))
|
||||
return
|
||||
}
|
||||
// PATCH the system (always present) with the NTP container nested: it is
|
||||
// created + enabled on first use and the server merges into the list.
|
||||
body := map[string]any{
|
||||
"ietf-system:dns-resolver": dnsResolver,
|
||||
"ietf-system:system": map[string]any{
|
||||
"ietf-system:ntp": map[string]any{
|
||||
"enabled": true,
|
||||
"server": []any{ntpServerFromForm(r, name)},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := h.RC.Put(r.Context(), candidatePath+"/ietf-system:system/dns-resolver", body); err != nil {
|
||||
log.Printf("configure system dns: %v", err)
|
||||
if err := h.RC.Patch(r.Context(), candidatePath+"/ietf-system:system", body); err != nil {
|
||||
log.Printf("configure ntp add server: %v", err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSaved(w, "DNS saved")
|
||||
renderSavedRedirect(w, "NTP server added", "/configure/ntp")
|
||||
}
|
||||
|
||||
// SaveNTPServer replaces one NTP server's configuration (per-row edit).
|
||||
// POST /configure/system/ntp/servers/{name}
|
||||
func (h *ConfigureSystemHandler) SaveNTPServer(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
body := map[string]any{"ietf-system:server": []any{ntpServerFromForm(r, name)}}
|
||||
if err := h.RC.Put(r.Context(), ntpServerCandPath(name), body); err != nil {
|
||||
log.Printf("configure ntp save server %q: %v", name, err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSaved(w, "Server saved")
|
||||
}
|
||||
|
||||
// DeleteNTPServer removes an NTP server.
|
||||
// DELETE /configure/system/ntp/servers/{name}
|
||||
func (h *ConfigureSystemHandler) DeleteNTPServer(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if err := h.RC.Delete(r.Context(), ntpServerCandPath(name)); err != nil && !restconf.IsNotFound(err) {
|
||||
log.Printf("configure ntp delete server %q: %v", name, err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSavedRedirect(w, "NTP server deleted", "/configure/ntp")
|
||||
}
|
||||
|
||||
// dnsCandPath is the DNS resolver container in the candidate datastore.
|
||||
const dnsCandPath = candidatePath + "/ietf-system:system/ietf-system:dns-resolver"
|
||||
|
||||
func dnsServerCandPath(name string) string {
|
||||
return dnsCandPath + "/server=" + url.PathEscape(name)
|
||||
}
|
||||
|
||||
func dnsServerFromForm(r *http.Request, name string) map[string]any {
|
||||
srv := map[string]any{"name": name}
|
||||
if addr := strings.TrimSpace(r.FormValue("address")); addr != "" {
|
||||
ut := map[string]any{"address": addr}
|
||||
if p, err := strconv.ParseUint(strings.TrimSpace(r.FormValue("port")), 10, 16); err == nil && p > 0 {
|
||||
ut["port"] = p
|
||||
}
|
||||
srv["udp-and-tcp"] = ut
|
||||
}
|
||||
return srv
|
||||
}
|
||||
|
||||
// AddDNSServer adds a DNS server, creating the resolver container on first
|
||||
// use, then re-renders the page.
|
||||
// POST /configure/system/dns/servers
|
||||
func (h *ConfigureSystemHandler) AddDNSServer(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(r.FormValue("name"))
|
||||
if name == "" {
|
||||
renderSaveError(w, fmt.Errorf("server name is required"))
|
||||
return
|
||||
}
|
||||
body := map[string]any{
|
||||
"ietf-system:system": map[string]any{
|
||||
"ietf-system:dns-resolver": map[string]any{"server": []any{dnsServerFromForm(r, name)}},
|
||||
},
|
||||
}
|
||||
if err := h.RC.Patch(r.Context(), candidatePath+"/ietf-system:system", body); err != nil {
|
||||
log.Printf("configure dns add server: %v", err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSavedRedirect(w, "DNS server added", "/configure/dns")
|
||||
}
|
||||
|
||||
// SaveDNSServer replaces one DNS server's configuration (per-row edit).
|
||||
// POST /configure/system/dns/servers/{name}
|
||||
func (h *ConfigureSystemHandler) SaveDNSServer(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := r.PathValue("name")
|
||||
body := map[string]any{"ietf-system:server": []any{dnsServerFromForm(r, name)}}
|
||||
if err := h.RC.Put(r.Context(), dnsServerCandPath(name), body); err != nil {
|
||||
log.Printf("configure dns save server %q: %v", name, err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSaved(w, "Server saved")
|
||||
}
|
||||
|
||||
// DeleteDNSServer removes a DNS server.
|
||||
// DELETE /configure/system/dns/servers/{name}
|
||||
func (h *ConfigureSystemHandler) DeleteDNSServer(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if err := h.RC.Delete(r.Context(), dnsServerCandPath(name)); err != nil && !restconf.IsNotFound(err) {
|
||||
log.Printf("configure dns delete server %q: %v", name, err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSavedRedirect(w, "DNS server deleted", "/configure/dns")
|
||||
}
|
||||
|
||||
// AddDNSSearch appends a search domain to the resolver's search leaf-list.
|
||||
// POST /configure/system/dns/search
|
||||
func (h *ConfigureSystemHandler) AddDNSSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
domain := strings.TrimSpace(r.FormValue("domain"))
|
||||
if domain == "" {
|
||||
renderSaveError(w, fmt.Errorf("domain is required"))
|
||||
return
|
||||
}
|
||||
// POST appends to the leaf-list; if the resolver container doesn't exist
|
||||
// yet, create it with this domain via a merge on the system.
|
||||
err := h.RC.PostJSON(r.Context(), dnsCandPath, map[string]any{"ietf-system:search": []any{domain}})
|
||||
if restconf.IsNotFound(err) {
|
||||
err = h.RC.Patch(r.Context(), candidatePath+"/ietf-system:system", map[string]any{
|
||||
"ietf-system:system": map[string]any{
|
||||
"ietf-system:dns-resolver": map[string]any{"search": []any{domain}},
|
||||
},
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("configure dns add search: %v", err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSavedRedirect(w, "Search domain added", "/configure/dns")
|
||||
}
|
||||
|
||||
// DeleteDNSSearch removes a search domain.
|
||||
// DELETE /configure/system/dns/search/{domain}
|
||||
func (h *ConfigureSystemHandler) DeleteDNSSearch(w http.ResponseWriter, r *http.Request) {
|
||||
domain := r.PathValue("domain")
|
||||
path := dnsCandPath + "/search=" + url.PathEscape(domain)
|
||||
if err := h.RC.Delete(r.Context(), path); err != nil && !restconf.IsNotFound(err) {
|
||||
log.Printf("configure dns delete search %q: %v", domain, err)
|
||||
renderSaveError(w, err)
|
||||
return
|
||||
}
|
||||
renderSavedRedirect(w, "Search domain removed", "/configure/dns")
|
||||
}
|
||||
|
||||
// ─── Form parsing helpers ─────────────────────────────────────────────────────
|
||||
|
||||
// parseNTPServers extracts NTP server entries from form values.
|
||||
// Fields: ntp_name_N, ntp_addr_N, ntp_port_N, ntp_prefer_N (checkbox).
|
||||
func parseNTPServers(r *http.Request) []cfgNTPServerJSON {
|
||||
var servers []cfgNTPServerJSON
|
||||
for i := 0; ; i++ {
|
||||
name := strings.TrimSpace(r.FormValue("ntp_name_" + strconv.Itoa(i)))
|
||||
if name == "" {
|
||||
break
|
||||
}
|
||||
addr := strings.TrimSpace(r.FormValue("ntp_addr_" + strconv.Itoa(i)))
|
||||
port, _ := strconv.ParseUint(r.FormValue("ntp_port_"+strconv.Itoa(i)), 10, 16)
|
||||
prefer := r.FormValue("ntp_prefer_"+strconv.Itoa(i)) == "on"
|
||||
srv := cfgNTPServerJSON{
|
||||
Name: name,
|
||||
UDP: cfgNTPUDPJSON{Address: addr, Port: uint16(port)},
|
||||
Prefer: prefer,
|
||||
}
|
||||
servers = append(servers, srv)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
// parseSearchList extracts DNS search domains from form values.
|
||||
// Fields: dns_search_N (one per domain).
|
||||
func parseSearchList(r *http.Request) []string {
|
||||
var search []string
|
||||
for i := 0; ; i++ {
|
||||
v := strings.TrimSpace(r.FormValue("dns_search_" + strconv.Itoa(i)))
|
||||
if v == "" {
|
||||
break
|
||||
}
|
||||
search = append(search, v)
|
||||
}
|
||||
return search
|
||||
}
|
||||
|
||||
// parseDNSServers extracts DNS server entries from form values.
|
||||
// Fields: dns_name_N, dns_addr_N, dns_port_N.
|
||||
func parseDNSServers(r *http.Request) []cfgDNSServerJSON {
|
||||
var servers []cfgDNSServerJSON
|
||||
for i := 0; ; i++ {
|
||||
name := strings.TrimSpace(r.FormValue("dns_name_" + strconv.Itoa(i)))
|
||||
if name == "" {
|
||||
break
|
||||
}
|
||||
addr := strings.TrimSpace(r.FormValue("dns_addr_" + strconv.Itoa(i)))
|
||||
port, _ := strconv.ParseUint(r.FormValue("dns_port_"+strconv.Itoa(i)), 10, 16)
|
||||
srv := cfgDNSServerJSON{
|
||||
Name: name,
|
||||
UDPAndTCP: cfgDNSAddrJSON{Address: addr, Port: uint16(port)},
|
||||
}
|
||||
servers = append(servers, srv)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
// SavePreferences patches infix-system augmented fields (motd-banner, text-editor).
|
||||
// POST /configure/system/preferences
|
||||
|
||||
@@ -351,8 +351,14 @@ func New(
|
||||
mux.HandleFunc("GET /configure/dns", cfgSys.OverviewDNS)
|
||||
mux.HandleFunc("POST /configure/system/identity", cfgSys.SaveIdentity)
|
||||
mux.HandleFunc("POST /configure/system/clock", cfgSys.SaveClock)
|
||||
mux.HandleFunc("PUT /configure/system/ntp", cfgSys.SaveNTP)
|
||||
mux.HandleFunc("PUT /configure/system/dns", cfgSys.SaveDNS)
|
||||
mux.HandleFunc("POST /configure/system/ntp/servers", cfgSys.AddNTPServer)
|
||||
mux.HandleFunc("POST /configure/system/ntp/servers/{name}", cfgSys.SaveNTPServer)
|
||||
mux.HandleFunc("DELETE /configure/system/ntp/servers/{name}", cfgSys.DeleteNTPServer)
|
||||
mux.HandleFunc("POST /configure/system/dns/servers", cfgSys.AddDNSServer)
|
||||
mux.HandleFunc("POST /configure/system/dns/servers/{name}", cfgSys.SaveDNSServer)
|
||||
mux.HandleFunc("DELETE /configure/system/dns/servers/{name}", cfgSys.DeleteDNSServer)
|
||||
mux.HandleFunc("POST /configure/system/dns/search", cfgSys.AddDNSSearch)
|
||||
mux.HandleFunc("DELETE /configure/system/dns/search/{domain}", cfgSys.DeleteDNSSearch)
|
||||
mux.HandleFunc("POST /configure/system/preferences", cfgSys.SavePreferences)
|
||||
mux.HandleFunc("GET /configure/interfaces", cfgIf.Overview)
|
||||
mux.HandleFunc("POST /configure/interfaces", cfgIf.CreateInterface)
|
||||
|
||||
@@ -8,75 +8,117 @@
|
||||
<div class="alert alert-error">{{.Error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form class="info-card"
|
||||
hx-put="/configure/system/dns"
|
||||
hx-swap="none">
|
||||
<div class="card-header">DNS</div>
|
||||
<section class="info-card">
|
||||
<div class="card-header">Search Domains</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table cfg-table">
|
||||
<thead><tr><th>Domain{{template "field-info" (index .Desc "search")}}</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{{range .DNS.Search}}
|
||||
<tr>
|
||||
<td>{{.}}</td>
|
||||
<td style="text-align:right">
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/system/dns/search/{{. | urlquery}}"
|
||||
hx-confirm="Remove search domain {{.}}?"
|
||||
hx-swap="none" title="Remove domain">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="2"><span class="text-muted">No search domains configured.</span></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="dns-search-add" hidden class="cfg-add-panel" style="padding:0.5rem 0.75rem">
|
||||
<form hx-post="/configure/system/dns/search" hx-swap="none" class="user-add-inline">
|
||||
<input class="cfg-input" type="text" name="domain" required placeholder="example.org">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="cfg-card-footer">
|
||||
<button type="button" class="btn-add-row" data-show="dns-search-add">+ Add domain</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-subtitle">Search domains</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table cfg-table" id="dns-search-table">
|
||||
<thead>
|
||||
<tr><th>Domain</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="dns-search-tbody">
|
||||
{{range $i, $d := .DNS.Search}}
|
||||
<tr>
|
||||
<td><input class="cfg-input" type="text"
|
||||
name="dns_search_{{$i}}" value="{{$d}}"></td>
|
||||
<td>
|
||||
<button type="button" class="btn-icon btn-icon-danger cfg-delete-row" title="Remove">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="padding: 0 0.75rem;">
|
||||
<button type="button" class="btn-add-row" data-table="dns-search-tbody"
|
||||
data-template="dns-search">+ Add domain</button>
|
||||
</div>
|
||||
|
||||
<div class="section-subtitle" style="margin-top:0.75rem">Servers</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table cfg-table" id="dns-server-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name / Key</th>
|
||||
<th>Address</th>
|
||||
<th class="cfg-input-sm">Port</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dns-server-tbody">
|
||||
{{range $i, $s := .DNS.Servers}}
|
||||
<tr>
|
||||
<td><input class="cfg-input" type="text"
|
||||
name="dns_name_{{$i}}" value="{{$s.Name}}" required></td>
|
||||
<td><input class="cfg-input" type="text"
|
||||
name="dns_addr_{{$i}}" value="{{$s.UDPAndTCP.Address}}"></td>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" min="1" max="65535"
|
||||
name="dns_port_{{$i}}" value="{{if $s.UDPAndTCP.Port}}{{$s.UDPAndTCP.Port}}{{end}}"
|
||||
placeholder="53"></td>
|
||||
<td>
|
||||
<button type="button" class="btn-icon btn-icon-danger cfg-delete-row" title="Remove">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="cfg-card-footer">
|
||||
<button type="button" class="btn-add-row" data-table="dns-server-tbody"
|
||||
data-template="dns-server">+ Add server</button>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
<section class="info-card">
|
||||
<div class="card-header">DNS Servers</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table cfg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name{{template "field-info" (index .Desc "name")}}</th>
|
||||
<th>Address{{template "field-info" (index .Desc "address")}}</th>
|
||||
<th class="cfg-input-sm">Port{{template "field-info" (index .Desc "port")}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $s := .DNS.Servers}}
|
||||
<tr>
|
||||
<td>
|
||||
<button class="key-row-toggle" type="button"
|
||||
aria-expanded="false" data-target="dns-{{$i}}">
|
||||
<span class="key-row-arrow" aria-hidden="true">▶</span>{{$s.Name}}
|
||||
</button>
|
||||
</td>
|
||||
<td>{{if $s.UDPAndTCP.Address}}{{$s.UDPAndTCP.Address}}{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
<td>{{if $s.UDPAndTCP.Port}}{{$s.UDPAndTCP.Port}}{{else}}<span class="text-muted">53</span>{{end}}</td>
|
||||
<td style="text-align:right">
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/system/dns/servers/{{$s.Name}}"
|
||||
hx-confirm="Delete DNS server {{$s.Name}}?"
|
||||
hx-swap="none" title="Delete server">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="key-detail-row" id="key-detail-dns-{{$i}}">
|
||||
<td colspan="4" class="key-detail-cell">
|
||||
<div class="key-detail-body">
|
||||
<form hx-post="/configure/system/dns/servers/{{$s.Name}}" hx-swap="none">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Address{{template "field-info" (index $.Desc "address")}}</th>
|
||||
<td><input class="cfg-input" type="text" name="address"
|
||||
value="{{$s.UDPAndTCP.Address}}" placeholder="1.1.1.1"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Port{{template "field-info" (index $.Desc "port")}}</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" min="1" max="65535"
|
||||
name="port" value="{{if $s.UDPAndTCP.Port}}{{$s.UDPAndTCP.Port}}{{end}}"
|
||||
placeholder="53" style="width:6rem"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="4"><span class="text-muted">No DNS servers configured.</span></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="dns-server-add" hidden class="cfg-add-panel" style="padding:0.5rem 0.75rem">
|
||||
<form hx-post="/configure/system/dns/servers" hx-swap="none" class="user-add-inline">
|
||||
<input class="cfg-input" type="text" name="name" required placeholder="name (e.g. cloudflare)">
|
||||
<input class="cfg-input" type="text" name="address" placeholder="address (e.g., 1.1.1.1)">
|
||||
<input class="cfg-input cfg-input-sm" type="number" name="port" min="1" max="65535"
|
||||
placeholder="53" style="width:6rem">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="cfg-card-footer">
|
||||
<button type="button" class="btn-add-row" data-show="dns-server-add">+ Add server</button>
|
||||
</div>
|
||||
</section>
|
||||
{{template "configure-toolbar" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -8,52 +8,107 @@
|
||||
<div class="alert alert-error">{{.Error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form class="info-card"
|
||||
hx-put="/configure/system/ntp"
|
||||
hx-swap="none">
|
||||
<div class="card-header">NTP Servers</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table cfg-table" id="ntp-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name / Key</th>
|
||||
<th>Address</th>
|
||||
<th class="cfg-input-sm">Port</th>
|
||||
<th>Prefer</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ntp-tbody">
|
||||
{{range $i, $s := .NTP.Servers}}
|
||||
<tr>
|
||||
<td><input class="cfg-input" type="text"
|
||||
name="ntp_name_{{$i}}" value="{{$s.Name}}" required></td>
|
||||
<td><input class="cfg-input" type="text"
|
||||
name="ntp_addr_{{$i}}" value="{{$s.UDP.Address}}"></td>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" min="1" max="65535"
|
||||
name="ntp_port_{{$i}}" value="{{if $s.UDP.Port}}{{$s.UDP.Port}}{{end}}"
|
||||
placeholder="123"></td>
|
||||
<td style="text-align:center">
|
||||
<input type="checkbox" name="ntp_prefer_{{$i}}" {{if $s.Prefer}}checked{{end}}>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn-icon btn-icon-danger cfg-delete-row"
|
||||
title="Remove">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="cfg-card-footer">
|
||||
<button type="button" class="btn-add-row" data-table="ntp-tbody"
|
||||
data-template="ntp">+ Add server</button>
|
||||
<button class="btn btn-primary" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
<section class="info-card">
|
||||
<div class="card-header">NTP Servers</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table cfg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name{{template "field-info" (index .Desc "name")}}</th>
|
||||
<th>Address{{template "field-info" (index .Desc "address")}}</th>
|
||||
<th class="cfg-input-sm">Port{{template "field-info" (index .Desc "port")}}</th>
|
||||
<th>Prefer{{template "field-info" (index .Desc "prefer")}}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $i, $s := .NTP.Servers}}
|
||||
<tr>
|
||||
<td>
|
||||
<button class="key-row-toggle" type="button"
|
||||
aria-expanded="false" data-target="ntp-{{$i}}">
|
||||
<span class="key-row-arrow" aria-hidden="true">▶</span>{{$s.Name}}
|
||||
</button>
|
||||
</td>
|
||||
<td>{{if $s.UDP.Address}}{{$s.UDP.Address}}{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
<td>{{if $s.UDP.Port}}{{$s.UDP.Port}}{{else}}<span class="text-muted">123</span>{{end}}</td>
|
||||
<td style="text-align:center">{{if $s.Prefer}}★{{end}}</td>
|
||||
<td style="text-align:right">
|
||||
<button type="button" class="btn-icon btn-icon-danger"
|
||||
hx-delete="/configure/system/ntp/servers/{{$s.Name}}"
|
||||
hx-confirm="Delete NTP server {{$s.Name}}?"
|
||||
hx-swap="none" title="Delete server">
|
||||
{{template "icon-trash"}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="key-detail-row" id="key-detail-ntp-{{$i}}">
|
||||
<td colspan="5" class="key-detail-cell">
|
||||
<div class="key-detail-body">
|
||||
<form hx-post="/configure/system/ntp/servers/{{$s.Name}}" hx-swap="none">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<th>Address{{template "field-info" (index $.Desc "address")}}</th>
|
||||
<td><input class="cfg-input" type="text" name="address"
|
||||
value="{{$s.UDP.Address}}" placeholder="address (e.g., pool.ntp.org)"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Port{{template "field-info" (index $.Desc "port")}}</th>
|
||||
<td><input class="cfg-input cfg-input-sm" type="number" min="1" max="65535"
|
||||
name="port" value="{{if $s.UDP.Port}}{{$s.UDP.Port}}{{end}}"
|
||||
placeholder="123" style="width:6rem"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Association{{template "field-info" (index $.Desc "assoc")}}</th>
|
||||
<td>
|
||||
<select class="cfg-input" name="association-type">
|
||||
{{range $.AssocTypes}}
|
||||
<option value="{{.}}" {{if eq . $s.AssociationType}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>iburst{{template "field-info" (index $.Desc "iburst")}}</th>
|
||||
<td><label><input type="checkbox" name="iburst" value="true"
|
||||
{{if $s.IBurst}}checked{{end}}> Send an initial burst for faster sync</label></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Prefer{{template "field-info" (index $.Desc "prefer")}}</th>
|
||||
<td><label><input type="checkbox" name="prefer" value="true"
|
||||
{{if $s.Prefer}}checked{{end}}> Mark as the preferred source</label></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cfg-card-footer" style="padding-left:0">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Save</button>
|
||||
<span class="cfg-save-status"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5"><span class="text-muted">No NTP servers configured.</span></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{/* Hidden add-server form, revealed by the + Add server button (data-show) */}}
|
||||
<div id="ntp-add-row" hidden class="cfg-add-panel" style="padding:0.5rem 0.75rem">
|
||||
<form hx-post="/configure/system/ntp/servers" hx-swap="none" class="user-add-inline">
|
||||
<input class="cfg-input" type="text" name="name" required placeholder="name (label)">
|
||||
<input class="cfg-input" type="text" name="address" placeholder="address (e.g., pool.ntp.org)">
|
||||
<input class="cfg-input cfg-input-sm" type="number" name="port" min="1" max="65535"
|
||||
placeholder="123" style="width:6rem">
|
||||
<label style="white-space:nowrap"><input type="checkbox" name="prefer" value="true"> Prefer</label>
|
||||
<button class="btn btn-primary btn-sm" type="submit">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="cfg-card-footer">
|
||||
<button type="button" class="btn-add-row" data-show="ntp-add-row">+ Add server</button>
|
||||
</div>
|
||||
</section>
|
||||
{{template "configure-toolbar" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user