From 83e9753b18a7af9757e25978b8c38e866a83cb84 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sat, 11 Jul 2026 00:32:21 -0400 Subject: [PATCH] Dynamic DNS UI: fix blank columns, redesign the record list The Host and Last-updated columns were blank because they relied on a jq-repeat parseData hook that the rest of the app doesn't actually use for display (working rows derive dates from the .momentFromNow class, not parseData). - Derive fqdn server-side via DynamicRecord.toJSON so it flows through both the REST list and websocket payloads; the template uses {{fqdn}} directly. - Render last-updated with the .momentFromNow class (data-date) like the rest of the app instead of a parseData-computed string. - apply() now clears last_status on success so the UI only surfaces real errors. - Redesign the section: prominent public-IP badge, cleaner add form, and a Bootstrap list-group of records (fqdn, IP badge, "updated N ago", inline error) with outline refresh/remove buttons. Co-Authored-By: Claude Opus 4.8 --- nodejs/models/dynamic_record.js | 9 ++- nodejs/views/dns.ejs | 102 ++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/nodejs/models/dynamic_record.js b/nodejs/models/dynamic_record.js index 1dfa812..c2c7762 100644 --- a/nodejs/models/dynamic_record.js +++ b/nodejs/models/dynamic_record.js @@ -51,6 +51,12 @@ class DynamicRecord extends Table{ return (this.name === '@' || !this.name) ? this.domain : `${this.name}.${this.domain}`; } + // Expose a derived fqdn to the client (REST list + websocket payloads both + // serialize via toJSON), so the UI doesn't depend on client-side parsing. + toJSON(){ + return {...super.toJSON(), fqdn: this.fqdn()}; + } + // Point this record at `ip` and record the outcome. Never throws — a single // bad record must not abort a whole refresh cycle. async apply(ip){ @@ -58,7 +64,8 @@ class DynamicRecord extends Table{ let Domain = require('.').models.Domain; let domain = await Domain.get(this.domain); let res = await domain.upsertARecord(this.name, ip); - await this.update({last_ip: ip, last_status: 'ok', last_updated: Date.now()}); + // Empty status on success so the UI only surfaces actual errors. + await this.update({last_ip: ip, last_status: '', last_updated: Date.now()}); return res; }catch(error){ console.error('DynamicRecord.apply', this.id, error.message); diff --git a/nodejs/views/dns.ejs b/nodejs/views/dns.ejs index 12e6690..2210101 100644 --- a/nodejs/views/dns.ejs +++ b/nodejs/views/dns.ejs @@ -63,12 +63,6 @@ return row }; - $.scope.DynamicRecord.parseData = function(row){ - row['fqdn'] = (row.name === '@' || !row.name) ? row.domain : `${row.name}.${row.domain}`; - row['last_updated_text'] = row.last_updated ? moment(row.last_updated, "x").fromNow() : 'never'; - return row; - }; - async function ddnsLoadCurrentIp(){ try{ let res = await app.api.get('dns/dynamic/ip'); @@ -229,58 +223,76 @@
-
- - Dynamic A Records (WAN IP) - Current public IP: +
+ + Dynamic A Records + + This server's public IP: + +
+
-

- These A records are updated to this server's current public IP every 4 hours - (and immediately when added). Use @ as the subdomain for the domain apex. +

+ These A records are pointed at this server's public IP automatically — + every 4 hours and immediately when added. Enter @ as the host + for the domain apex.

-
-
- + + +
+
-
- - +
+ + +
-
- +
+
- - - - - - - - - - - - - -
HostCurrent IPLast updatedStatusActions
{{ fqdn }}{{ last_ip }}{{ last_updated_text }}{{ last_status }} - - -
+ +
+
+ +
+
+ {{ fqdn }} +
+
+ {{#last_ip}} + + {{ last_ip }} + updated recently + {{/last_ip}} + {{^last_ip}} + awaiting first update… + {{/last_ip}} + {{#last_status}} + {{ last_status }} + {{/last_status}} +
+
+ + + +
+