From ad7bc0345a1b29969ccaf3eb57d9327f60b78700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Feb 2026 16:43:41 +0100 Subject: [PATCH 01/10] Added headers and table for records --- static/css/index.css | 129 +++++++++++++++++++++++++++++++++++++------ static/js/dns.mjs | 38 +++++++++---- 2 files changed, 139 insertions(+), 28 deletions(-) diff --git a/static/css/index.css b/static/css/index.css index 6957ab1..b5497b8 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -2,9 +2,6 @@ --line-color: #ccc; --line-color-record: #eee; - --type-background: #ddd; - --type-foreground: #000; - --label-first: #800033; --label-rest: #666; @@ -12,6 +9,16 @@ --label-border: #ccc; --copy-color: #d48700; + + --header-line: #d0d0d0; + + --record-A: #89a02c; + --record-AAAA: #2f4858; + --record-CNAME: #f6ae2d; + --record-FWD: #f26419; + --record-TXT: #86bbd8; + --record-NXDOMAIN: #aa0000; + --record-other: #888; } html { @@ -149,17 +156,52 @@ button { &>.records { padding-left: 30px; + padding-top: 8px; + padding-bottom: 8px; + margin-left: 10px; display: grid; - grid-template-columns: repeat(3, min-content) 1fr; - grid-gap: 4px 10px; + grid-template-columns: repeat(5, min-content); + width: min-content; + /*grid-gap: 4px 10px;*/ align-items: center; border-left: 1px solid var(--line-color); - &>img { - display: block; - padding-left: 4px; + .header { + font-weight: bold; + font-size: 0.75em; + background: #eee; + padding: 2px 8px; + border-left: 1px solid var(--header-line); + border-top: 1px solid var(--header-line); + border-bottom: 1px solid var(--header-line); + + &:first-child { + grid-column: 1 / 3; + border-top-left-radius: 4px; + } + + &.last { + border-right: 1px solid var(--header-line); + border-top-right-radius: 4px; + } + } + + &>:not(.header) { + height: 40px; + } + + &>.record-icon { + border-left: 1px solid var(--header-line); + align-content: center; + padding-left: 8px; + border-bottom: 1px solid var(--header-line); + + img { + display: block; + padding-left: 4px; + } } .copy { @@ -174,6 +216,10 @@ button { cursor: pointer; user-select: none; display: flex; + padding: 0 16px 0 8px; + border-bottom: 1px solid var(--header-line); + height: 100%; + align-items: center; &.created { * { @@ -202,20 +248,69 @@ button { } .type { - background-color: var(--type-background); - color: var(--type-foreground); - padding: 4px 8px; - border-radius: 4px; - margin-top: 2px; - margin-bottom: 2px; - width: min-content; - font-weight: bold; - font-size: 0.85em; + padding: 2px 8px; + border-bottom: 1px solid var(--header-line); + border-left: 1px solid var(--header-line); + align-content: center; + cursor: pointer; + + div { + background-color: var(--record-other); + color: #fff; + + padding: 4px 8px; + border-radius: 4px; + width: min-content; + height: min-content; + font-weight: bold; + font-size: 0.85em; + } + + &.A div { + background-color: var(--record-A); + } + + &.AAAA div { + background-color: var(--record-AAAA); + } + + &.CNAME div { + background-color: var(--record-CNAME); + color: #000; + } + + &.FWD div { + background-color: var(--record-FWD); + } + + &.TXT div { + background-color: var(--record-TXT); + color: #000; + } + + &.NXDOMAIN div { + background-color: var(--record-NXDOMAIN); + } } .value { cursor: pointer; user-select: none; + border-left: 1px solid var(--header-line); + border-bottom: 1px solid var(--header-line); + padding: 0px 8px; + height: 100%; + align-content: center; + } + + .ttl { + cursor: pointer; + border-left: 1px solid var(--header-line); + border-right: 1px solid var(--header-line); + border-bottom: 1px solid var(--header-line); + padding: 0px 8px; + height: 100%; + align-content: center; } .separator { diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 431521c..de534d1 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -232,7 +232,12 @@ class Folder { ${firstLabel}${restLabels != '' ? '.' + restLabels : ''}
-
+
+
FQDN
+
Type
+
Value
+
TTL
+
` this.divSubfolders = this.div.querySelector('.subfolders') this.divRecords = this.div.querySelector('.records') @@ -249,11 +254,16 @@ class Folder { this.divSubfolders.append(folder.render()) // Records are refreshed. + if (this.records.length == 0) + this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'none') + else + this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'block') + for (const rec of Array.from(this.records)) this.divRecords.append(...rec.render()) // Open this folder automatically if it is a toplevel folder and the settings change to open. - _mbus.subscribe('settings_updated', ({ detail: { key, value }}) => { + _mbus.subscribe('settings_updated', ({ detail: { key, value } }) => { if (key !== 'toplevel_open') return @@ -274,7 +284,7 @@ class Record { this.divFQDN = null this.divType = null this.divValue = null - this.divSeparator = null + this.divTTL = null }// }}} id() {// {{{ @@ -332,18 +342,22 @@ class Record { }// }}} render() {// {{{ if (this.divFQDN === null) { - this.imgIcon = document.createElement('img') + this.imgIcon = document.createElement('div') this.divFQDN = document.createElement('div') this.divType = document.createElement('div') this.divValue = document.createElement('div') - this.divSeparator = document.createElement('div') + this.divTTL = document.createElement('div') + + this.imgIcon.innerHTML = `` + this.imgIcon.classList.add("record-icon") - this.imgIcon.src = `/images/${_VERSION}/icon_record.svg` this.divFQDN.classList.add('fqdn') this.divType.classList.add('type') + this.divType.classList.add(this.type()) this.divValue.classList.add('value') - this.divSeparator.classList.add('separator') + this.divTTL.classList.add('ttl') + this.divType.innerHTML = `
` this.divFQDN.innerHTML = ` *. @@ -356,13 +370,14 @@ class Record { else this.edit() }) - this.divValue.addEventListener('click', event => { if (event.shiftKey) this.copy(event.target.closest('.value'), this.value()) else this.edit() }) + this.divType.addEventListener('click', () => this.edit()) + this.divTTL.addEventListener('click', () => this.edit()) } // FQDN is updated. @@ -376,10 +391,11 @@ class Record { this.divFQDN.querySelector('.first-label').innerText = fl this.divFQDN.querySelector('.rest-label').innerText = rl != '' ? `.${rl}` : '' - this.divType.innerText = this.type() + this.divType.querySelector('div').innerText = this.type() this.divValue.innerText = this.value() + this.divTTL.innerText = this.ttl() - return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divSeparator] + return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL] }// }}} save() {// {{{ const created = (this.id() == '') @@ -505,7 +521,7 @@ class RecordDialog { class Settings { constructor() {// {{{ this.settings = new Map([ - ['boxed_folders', true], + ['boxed_folders', false], ['toplevel_open', true], ]) From 52bd8b34b38d63e7a25953966c6ff1528430d366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Feb 2026 16:54:39 +0100 Subject: [PATCH 02/10] Hover over row --- static/css/index.css | 23 ++++++++++++++++------- static/js/dns.mjs | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/static/css/index.css b/static/css/index.css index b5497b8..7d2f395 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -19,6 +19,8 @@ --record-TXT: #86bbd8; --record-NXDOMAIN: #aa0000; --record-other: #888; + + --record-hover: #fffff8; } html { @@ -221,6 +223,10 @@ button { height: 100%; align-items: center; + &.mouse-over { + background-color: var(--record-hover); + } + &.created { * { color: #44aa00 !important; @@ -254,6 +260,10 @@ button { align-content: center; cursor: pointer; + &.mouse-over { + background-color: var(--record-hover); + } + div { background-color: var(--record-other); color: #fff; @@ -301,6 +311,10 @@ button { padding: 0px 8px; height: 100%; align-content: center; + + &.mouse-over { + background-color: var(--record-hover); + } } .ttl { @@ -311,16 +325,11 @@ button { padding: 0px 8px; height: 100%; align-content: center; - } - .separator { - grid-column: 1 / -1; - - &:not(:last-child) { - border-bottom: 1px solid var(--line-color-record); + &.mouse-over { + background-color: var(--record-hover); } } - } } diff --git a/static/js/dns.mjs b/static/js/dns.mjs index de534d1..6297f25 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -357,6 +357,15 @@ class Record { this.divValue.classList.add('value') this.divTTL.classList.add('ttl') + this.divFQDN.addEventListener('mouseenter', ()=>this.mouseEnter()) + this.divFQDN.addEventListener('mouseleave', ()=>this.mouseLeave()) + this.divType.addEventListener('mouseenter', ()=>this.mouseEnter()) + this.divType.addEventListener('mouseleave', ()=>this.mouseLeave()) + this.divValue.addEventListener('mouseenter', ()=>this.mouseEnter()) + this.divValue.addEventListener('mouseleave', ()=>this.mouseLeave()) + this.divTTL.addEventListener('mouseenter', ()=>this.mouseEnter()) + this.divTTL.addEventListener('mouseleave', ()=>this.mouseLeave()) + this.divType.innerHTML = `
` this.divFQDN.innerHTML = ` *. @@ -397,6 +406,19 @@ class Record { return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL] }// }}} + mouseEnter() { + this.divFQDN.classList.add('mouse-over') + this.divType.classList.add('mouse-over') + this.divValue.classList.add('mouse-over') + this.divTTL.classList.add('mouse-over') + } + mouseLeave() { + this.divFQDN.classList.remove('mouse-over') + this.divType.classList.remove('mouse-over') + this.divValue.classList.remove('mouse-over') + this.divTTL.classList.remove('mouse-over') + } + save() {// {{{ const created = (this.id() == '') From df936baa8f57223f2f716a2edbfb414b3eddc8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Feb 2026 21:05:02 +0100 Subject: [PATCH 03/10] Delete records --- routeros_device.go | 15 ++++++- static/css/index.css | 24 ++++++++++-- static/images/icon_delete.svg | 49 +++++++++++++++++++++++ static/js/dns.mjs | 74 +++++++++++++++++++++++++++-------- webserver.go | 28 ++++++++++++- 5 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 static/images/icon_delete.svg diff --git a/routeros_device.go b/routeros_device.go index 4c65e25..50c19d1 100644 --- a/routeros_device.go +++ b/routeros_device.go @@ -58,7 +58,7 @@ func (dev *RouterosDevice) Init() { // {{{ // query sends a RouterOS REST API query and returns the unparsed body. func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{ - url := fmt.Sprintf("https://%s/rest%s", dev.Host, path) + url := fmt.Sprintf("https://%s:%d/rest%s", dev.Host, dev.Port, path) logger.Info("URL", "method", method, "url", url) var request *http.Request @@ -150,6 +150,19 @@ func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (entry DNSEntry, err err = json.Unmarshal(body, &entry) return }// }}} +func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) { + _, err = dev.query("DELETE", "/ip/dns/static/"+id, []byte{}) + if err != nil { + rosError := struct{ Detail string }{} + if jsonError := json.Unmarshal([]byte(err.Error()), &rosError); jsonError == nil { + logger.Error("routeros", "error", jsonError) + err = errors.New(rosError.Detail) + return + } + return + } + return +} /* // FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router diff --git a/static/css/index.css b/static/css/index.css index 7d2f395..e54174e 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -20,7 +20,7 @@ --record-NXDOMAIN: #aa0000; --record-other: #888; - --record-hover: #fffff8; + --record-hover: #fffff4; } html { @@ -164,9 +164,8 @@ button { margin-left: 10px; display: grid; - grid-template-columns: repeat(5, min-content); + grid-template-columns: repeat(6, min-content); width: min-content; - /*grid-gap: 4px 10px;*/ align-items: center; border-left: 1px solid var(--line-color); @@ -174,7 +173,7 @@ button { font-weight: bold; font-size: 0.75em; background: #eee; - padding: 2px 8px; + padding: 4px 8px; border-left: 1px solid var(--header-line); border-top: 1px solid var(--header-line); border-bottom: 1px solid var(--header-line); @@ -319,6 +318,18 @@ button { .ttl { cursor: pointer; + border-left: 1px solid var(--header-line); + border-bottom: 1px solid var(--header-line); + padding: 0px 8px; + height: 100%; + align-content: center; + + &.mouse-over { + background-color: var(--record-hover); + } + } + + .actions { border-left: 1px solid var(--header-line); border-right: 1px solid var(--header-line); border-bottom: 1px solid var(--header-line); @@ -329,6 +340,11 @@ button { &.mouse-over { background-color: var(--record-hover); } + + img { + display: block; + cursor: pointer; + } } } diff --git a/static/images/icon_delete.svg b/static/images/icon_delete.svg new file mode 100644 index 0000000..0bc82d4 --- /dev/null +++ b/static/images/icon_delete.svg @@ -0,0 +1,49 @@ + + + + + + + + trash-can-outline + + + diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 6297f25..3148375 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -29,6 +29,10 @@ export class Application { this.cleanFolders(folder) }) }// }}} + deleteRecord(id) {// {{{ + const i = this.records.findIndex(rec => rec.id() == id) + this.records.splice(i, 1) + }// }}} renderFolders() {// {{{ this.records.sort(this.sortRecords) @@ -236,7 +240,8 @@ class Folder {
FQDN
Type
Value
-
TTL
+
TTL
+
Actions
` this.divSubfolders = this.div.querySelector('.subfolders') @@ -259,6 +264,17 @@ class Folder { else this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'block') + // Remove old ones + console.log(`removing records from ${this.name()}`) + for (const recdiv of this.divRecords.children) { + if (recdiv?.classList?.contains('fqdn')) { + const rec = this.records.find(r => r.id() == recdiv.dataset.record_id) + if (!rec) + this.divRecords.querySelectorAll(`[data-record_id="${recdiv.dataset.record_id}"]`) + .forEach(el => el.remove()) + } + } + for (const rec of Array.from(this.records)) this.divRecords.append(...rec.render()) @@ -285,6 +301,7 @@ class Record { this.divType = null this.divValue = null this.divTTL = null + this.divActions = null }// }}} id() {// {{{ @@ -347,24 +364,24 @@ class Record { this.divType = document.createElement('div') this.divValue = document.createElement('div') this.divTTL = document.createElement('div') + this.divActions = document.createElement('div') this.imgIcon.innerHTML = `` - this.imgIcon.classList.add("record-icon") + this.imgIcon.classList.add("record-icon"); + + [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions] + .forEach(div => { + div.addEventListener('mouseenter', () => this.mouseEnter()) + div.addEventListener('mouseleave', () => this.mouseLeave()) + div.dataset.record_id = this.id() + }) + this.divType.classList.add(this.type()) this.divFQDN.classList.add('fqdn') this.divType.classList.add('type') - this.divType.classList.add(this.type()) this.divValue.classList.add('value') this.divTTL.classList.add('ttl') - - this.divFQDN.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divFQDN.addEventListener('mouseleave', ()=>this.mouseLeave()) - this.divType.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divType.addEventListener('mouseleave', ()=>this.mouseLeave()) - this.divValue.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divValue.addEventListener('mouseleave', ()=>this.mouseLeave()) - this.divTTL.addEventListener('mouseenter', ()=>this.mouseEnter()) - this.divTTL.addEventListener('mouseleave', ()=>this.mouseLeave()) + this.divActions.classList.add('actions') this.divType.innerHTML = `
` this.divFQDN.innerHTML = ` @@ -372,6 +389,9 @@ class Record { ` + this.divActions.innerHTML = ` + + ` this.divFQDN.addEventListener('click', event => { if (event.shiftKey) @@ -387,6 +407,7 @@ class Record { }) this.divType.addEventListener('click', () => this.edit()) this.divTTL.addEventListener('click', () => this.edit()) + this.divActions.querySelector('.delete').addEventListener('click', () => this.delete()) } // FQDN is updated. @@ -404,20 +425,22 @@ class Record { this.divValue.innerText = this.value() this.divTTL.innerText = this.ttl() - return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL] + return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions] }// }}} - mouseEnter() { + mouseEnter() {// {{{ this.divFQDN.classList.add('mouse-over') this.divType.classList.add('mouse-over') this.divValue.classList.add('mouse-over') this.divTTL.classList.add('mouse-over') - } - mouseLeave() { + this.divActions.classList.add('mouse-over') + }// }}} + mouseLeave() {// {{{ this.divFQDN.classList.remove('mouse-over') this.divType.classList.remove('mouse-over') this.divValue.classList.remove('mouse-over') this.divTTL.classList.remove('mouse-over') - } + this.divActions.classList.remove('mouse-over') + }// }}} save() {// {{{ const created = (this.id() == '') @@ -456,6 +479,23 @@ class Record { } }) }// }}} + delete() {// {{{ + if (!confirm(`Are you sure you want to delete ${this.name()}?`)) + return + + fetch(`/record/delete/${this.id()}`) + .then(data => data.json()) + .then(json => { + if (!json.OK) { + alert(json.Error) + return + } + _app.deleteRecord(this.id()) + _app.cleanFolders() + _app.renderFolders() + _app.render() + }) + }// }}} openParentFolders(folder) {// {{{ if (folder === undefined) diff --git a/webserver.go b/webserver.go index 56f75c8..fc916ec 100644 --- a/webserver.go +++ b/webserver.go @@ -7,6 +7,7 @@ import ( // Standard "embed" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -33,6 +34,7 @@ func registerWebserverHandlers() { http.HandleFunc("/", rootHandler) http.HandleFunc("/record/save", actionRecordSave) + http.HandleFunc("/record/delete/{id}", actionRecordDelete) } func startWebserver() { @@ -110,6 +112,30 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) { record = NewDNSRecord(entry) - j, _ := json.Marshal(struct{ OK bool; Record DNSRecord }{true, record}) + j, _ := json.Marshal(struct { + OK bool + Record DNSRecord + }{true, record}) + w.Write(j) +} + +func actionRecordDelete(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + + if id == "" { + httpError(w, errors.New("No ID provided")) + return + } + + err := device.DeleteDNSEntry(id) + if err != nil { + httpError(w, err) + logger.Error("webserver", "op", "record_delete", "error", err) + return + } + + j, _ := json.Marshal(struct { + OK bool + }{true}) w.Write(j) } From 8aef6d8a2eeddff6c02a5c7497aa6fcd31eddc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Feb 2026 21:18:00 +0100 Subject: [PATCH 04/10] Creating records in folders --- static/css/index.css | 14 +++++++++++++- static/js/dns.mjs | 8 +++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/static/css/index.css b/static/css/index.css index e54174e..7fa616d 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -138,7 +138,7 @@ button { &>.label { display: grid; - grid-template-columns: min-content min-content 1fr; + grid-template-columns: repeat(4, min-content); align-items: center; padding: 5px 0px; cursor: pointer; @@ -149,6 +149,18 @@ button { margin-right: 6px; display: none; } + + img.create { + display: none; + height: 16px; + margin-left: 8px; + } + + &:hover { + img.create { + display: block; + } + } } &>.subfolders { diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 3148375..a4fe589 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -234,6 +234,7 @@ class Folder { ${firstLabel}${restLabels != '' ? '.' + restLabels : ''} +
@@ -248,6 +249,7 @@ class Folder { this.divRecords = this.div.querySelector('.records') this.div.querySelector('.label').addEventListener('click', event => this.toggleFolder(event)) + this.div.querySelector('.label .create').addEventListener('click', event => this.createRecord(event)) } @@ -265,7 +267,6 @@ class Folder { this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'block') // Remove old ones - console.log(`removing records from ${this.name()}`) for (const recdiv of this.divRecords.children) { if (recdiv?.classList?.contains('fqdn')) { const rec = this.records.find(r => r.id() == recdiv.dataset.record_id) @@ -289,6 +290,11 @@ class Folder { return this.div }// }}} + createRecord(event) {// {{{ + event.stopPropagation() + const record = new Record({ Name: this.name() }) + new RecordDialog(record).show() + }// }}} } class Record { From 39b9515f7613f8cc5747682037b5c5371f41ad9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Feb 2026 21:46:28 +0100 Subject: [PATCH 05/10] Basic search --- static/css/index.css | 4 ++++ static/js/dns.mjs | 51 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/static/css/index.css b/static/css/index.css index 7fa616d..b85fe3b 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -106,6 +106,10 @@ button { cursor: pointer; } +#search { + margin-bottom: 16px; +} + #records-tree { white-space: nowrap; diff --git a/static/js/dns.mjs b/static/js/dns.mjs index a4fe589..32b77c8 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -9,9 +9,20 @@ export class Application { this.recordsTree = null this.settingsIcon = null + this.searchFor = '' + this.renderFolders() this.render() }// }}} + filteredRecords() {// {{{ + if (this.searchFor === '') + return this.records + + const records = this.records.filter(r => { + return (r.name().includes(this.searchFor)) + }) + return records + }// }}} parseRecords(recordsData) {// {{{ const records = recordsData.map(d => new Record(d)) return records @@ -34,11 +45,11 @@ export class Application { this.records.splice(i, 1) }// }}} renderFolders() {// {{{ - this.records.sort(this.sortRecords) - + const records = this.filteredRecords() + records.sort(this.sortRecords) // rec: for example www.google.com - for (const rec of this.records) { + for (const rec of records) { // com.google (reverse and remove wwww) const labels = rec.labels().reverse().slice(0, -1) @@ -98,9 +109,6 @@ export class Application { }// }}} render() {// {{{ if (this.recordsTree == null) { - this.recordsTree = document.createElement('div') - this.recordsTree.id = 'records-tree' - this.createIcon = document.createElement('img') this.createIcon.id = 'create-icon' this.createIcon.src = `/images/${_VERSION}/icon_create.svg` @@ -111,6 +119,26 @@ export class Application { this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg` this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show()) + const searchEl = document.createElement('div') + searchEl.id = 'search' + searchEl.innerHTML = ` + + + ` + this.searchField = searchEl.querySelector('.search-for') + this.searchField.addEventListener('keydown', event => { + if (event.key == 'Enter') + this.search() + }) + const search = searchEl.querySelector('button.search') + search.addEventListener('click', () => this.search()) + + + this.recordsTree = document.createElement('div') + this.recordsTree.id = 'records-tree' + + + document.body.appendChild(searchEl) document.body.appendChild(this.recordsTree) document.body.appendChild(this.settingsIcon) document.body.appendChild(this.createIcon) @@ -167,6 +195,13 @@ export class Application { else document.body.classList.remove('boxed-folders') }// }}} + search() { + this.searchFor = this.searchField.value.trim().toLowerCase() + + this.cleanFolders() + this.renderFolders() + this.render() + } } class Folder { @@ -262,9 +297,9 @@ class Folder { // Records are refreshed. if (this.records.length == 0) - this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'none') + this.divRecords.style.display = 'none' else - this.divRecords.querySelectorAll('.header').forEach(h => h.style.display = 'block') + this.divRecords.style.display = '' // Remove old ones for (const recdiv of this.divRecords.children) { From b20943cfd273bb180f6f04203768ee62a960da6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Wed, 25 Feb 2026 23:50:31 +0100 Subject: [PATCH 06/10] WIP search --- static/js/dns.mjs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 32b77c8..332df24 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -153,11 +153,31 @@ export class Application { for (const folder of folders) this.recordsTree.append(folder.render()) + this.removeEmptyFolders() + // Subscribe to settings update since the elements they will change // exists now. _mbus.subscribe('settings_updated', event => this.handlerSettingsUpdated(event.detail)) this.setBoxedFolders(this.settings.get('boxed_folders')) + }// }}} + removeEmptyFolders(folder) {// {{{ + if (folder === undefined) + folder = this.topFolder + folder.subfolders.forEach((folder, _label) => { + this.removeEmptyFolders(folder) + }) + + // This is a leaf folder in the tree. + // It has to be removed from the parent as well, since that could be up for + // removal as well, all the way up the chain. + if (folder.subfolders.size === 0 && folder.records.length === 0) { + if (folder.parentFolder) { + folder.parentFolder.subfolders.delete(folder.labels()[0]) + } + + folder.div?.remove() + } }// }}} handlerKeys(event) {// {{{ let handled = true @@ -175,6 +195,10 @@ export class Application { new RecordDialog(new Record()).show() break + case 'f': + this.searchField.focus() + break + default: handled = false } @@ -195,13 +219,13 @@ export class Application { else document.body.classList.remove('boxed-folders') }// }}} - search() { + search() {// {{{ this.searchFor = this.searchField.value.trim().toLowerCase() this.cleanFolders() this.renderFolders() this.render() - } + }// }}} } class Folder { From cbd1b137cf3b528b63604fb7f971f3f4c4a663f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 26 Feb 2026 08:31:27 +0100 Subject: [PATCH 07/10] Recreate the records tree when searching --- static/js/dns.mjs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 332df24..88f095c 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -8,6 +8,7 @@ export class Application { this.topFolder = new Folder(this, null, 'root') this.recordsTree = null this.settingsIcon = null + this.createIcon = null this.searchFor = '' @@ -108,7 +109,7 @@ export class Application { return 0 }// }}} render() {// {{{ - if (this.recordsTree == null) { + if (this.createIcon === null) { this.createIcon = document.createElement('img') this.createIcon.id = 'create-icon' this.createIcon.src = `/images/${_VERSION}/icon_create.svg` @@ -133,17 +134,20 @@ export class Application { const search = searchEl.querySelector('button.search') search.addEventListener('click', () => this.search()) + document.body.appendChild(this.settingsIcon) + document.body.appendChild(this.createIcon) + document.body.appendChild(searchEl) + document.body.addEventListener('keydown', event => this.handlerKeys(event)) + } + + // The recordstree is deleted when making a search and is therefore + // not created along with icons and search above. + if (this.recordsTree === null) { this.recordsTree = document.createElement('div') this.recordsTree.id = 'records-tree' - - document.body.appendChild(searchEl) document.body.appendChild(this.recordsTree) - document.body.appendChild(this.settingsIcon) - document.body.appendChild(this.createIcon) - - document.body.addEventListener('keydown', event => this.handlerKeys(event)) } // Top root folder doesn't have to be shown. @@ -221,6 +225,9 @@ export class Application { }// }}} search() {// {{{ this.searchFor = this.searchField.value.trim().toLowerCase() + this.recordsTree.remove() + this.topFolder = new Folder(this, null, 'root') + this.recordsTree = null this.cleanFolders() this.renderFolders() From fc9583ecd2f201bfacf07babcb3cf41fe84ca24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 26 Feb 2026 09:12:05 +0100 Subject: [PATCH 08/10] Concatenate the two most significant labels --- static/js/dns.mjs | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 88f095c..752ad53 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -49,10 +49,16 @@ export class Application { const records = this.filteredRecords() records.sort(this.sortRecords) - // rec: for example www.google.com for (const rec of records) { - // com.google (reverse and remove wwww) - const labels = rec.labels().reverse().slice(0, -1) + // It felt wrong when records for the base domain (e.g. google.com) was put in the top level domain (.com). + // While technically correct, the first label (com) is grouped together with the second label (google). + // Labels are counted reversely since the top domain is most significant. + // + // The `if` here is the exception for records that only has the two first labels. It would otherwise just + // be an empty array of labels and thus discarded. + let labels = rec.labels().reverse().slice(0, -1) + if (rec.labels().length == 1) + labels = rec.labels() // Start each record from the top and iterate through all its labels // except the first one since that would be the actual record. @@ -254,7 +260,18 @@ class Folder { return this.folderName.toLowerCase() }// }}} labels() {// {{{ - return this.name().split('.') + let labels = this.name().split('.') + + // It is very uncommon to see just the top level domain. + // We're much more used to google.com than com and then google. + // First level is therefore the two most significant labels concatenated. + if (labels.length > 1) { + labels.reverse() + labels = [`${labels[1]}.${labels[0]}`].concat(labels.slice(2)) + labels.reverse() + } + + return labels }// }}} addRecord(rec) {// {{{ this.records.push(rec) @@ -401,7 +418,24 @@ class Record { return this.data.MatchSubdomain === 'true' }// }}} labels() {// {{{ - return this.name().split('.') + let labels = this.name().split('.') + + if (labels.length === 1) { + labels = [labels[0], '_no', 'domain'] + } + + // It is very uncommon to see just the top level domain. + // We're much more used to google.com than com and then google. + // First level is therefore the two most significant labels concatenated. + if (labels.length > 1) { + labels.reverse() + labels = [`${labels[1]}.${labels[0]}`].concat(labels.slice(2)) + labels.reverse() + } else { + console.log(this, labels) + } + + return labels }// }}} copy(el, text) {// {{{ From 96f3a90ab94682a711095d12d2eef1682575e28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 26 Feb 2026 09:17:48 +0100 Subject: [PATCH 09/10] Fixed creating records beneath _no.domain --- static/js/dns.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/js/dns.mjs b/static/js/dns.mjs index 752ad53..b40adcc 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -451,6 +451,8 @@ class Record { if (value.slice(0, 2) == '*.') { this.data['Name'] = value.slice(2) this.data['MatchSubdomain'] = 'true' + } else if (value.slice(-11) == '._no.domain') { + this.data['Name'] = value.slice(0, -11) } else { this.data['Name'] = value this.data['MatchSubdomain'] = 'false' @@ -690,7 +692,7 @@ class Settings { constructor() {// {{{ this.settings = new Map([ ['boxed_folders', false], - ['toplevel_open', true], + ['toplevel_open', false], ]) // Read any configured settings from local storage, but keeping default value From d0b0aba30da58c108f689b10ad8794be1d970888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 26 Feb 2026 09:28:14 +0100 Subject: [PATCH 10/10] Show _no.domain in italic --- static/css/index.css | 8 ++++++++ static/js/dns.mjs | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/static/css/index.css b/static/css/index.css index b85fe3b..ba6c2a1 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -120,6 +120,10 @@ button { padding-left: 0px; } + &.no-domain > .label { + font-style: italic; + } + &.open { &>.label>img.open { display: block; @@ -265,6 +269,10 @@ button { .rest-label { color: var(--label-rest); + + &.no-domain { + font-style: italic; + } } } diff --git a/static/js/dns.mjs b/static/js/dns.mjs index b40adcc..8f9195d 100644 --- a/static/js/dns.mjs +++ b/static/js/dns.mjs @@ -333,6 +333,11 @@ class Folder { this.div.querySelector('.label').addEventListener('click', event => this.toggleFolder(event)) this.div.querySelector('.label .create').addEventListener('click', event => this.createRecord(event)) + + if (this.name() == '_no.domain') { + console.log('wut') + this.div.classList.add('no-domain') + } } @@ -526,8 +531,15 @@ class Record { const fl = this.labels()[0] const rl = this.labels().slice(1).join('.') - this.divFQDN.querySelector('.first-label').innerText = fl - this.divFQDN.querySelector('.rest-label').innerText = rl != '' ? `.${rl}` : '' + const flEl = this.divFQDN.querySelector('.first-label') + const rlEl = this.divFQDN.querySelector('.rest-label') + flEl.innerText = fl + rlEl.innerText = rl != '' ? `.${rl}` : '' + + if (rl == '_no.domain') + rlEl.classList.add('no-domain') + else + rlEl.classList.remove('no-domain') this.divType.querySelector('div').innerText = this.type() this.divValue.innerText = this.value()