secubox-openwrt/package/secubox/luci-app-dns-master/htdocs/luci-static/resources/view/dns-master/overview.js
CyberMind-FR c2cd204ea9 feat(hexojs): Multi-instance enhancement with backup/restore and Git integration
- Add backup/restore commands to hexoctl (backup, restore, backup list/delete)
- Add GitHub clone support (hexoctl github clone <url> [instance] [branch])
- Add Gitea push support (hexoctl gitea push [instance] [message])
- Add quick-publish command (clean + build + publish in one step)
- Add 15 new RPCD methods for instance/backup/git management
- Rewrite LuCI dashboard with KISS theme:
  - Multi-instance management with status cards
  - Instance controls: start/stop, quick publish, backup, editor, preview
  - GitHub/Gitea clone modals
  - Backup table with restore/delete
  - Stats grid: instances, posts, drafts, backups
- Update API with 12 new RPC declarations
- Update ACL with new permissions

Also includes DNS Master app created in previous session.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 10:26:55 +01:00

645 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
'require view';
'require dom';
'require poll';
'require rpc';
'require ui';
var callStatus = rpc.declare({
object: 'luci.dns-master',
method: 'status',
expect: {}
});
var callZones = rpc.declare({
object: 'luci.dns-master',
method: 'zones',
expect: {}
});
var callRecords = rpc.declare({
object: 'luci.dns-master',
method: 'records',
params: ['zone'],
expect: {}
});
var callAddRecord = rpc.declare({
object: 'luci.dns-master',
method: 'add_record',
params: ['zone', 'type', 'name', 'value', 'ttl'],
expect: {}
});
var callDelRecord = rpc.declare({
object: 'luci.dns-master',
method: 'del_record',
params: ['zone', 'type', 'name', 'value'],
expect: {}
});
var callAddZone = rpc.declare({
object: 'luci.dns-master',
method: 'add_zone',
params: ['name'],
expect: {}
});
var callReload = rpc.declare({
object: 'luci.dns-master',
method: 'reload',
expect: {}
});
var callCheck = rpc.declare({
object: 'luci.dns-master',
method: 'check',
params: ['zone'],
expect: {}
});
var callBackup = rpc.declare({
object: 'luci.dns-master',
method: 'backup',
params: ['zone'],
expect: {}
});
// State
var currentZone = null;
var currentRecords = [];
var allZones = [];
var typeFilter = 'ALL';
// Record type definitions
var RECORD_TYPES = {
'A': { color: '#3fb950', label: 'IPv4 Address', placeholder: '192.168.1.1' },
'AAAA': { color: '#58a6ff', label: 'IPv6 Address', placeholder: '2001:db8::1' },
'CNAME': { color: '#39c5cf', label: 'Canonical Name', placeholder: 'target.example.com.' },
'MX': { color: '#a371f7', label: 'Mail Exchange', placeholder: '10 mail.example.com.' },
'TXT': { color: '#d29922', label: 'Text Record', placeholder: '"v=spf1 mx ~all"' },
'SRV': { color: '#db6d28', label: 'Service', placeholder: '0 0 443 target.example.com.' },
'NS': { color: '#8b949e', label: 'Nameserver', placeholder: 'ns1.example.com.' },
'PTR': { color: '#8b949e', label: 'Pointer', placeholder: 'host.example.com.' },
'CAA': { color: '#f85149', label: 'Cert Authority', placeholder: '0 issue "letsencrypt.org"' }
};
return view.extend({
load: function() {
return Promise.all([
callStatus(),
callZones()
]);
},
css: function() {
return `
/* KISS DNS Master */
:root {
--k-bg: #0d1117; --k-surface: #161b22; --k-card: #1c2128;
--k-line: #30363d; --k-text: #e6edf3; --k-muted: #8b949e;
--k-green: #3fb950; --k-red: #f85149; --k-blue: #58a6ff;
--k-cyan: #39c5cf; --k-purple: #a371f7; --k-yellow: #d29922;
--k-orange: #db6d28;
}
.dns-wrap { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: var(--k-text); }
.dns-header { margin-bottom: 24px; }
.dns-header h2 { margin: 0 0 4px 0; font-size: 26px; font-weight: 700; }
.dns-header p { color: var(--k-muted); margin: 0; font-size: 14px; }
/* Stats Grid */
.dns-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; margin-bottom: 20px; }
.dns-stat { background: var(--k-surface); border: 1px solid var(--k-line); border-radius: 10px; padding: 16px; text-align: center; }
.dns-stat-val { font-size: 28px; font-weight: 700; line-height: 1.2; }
.dns-stat-lbl { font-size: 11px; color: var(--k-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 4px; }
/* Cards */
.dns-card { background: var(--k-surface); border: 1px solid var(--k-line); border-radius: 10px; padding: 16px; margin-bottom: 16px; }
.dns-card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.dns-card-title { font-size: 13px; font-weight: 600; color: var(--k-muted); text-transform: uppercase; letter-spacing: 0.5px; display: flex; align-items: center; gap: 8px; }
.dns-card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
/* Buttons */
.dns-btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border-radius: 6px; border: 1px solid var(--k-line); background: var(--k-card); color: var(--k-text); font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.15s; }
.dns-btn:hover { background: var(--k-line); border-color: var(--k-muted); }
.dns-btn-sm { padding: 4px 10px; font-size: 11px; }
.dns-btn-green { background: var(--k-green); border-color: var(--k-green); color: #000; }
.dns-btn-green:hover { background: #2ea043; }
.dns-btn-blue { background: var(--k-blue); border-color: var(--k-blue); color: #000; }
.dns-btn-blue:hover { background: #4090e0; }
.dns-btn-red { background: transparent; border-color: var(--k-red); color: var(--k-red); }
.dns-btn-red:hover { background: rgba(248,81,73,0.15); }
.dns-btn-icon { padding: 5px 8px; }
/* Table */
.dns-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.dns-table th { text-align: left; padding: 10px 12px; font-size: 10px; font-weight: 600; color: var(--k-muted); text-transform: uppercase; border-bottom: 1px solid var(--k-line); }
.dns-table td { padding: 10px 12px; border-bottom: 1px solid var(--k-line); vertical-align: middle; }
.dns-table tr:hover td { background: rgba(255,255,255,0.02); }
.dns-table .mono { font-family: 'SF Mono', Monaco, monospace; font-size: 12px; }
.dns-table .truncate { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dns-table .actions { display: flex; gap: 6px; justify-content: flex-end; }
/* Badges */
.dns-badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; letter-spacing: 0.3px; }
.dns-badge-ok { background: rgba(63,185,80,0.15); color: var(--k-green); }
.dns-badge-err { background: rgba(248,81,73,0.15); color: var(--k-red); }
/* Type badge */
.dns-type { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 700; font-family: monospace; }
/* Filter bar */
.dns-filter { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; flex-wrap: wrap; }
.dns-filter-group { display: flex; gap: 4px; }
.dns-filter-btn { padding: 5px 10px; border-radius: 4px; border: 1px solid var(--k-line); background: transparent; color: var(--k-muted); font-size: 11px; cursor: pointer; transition: all 0.15s; }
.dns-filter-btn:hover { color: var(--k-text); border-color: var(--k-muted); }
.dns-filter-btn.active { background: var(--k-blue); border-color: var(--k-blue); color: #000; }
.dns-search { flex: 1; min-width: 150px; max-width: 300px; }
.dns-search input { width: 100%; padding: 6px 10px; border-radius: 5px; border: 1px solid var(--k-line); background: var(--k-bg); color: var(--k-text); font-size: 12px; }
.dns-search input:focus { outline: none; border-color: var(--k-blue); }
.dns-search input::placeholder { color: var(--k-muted); }
/* Zone selector */
.dns-zone-select { display: flex; gap: 8px; align-items: center; }
.dns-zone-select select { padding: 6px 10px; border-radius: 5px; border: 1px solid var(--k-line); background: var(--k-bg); color: var(--k-text); font-size: 12px; min-width: 180px; }
.dns-zone-select select:focus { outline: none; border-color: var(--k-blue); }
/* Modal */
.dns-modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.75); display: flex; align-items: center; justify-content: center; z-index: 9999; }
.dns-modal { background: var(--k-surface); border: 1px solid var(--k-line); border-radius: 12px; padding: 20px; width: 420px; max-width: 95vw; max-height: 90vh; overflow-y: auto; }
.dns-modal-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
.dns-modal-close { margin-left: auto; background: none; border: none; color: var(--k-muted); font-size: 18px; cursor: pointer; padding: 4px; }
.dns-modal-close:hover { color: var(--k-text); }
/* Form */
.dns-form-row { margin-bottom: 14px; }
.dns-form-label { display: block; font-size: 11px; color: var(--k-muted); margin-bottom: 5px; text-transform: uppercase; }
.dns-form-input { width: 100%; padding: 8px 10px; border-radius: 5px; border: 1px solid var(--k-line); background: var(--k-bg); color: var(--k-text); font-size: 13px; box-sizing: border-box; }
.dns-form-input:focus { outline: none; border-color: var(--k-blue); }
.dns-form-hint { font-size: 11px; color: var(--k-muted); margin-top: 4px; }
.dns-form-row-inline { display: flex; gap: 8px; align-items: center; }
.dns-form-suffix { color: var(--k-muted); font-size: 12px; white-space: nowrap; }
.dns-form-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--k-line); }
/* Empty state */
.dns-empty { text-align: center; padding: 40px 20px; color: var(--k-muted); }
.dns-empty-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.5; }
.dns-empty-text { font-size: 13px; }
/* Two column layout */
.dns-cols { display: grid; grid-template-columns: 1fr 2fr; gap: 16px; }
@media (max-width: 900px) { .dns-cols { grid-template-columns: 1fr; } }
`;
},
statCard: function(label, value, color, id) {
return E('div', { 'class': 'dns-stat' }, [
E('div', { 'class': 'dns-stat-val', 'style': 'color:' + color, 'data-stat': id }, String(value)),
E('div', { 'class': 'dns-stat-lbl' }, label)
]);
},
render: function(data) {
var self = this;
var status = data[0] || {};
var zonesData = data[1] || {};
allZones = zonesData.zones || [];
poll.add(function() {
return Promise.all([callStatus(), callZones()]).then(function(d) {
self.updateStats(d[0]);
allZones = (d[1] || {}).zones || [];
self.updateZonesTable();
});
}, 15);
var isRunning = status.running === true;
return E('div', { 'class': 'dns-wrap' }, [
E('style', {}, this.css()),
// Header
E('div', { 'class': 'dns-header' }, [
E('h2', {}, '🌐 DNS Master'),
E('p', {}, 'BIND DNS Zone Management')
]),
// Stats
E('div', { 'class': 'dns-stats' }, [
this.statCard('Status', isRunning ? 'Running' : 'Stopped', isRunning ? 'var(--k-green)' : 'var(--k-red)', 'status'),
this.statCard('Zones', status.zones || 0, 'var(--k-blue)', 'zones'),
this.statCard('Records', status.records || 0, 'var(--k-cyan)', 'records'),
this.statCard('TTL', (status.default_ttl || 300) + 's', 'var(--k-purple)', 'ttl')
]),
// Two column layout
E('div', { 'class': 'dns-cols' }, [
// Left: Zones
E('div', { 'class': 'dns-card' }, [
E('div', { 'class': 'dns-card-head' }, [
E('div', { 'class': 'dns-card-title' }, '📁 Zones'),
E('button', { 'class': 'dns-btn dns-btn-green dns-btn-sm', 'click': L.bind(this.showAddZoneModal, this) }, '+ Add')
]),
E('div', { 'id': 'zones-list' }, this.renderZonesList(allZones))
]),
// Right: Records
E('div', { 'class': 'dns-card' }, [
E('div', { 'class': 'dns-card-head' }, [
E('div', { 'class': 'dns-card-title' }, [
E('span', {}, '📝 Records'),
E('span', { 'id': 'current-zone-label', 'style': 'color: var(--k-blue); font-weight: 400;' }, '')
]),
E('div', { 'class': 'dns-card-actions' }, [
E('button', { 'class': 'dns-btn dns-btn-sm', 'click': L.bind(this.handleReload, this) }, '🔄 Reload'),
E('button', { 'class': 'dns-btn dns-btn-green dns-btn-sm', 'id': 'add-record-btn', 'style': 'display:none;', 'click': L.bind(this.showRecordModal, this, null) }, '+ Add')
])
]),
E('div', { 'id': 'records-filter', 'style': 'display:none;' }, this.renderFilter()),
E('div', { 'id': 'records-container' }, this.renderEmptyRecords())
])
])
]);
},
renderZonesList: function(zones) {
var self = this;
if (!zones || zones.length === 0) {
return E('div', { 'class': 'dns-empty' }, [
E('div', { 'class': 'dns-empty-icon' }, '📂'),
E('div', { 'class': 'dns-empty-text' }, 'No zones configured')
]);
}
return E('div', {}, zones.map(function(zone) {
var isActive = currentZone === zone.name;
return E('div', {
'style': 'display: flex; align-items: center; padding: 10px; border-radius: 6px; margin-bottom: 6px; cursor: pointer; border: 1px solid ' + (isActive ? 'var(--k-blue)' : 'transparent') + '; background: ' + (isActive ? 'rgba(88,166,255,0.1)' : 'var(--k-card)') + ';',
'click': L.bind(self.selectZone, self, zone.name)
}, [
E('div', { 'style': 'flex: 1; min-width: 0;' }, [
E('div', { 'style': 'font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis;' }, zone.name),
E('div', { 'style': 'font-size: 11px; color: var(--k-muted); margin-top: 2px;' }, zone.records + ' records')
]),
E('div', { 'style': 'display: flex; align-items: center; gap: 6px;' }, [
zone.valid ?
E('span', { 'class': 'dns-badge dns-badge-ok' }, '✓') :
E('span', { 'class': 'dns-badge dns-badge-err' }, '✗'),
E('button', {
'class': 'dns-btn dns-btn-icon dns-btn-sm',
'title': 'Backup',
'click': function(ev) { ev.stopPropagation(); self.handleBackupZone(zone.name); }
}, '💾')
])
]);
}));
},
renderEmptyRecords: function() {
return E('div', { 'class': 'dns-empty' }, [
E('div', { 'class': 'dns-empty-icon' }, '👈'),
E('div', { 'class': 'dns-empty-text' }, 'Select a zone to view records')
]);
},
renderFilter: function() {
var self = this;
var types = ['ALL'].concat(Object.keys(RECORD_TYPES));
return E('div', { 'class': 'dns-filter' }, [
E('div', { 'class': 'dns-filter-group' }, types.map(function(t) {
return E('button', {
'class': 'dns-filter-btn' + (typeFilter === t ? ' active' : ''),
'data-type': t,
'click': function() { self.setTypeFilter(t); }
}, t);
})),
E('div', { 'class': 'dns-search' }, [
E('input', {
'type': 'text',
'placeholder': '🔍 Search records...',
'id': 'record-search',
'input': L.bind(this.filterRecords, this)
})
])
]);
},
renderRecordsTable: function(records) {
var self = this;
var search = (document.getElementById('record-search') || {}).value || '';
search = search.toLowerCase();
var filtered = records.filter(function(r) {
if (typeFilter !== 'ALL' && r.type !== typeFilter) return false;
if (search && r.name.toLowerCase().indexOf(search) === -1 && r.value.toLowerCase().indexOf(search) === -1) return false;
return true;
});
if (filtered.length === 0) {
return E('div', { 'class': 'dns-empty' }, [
E('div', { 'class': 'dns-empty-icon' }, '🔍'),
E('div', { 'class': 'dns-empty-text' }, typeFilter !== 'ALL' || search ? 'No matching records' : 'No records in this zone')
]);
}
return E('table', { 'class': 'dns-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', { 'style': 'width: 60px;' }, 'Type'),
E('th', {}, 'Name'),
E('th', {}, 'Value'),
E('th', { 'style': 'width: 50px;' }, 'TTL'),
E('th', { 'style': 'width: 80px; text-align: right;' }, '')
])
]),
E('tbody', {}, filtered.map(function(rec) {
var typeInfo = RECORD_TYPES[rec.type] || { color: 'var(--k-muted)' };
// Clean up value display (remove extra "IN TYPE" if present)
var displayValue = rec.value.replace(/^\s*IN\s+\w+\s+/, '').trim();
return E('tr', {}, [
E('td', {}, [
E('span', {
'class': 'dns-type',
'style': 'background: ' + typeInfo.color + '20; color: ' + typeInfo.color + ';'
}, rec.type)
]),
E('td', { 'class': 'mono' }, rec.name),
E('td', { 'class': 'mono truncate', 'title': displayValue }, displayValue),
E('td', { 'style': 'color: var(--k-muted);' }, rec.ttl || '-'),
E('td', { 'class': 'actions' }, [
E('button', {
'class': 'dns-btn dns-btn-icon dns-btn-sm',
'title': 'Edit',
'click': L.bind(self.showRecordModal, self, rec)
}, '✏️'),
E('button', {
'class': 'dns-btn dns-btn-icon dns-btn-sm dns-btn-red',
'title': 'Delete',
'click': L.bind(self.handleDeleteRecord, self, rec)
}, '✗')
])
]);
}))
]);
},
selectZone: function(zoneName) {
var self = this;
currentZone = zoneName;
typeFilter = 'ALL';
document.getElementById('current-zone-label').textContent = ': ' + zoneName;
document.getElementById('add-record-btn').style.display = '';
document.getElementById('records-filter').style.display = '';
callRecords(zoneName).then(function(data) {
currentRecords = data.records || [];
self.updateRecordsTable();
self.updateZonesTable();
});
},
setTypeFilter: function(type) {
typeFilter = type;
document.querySelectorAll('.dns-filter-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.type === type);
});
this.updateRecordsTable();
},
filterRecords: function() {
this.updateRecordsTable();
},
updateStats: function(status) {
var isRunning = status.running === true;
var updates = {
'status': { val: isRunning ? 'Running' : 'Stopped', color: isRunning ? 'var(--k-green)' : 'var(--k-red)' },
'zones': { val: status.zones || 0 },
'records': { val: status.records || 0 },
'ttl': { val: (status.default_ttl || 300) + 's' }
};
Object.keys(updates).forEach(function(k) {
var el = document.querySelector('[data-stat="' + k + '"]');
if (el) {
el.textContent = updates[k].val;
if (updates[k].color) el.style.color = updates[k].color;
}
});
},
updateZonesTable: function() {
var container = document.getElementById('zones-list');
if (container) dom.content(container, this.renderZonesList(allZones));
},
updateRecordsTable: function() {
var container = document.getElementById('records-container');
if (container) dom.content(container, this.renderRecordsTable(currentRecords));
},
// === Modals ===
showAddZoneModal: function() {
var self = this;
var modal = E('div', { 'class': 'dns-modal-bg', 'id': 'modal-zone' }, [
E('div', { 'class': 'dns-modal' }, [
E('div', { 'class': 'dns-modal-title' }, [
'📁 Add Zone',
E('button', { 'class': 'dns-modal-close', 'click': function() { document.getElementById('modal-zone').remove(); } }, '×')
]),
E('div', { 'class': 'dns-form-row' }, [
E('label', { 'class': 'dns-form-label' }, 'Zone Name'),
E('input', { 'type': 'text', 'class': 'dns-form-input', 'id': 'input-zone-name', 'placeholder': 'example.com' }),
E('div', { 'class': 'dns-form-hint' }, 'Enter the domain name for this zone')
]),
E('div', { 'class': 'dns-form-actions' }, [
E('button', { 'class': 'dns-btn', 'click': function() { document.getElementById('modal-zone').remove(); } }, 'Cancel'),
E('button', { 'class': 'dns-btn dns-btn-green', 'click': L.bind(self.handleAddZone, self) }, '+ Create Zone')
])
])
]);
document.body.appendChild(modal);
document.getElementById('input-zone-name').focus();
},
showRecordModal: function(record) {
var self = this;
var isEdit = !!record;
var title = isEdit ? '✏️ Edit Record' : ' Add Record';
var typeOptions = Object.keys(RECORD_TYPES).map(function(t) {
var info = RECORD_TYPES[t];
return E('option', { 'value': t, 'selected': isEdit && record.type === t }, t + ' - ' + info.label);
});
var modal = E('div', { 'class': 'dns-modal-bg', 'id': 'modal-record' }, [
E('div', { 'class': 'dns-modal' }, [
E('div', { 'class': 'dns-modal-title' }, [
title,
E('button', { 'class': 'dns-modal-close', 'click': function() { document.getElementById('modal-record').remove(); } }, '×')
]),
E('div', { 'class': 'dns-form-row' }, [
E('label', { 'class': 'dns-form-label' }, 'Type'),
E('select', {
'class': 'dns-form-input',
'id': 'input-rec-type',
'disabled': isEdit,
'change': L.bind(this.updatePlaceholder, this)
}, typeOptions)
]),
E('div', { 'class': 'dns-form-row' }, [
E('label', { 'class': 'dns-form-label' }, 'Name'),
E('div', { 'class': 'dns-form-row-inline' }, [
E('input', {
'type': 'text',
'class': 'dns-form-input',
'id': 'input-rec-name',
'placeholder': '@ or www',
'value': isEdit ? record.name : '',
'style': 'flex: 1;'
}),
E('span', { 'class': 'dns-form-suffix' }, '.' + currentZone)
]),
E('div', { 'class': 'dns-form-hint' }, 'Use @ for zone root')
]),
E('div', { 'class': 'dns-form-row' }, [
E('label', { 'class': 'dns-form-label' }, 'Value'),
E('input', {
'type': 'text',
'class': 'dns-form-input',
'id': 'input-rec-value',
'placeholder': isEdit ? '' : RECORD_TYPES['A'].placeholder,
'value': isEdit ? record.value.replace(/^\s*IN\s+\w+\s+/, '').trim() : ''
}),
E('div', { 'class': 'dns-form-hint', 'id': 'value-hint' }, isEdit ? '' : RECORD_TYPES['A'].label)
]),
E('div', { 'class': 'dns-form-row' }, [
E('label', { 'class': 'dns-form-label' }, 'TTL (seconds)'),
E('input', {
'type': 'number',
'class': 'dns-form-input',
'id': 'input-rec-ttl',
'placeholder': '300',
'value': isEdit && record.ttl ? record.ttl : '',
'style': 'width: 120px;'
})
]),
E('div', { 'class': 'dns-form-actions' }, [
E('button', { 'class': 'dns-btn', 'click': function() { document.getElementById('modal-record').remove(); } }, 'Cancel'),
E('button', {
'class': 'dns-btn dns-btn-green',
'click': L.bind(self.handleSaveRecord, self, record)
}, isEdit ? '💾 Save' : '+ Add')
])
])
]);
document.body.appendChild(modal);
if (!isEdit) document.getElementById('input-rec-name').focus();
},
updatePlaceholder: function() {
var type = document.getElementById('input-rec-type').value;
var info = RECORD_TYPES[type] || { placeholder: '', label: '' };
document.getElementById('input-rec-value').placeholder = info.placeholder;
document.getElementById('value-hint').textContent = info.label;
},
// === Handlers ===
handleAddZone: function() {
var self = this;
var name = document.getElementById('input-zone-name').value.trim();
if (!name) {
ui.addNotification(null, E('p', {}, 'Zone name required'), 'warning');
return;
}
callAddZone(name).then(function(res) {
document.getElementById('modal-zone').remove();
if (res.code === 0) {
ui.addNotification(null, E('p', {}, 'Zone created: ' + name), 'success');
callZones().then(function(d) {
allZones = (d || {}).zones || [];
self.updateZonesTable();
self.selectZone(name);
});
} else {
ui.addNotification(null, E('p', {}, res.output || 'Error'), 'error');
}
});
},
handleSaveRecord: function(oldRecord) {
var self = this;
var type = document.getElementById('input-rec-type').value;
var name = document.getElementById('input-rec-name').value.trim() || '@';
var value = document.getElementById('input-rec-value').value.trim();
var ttl = document.getElementById('input-rec-ttl').value.trim();
if (!value) {
ui.addNotification(null, E('p', {}, 'Value required'), 'warning');
return;
}
var doAdd = function() {
callAddRecord(currentZone, type, name, value, ttl ? parseInt(ttl) : null).then(function(res) {
document.getElementById('modal-record').remove();
if (res.code === 0) {
ui.addNotification(null, E('p', {}, 'Record saved'), 'success');
self.selectZone(currentZone);
} else {
ui.addNotification(null, E('p', {}, res.output || 'Error'), 'error');
}
});
};
// If editing, delete old record first
if (oldRecord) {
callDelRecord(currentZone, oldRecord.type, oldRecord.name, oldRecord.value).then(doAdd);
} else {
doAdd();
}
},
handleDeleteRecord: function(record) {
var self = this;
if (!confirm('Delete ' + record.type + ' record for "' + record.name + '"?')) return;
callDelRecord(currentZone, record.type, record.name, record.value).then(function(res) {
if (res.code === 0) {
ui.addNotification(null, E('p', {}, 'Record deleted'), 'success');
self.selectZone(currentZone);
} else {
ui.addNotification(null, E('p', {}, res.output || 'Error'), 'error');
}
});
},
handleReload: function() {
callReload().then(function(res) {
if (res.code === 0) {
ui.addNotification(null, E('p', {}, 'BIND reloaded'), 'success');
} else {
ui.addNotification(null, E('p', {}, res.output || 'Reload failed'), 'error');
}
});
},
handleBackupZone: function(zoneName) {
callBackup(zoneName).then(function(res) {
if (res.code === 0) {
ui.addNotification(null, E('p', {}, 'Zone backed up: ' + zoneName), 'success');
} else {
ui.addNotification(null, E('p', {}, res.output || 'Backup failed'), 'error');
}
});
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});