diff --git a/config.go b/config.go
index d98f201..34df751 100644
--- a/config.go
+++ b/config.go
@@ -3,10 +3,7 @@ package main
import (
// Standard
"encoding/json"
- "fmt"
"os"
- "slices"
- "strings"
)
type Config struct {
@@ -22,18 +19,13 @@ type Config struct {
LogDir string
}
- Devices []Device
-
- filename string
-}
-
-type Device struct {
- Name string
- Address string
- Port int
- Username string
- Password string
- Timeout int
+ Device struct {
+ Address string
+ Port int
+ Username string
+ Password string
+ Timeout int
+ }
}
func readConfig() (config Config, err error) {
@@ -44,64 +36,5 @@ func readConfig() (config Config, err error) {
}
err = json.Unmarshal(configData, &config)
- config.filename = flagConfig
- return
-}
-
-func (cfg *Config) UpdateDevice(currentName string, deviceToUpdate Device) (dev Device, err error) {
-
- i := slices.IndexFunc(cfg.Devices, func(d Device) bool {
- return strings.TrimSpace(strings.ToLower(d.Name)) == strings.TrimSpace(strings.ToLower(currentName))
- })
-
- if i > -1 {
- dev = cfg.Devices[i]
- }
-
- if strings.TrimSpace(deviceToUpdate.Name) == "" {
- err = fmt.Errorf("Name can't be empty")
- return
- }
- if strings.TrimSpace(deviceToUpdate.Address) == "" {
- err = fmt.Errorf("Address can't be empty")
- return
- }
- if strings.TrimSpace(deviceToUpdate.Username) == "" {
- err = fmt.Errorf("Username can't be empty")
- return
- }
- if deviceToUpdate.Port < 1 || deviceToUpdate.Port > 65535 {
- err = fmt.Errorf("Invalid port")
- return
- }
-
- dev.Name = strings.TrimSpace(strings.ToLower(deviceToUpdate.Name))
- dev.Address = strings.TrimSpace(strings.ToLower(deviceToUpdate.Address))
- dev.Port = deviceToUpdate.Port
- dev.Username = strings.TrimSpace(deviceToUpdate.Username)
-
- // TODO - Should be configurable...
- if dev.Timeout == 0 {
- dev.Timeout = 10
- }
-
- // Device not found - create it!
- if i == -1 {
- if strings.TrimSpace(deviceToUpdate.Password) == "" {
- err = fmt.Errorf("Password can't be empty")
- return
- }
- dev.Password = strings.TrimSpace(deviceToUpdate.Password)
- cfg.Devices = append(cfg.Devices, dev)
- } else {
- if deviceToUpdate.Password != "" {
- dev.Password = strings.TrimSpace(deviceToUpdate.Password)
- }
- cfg.Devices[i] = dev
- }
-
- j, _ := json.Marshal(cfg)
- err = os.WriteFile(cfg.filename, j, 0600)
-
return
}
diff --git a/main.go b/main.go
index 6be7b38..2f7fbd1 100644
--- a/main.go
+++ b/main.go
@@ -9,8 +9,6 @@ import (
"log/slog"
"os"
"path"
- "slices"
- "fmt"
)
const VERSION = "v1"
@@ -25,7 +23,7 @@ var (
initLogger *slog.Logger
logger *slog.Logger
- devices map[string]RouterosDevice
+ device RouterosDevice
)
func init() { // {{{
@@ -75,45 +73,13 @@ func initLogging(config Config) *slog.Logger { // {{{
func main() {
initLogger.Info("application", "version", VERSION)
- devices = make(map[string]RouterosDevice)
- /*
device.Host = config.Device.Address
device.Port = config.Device.Port
device.Username = config.Device.Username
device.Password = config.Device.Password
device.Timeout = config.Device.Timeout
device.Init()
- */
registerWebserverHandlers()
startWebserver()
}
-
-func routerosDevice(name string) (dev RouterosDevice, err error) {
- var found bool
- if dev, found = devices[name]; found {
- logger.Debug("routeros", "op", "connection", "name", name, "cached", true)
- return
- }
-
- i := slices.IndexFunc(config.Devices, func(d Device) bool {
- return d.Name == name
- })
- if i == -1 {
- err = fmt.Errorf("Unknown device '%s'", name)
- logger.Error("routeros", "op", "connection", "error", err)
- return
- }
-
- logger.Debug("routeros", "name", name, "cached", false)
- confDev := config.Devices[i]
- dev.Host = confDev.Address
- dev.Port = confDev.Port
- dev.Username = confDev.Username
- dev.Password = confDev.Password
- dev.Timeout = confDev.Timeout
- dev.Init()
- devices[name] = dev
-
- return
-}
diff --git a/routeros_device.go b/routeros_device.go
index 93e5738..50c19d1 100644
--- a/routeros_device.go
+++ b/routeros_device.go
@@ -58,7 +58,6 @@ 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) { // {{{
- logger.Debug("FOO", "port", dev.Port)
url := fmt.Sprintf("https://%s:%d/rest%s", dev.Host, dev.Port, path)
logger.Info("URL", "method", method, "url", url)
@@ -151,7 +150,7 @@ func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (entry DNSEntry, err
err = json.Unmarshal(body, &entry)
return
}// }}}
-func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {// {{{
+func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {
_, err = dev.query("DELETE", "/ip/dns/static/"+id, []byte{})
if err != nil {
rosError := struct{ Detail string }{}
@@ -163,7 +162,7 @@ func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {// {{{
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 407ae3a..ba6c2a1 100644
--- a/static/css/index.css
+++ b/static/css/index.css
@@ -1,7 +1,4 @@
:root {
- --header-background: #e1eabe;
- --header-border: 1px solid #869a41;
-
--line-color: #ccc;
--line-color-record: #eee;
@@ -23,7 +20,7 @@
--record-NXDOMAIN: #aa0000;
--record-other: #888;
- --record-hover: #fafafa;
+ --record-hover: #fffff4;
}
html {
@@ -35,35 +32,19 @@ html {
*:after {
box-sizing: inherit;
}
-*:focus {
- outline: none;
-}
[onClick] {
cursor: pointer;
}
-h1 {
- font-size: 1.5em;
-
- &:first-child {
- margin-top: 0px;
- }
-}
-
label {
user-select: none;
}
-html,
-body {
- margin: 0px;
- padding: 0px;
-}
-
body {
font-family: sans-serif;
font-size: 12pt;
+ margin-left: 32px;
/* Boxed folders are a settings for the user. */
&.boxed-folders {
@@ -107,79 +88,30 @@ select,
button {
font-size: 1em;
padding: 4px 8px;
- border-radius: 4px;
- border: 1px solid #444;
}
-#application-header {
- display: grid;
- grid-template-columns: min-content min-content 1fr min-content;
- align-items: center;
- justify-items: end;
- border-bottom: var(--header-border);
- background-color: var(--header-background);
+#create-icon {
+ position: absolute;
+ top: 18px;
+ right: 64px;
+ width: 24px;
+ cursor: pointer;
+}
- .device-select {
- display: grid;
- grid-template-columns: min-content min-content;
- align-items: center;
-
- padding: 16px;
- border-right: var(--header-border);
-
- .label {
- grid-column: 1 / -1;
- font-weight: bold;
- }
-
- img {
- margin-left: 8px;
- }
- }
-
- #search {
- padding: 16px;
- display: grid;
- grid-template-columns: min-content min-content;
- grid-gap: 0px 8px;
-
-
- &>* {
- display: none;
- }
-
- &.show {
- border-right: var(--header-border);
- &>* {
- display: initial;
- }
- }
-
- .search-label {
- grid-column: 1 / -1;
- font-weight: bold;
- }
- }
-
- #create-icon {
- width: 24px;
- cursor: pointer;
- margin-right: 16px;
- }
-
- #settings-icon {
- width: 32px;
- cursor: pointer;
- margin-right: 16px;
- }
+#settings-icon {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ width: 32px;
+ cursor: pointer;
+}
+#search {
+ margin-bottom: 16px;
}
#records-tree {
white-space: nowrap;
- margin-top: calc(32px - 5px);
- /* padding from the topmost folder */
- margin-left: 32px;
.folder {
padding-left: 32px;
@@ -188,7 +120,7 @@ button {
padding-left: 0px;
}
- &.no-domain>.label {
+ &.no-domain > .label {
font-style: italic;
}
@@ -287,10 +219,6 @@ button {
padding-left: 8px;
border-bottom: 1px solid var(--header-line);
- &.mouse-over {
- background-color: var(--record-hover);
- }
-
img {
display: block;
padding-left: 4px;
@@ -354,8 +282,6 @@ button {
border-left: 1px solid var(--header-line);
align-content: center;
cursor: pointer;
- display: grid;
- justify-items: center;
&.mouse-over {
background-color: var(--record-hover);
@@ -474,49 +400,3 @@ button {
margin-top: 8px;
}
}
-
-#device-dialog {
- display: grid;
- grid-template-columns: min-content min-content;
- align-items: top;
- grid-gap: 16px;
-
- .devices {
- display: flex;
- flex-direction:column;
- gap: 8px;
- align-items: flex-start;
-
- .header {
- font-weight: bold;
- }
-
- .device {
- border: 1px solid #aaa;
- background-color: #f0f0f0;
- padding: 8px 16px;
- border-radius: 8px;
- cursor: pointer;
- user-select: none;
-
- &.selected {
- background-color: var(--header-background);
- }
- }
- }
-
- .fields {
- display: grid;
- grid-template-columns: min-content 1fr;
- grid-gap: 8px 16px;
- align-items: center;
-
- border: 1px solid #aaa;
- padding: 16px;
-
- .actions {
- grid-column: 1 / -1;
- justify-self: right;
- }
- }
-}
diff --git a/static/images/icon_create.svg b/static/images/icon_create.svg
index e91d683..cc1dbd4 100644
--- a/static/images/icon_create.svg
+++ b/static/images/icon_create.svg
@@ -25,9 +25,9 @@
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="5.6568542"
- inkscape:cx="-20.948038"
- inkscape:cy="9.0156115"
+ inkscape:zoom="1"
+ inkscape:cx="-71"
+ inkscape:cy="-52"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
@@ -62,6 +62,6 @@
+ style="stroke-width:0.264583;fill:#89a02c" />
diff --git a/static/js/dns.mjs b/static/js/dns.mjs
index 707b6ed..8f9195d 100644
--- a/static/js/dns.mjs
+++ b/static/js/dns.mjs
@@ -1,109 +1,26 @@
import { MessageBus } from '@mbus'
export class Application {
- constructor() {// {{{
+ constructor(records) {// {{{
window._mbus = new MessageBus()
this.settings = new Settings()
- this.devices = new Map()
- this.currentDevice = null
-
+ this.records = this.parseRecords(records)
+ this.topFolder = new Folder(this, null, 'root')
+ this.recordsTree = null
this.settingsIcon = null
this.createIcon = null
- this.elHeader = null
- this.deviceSelector = null
- this.searchWidget = null
- this.resetDeviceState()
- this.renderApplication()
- this.retrieveDevices()
- .then(() => {
- this.deviceSelector.setDevices(this.devices)
- this.renderApplication()
- })
- .catch(err => {
- console.error(err)
- alert(err)
- })
+ this.searchFor = ''
- _mbus.subscribe('device_selected', event => this.connectDevice(event.detail.devName))
- _mbus.subscribe('search', ()=>this.search())
+ this.renderFolders()
+ this.render()
}// }}}
-
- // resetDeviceState removes the device state such as the records, records tree and search field.
- resetDeviceState() {// {{{
- if (this.recordsTree) {
- this.recordsTree.remove()
- this.recordsTree = null
- return
- }
-
- this.topFolder = new Folder(this, null, 'root')
- this.records = null
- this.recordsTree = null
-
- if (this.searchWidget)
- this.searchWidget.reset()
- }// }}}
-
- // retrieveDevices fetches the preconfigured devices from server and populates this.devices.
- retrieveDevices() {// {{{
- return new Promise((resolve, reject) => {
- fetch('/devices')
- .then(data => data.json())
- .then(json => {
- if (!json.OK) {
- reject(json.Error)
- return
- }
-
- this.devices = new Map()
- for (const devData of json.Devices) {
- const dev = new Device(devData)
- this.devices.set(dev.name(), dev)
- }
-
- resolve()
- })
- })
- }// }}}
-
- // connectDevice resets the device state, retrieves the records and renders everything necessary.
- async connectDevice(devName) {// {{{
- this.resetDeviceState()
- if (devName == '-') {
- this.settings.set('last_device', '')
- return
- }
-
- try {
- const dev = this.devices.get(devName)
- if (dev === undefined) {
- alert(`Unknown device '${devName}'`)
- return
- }
- this.currentDevice = dev
-
- const records = await this.currentDevice.retrieveRecords()
- this.records = this.parseRecords(records)
-
- this.settings.set('last_device', this.currentDevice.name())
-
- this.createFolders()
- this.renderDevice()
- } catch (err) {
- console.error(err)
- alert(err)
- }
- }// }}}
-
- // filteredRecords takes the current search value and only returns records matching it.
filteredRecords() {// {{{
- const searchFor = this.searchWidget.value()
- if (searchFor === '')
+ if (this.searchFor === '')
return this.records
const records = this.records.filter(r => {
- return (r.name().includes(searchFor))
+ return (r.name().includes(this.searchFor))
})
return records
}// }}}
@@ -113,7 +30,7 @@ export class Application {
}// }}}
// cleanFolders removes all records from all folders.
- // createFolders can then put moved records in the correct
+ // renderFolders can then put moved records in the correct
// (or newly created) folders again when record names are updated.
cleanFolders(folder) {// {{{
if (folder === undefined)
@@ -128,99 +45,7 @@ export class Application {
const i = this.records.findIndex(rec => rec.id() == id)
this.records.splice(i, 1)
}// }}}
- sortFolders(a, b) {// {{{
- const aLabels = a.labels().reverse()
- const bLabels = b.labels().reverse()
-
- for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
- if (aLabels[i] < bLabels[i])
- return -1
- if (aLabels[i] > bLabels[i])
- return 1
- }
-
- if (a.length < b.length) return 1
- if (a.length > b.length) return -1
-
- return 0
- }// }}}
- sortRecords(a, b) {// {{{
- const aLabels = a.labels().reverse()
- const bLabels = b.labels().reverse()
-
- for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
- if (aLabels[i] < bLabels[i])
- return -1
- if (aLabels[i] > bLabels[i])
- return 1
- }
-
- return 0
- }// }}}
-
- // renderApplication creates and updates application-level elements (not device level elements).
- renderApplication() {// {{{
- if (this.elHeader === null) {
- this.elHeader = document.createElement('div')
- this.elHeader.id = 'application-header'
-
- this.deviceSelector = new DeviceSelectWidget(this.devices)
- this.searchWidget = new SearchWidget()
-
- this.createIcon = document.createElement('img')
- this.createIcon.id = 'create-icon'
- this.createIcon.src = `/images/${_VERSION}/icon_create.svg`
- this.createIcon.addEventListener('click', () => new RecordDialog(new Record()).show())
-
- this.settingsIcon = document.createElement('img')
- this.settingsIcon.id = 'settings-icon'
- this.settingsIcon.src = `/images/${_VERSION}/icon_settings.svg`
- this.settingsIcon.addEventListener('click', () => new SettingsDialog(this).show())
-
- this.elHeader.appendChild(this.deviceSelector.render())
- this.elHeader.appendChild(this.searchWidget.render())
- this.elHeader.appendChild(this.createIcon)
- this.elHeader.appendChild(this.settingsIcon)
-
- document.getElementById('app').appendChild(this.elHeader)
- document.body.addEventListener('keydown', event => this.handlerKeys(event))
- return
- }
-
- this.deviceSelector.render()
- const lastDevice = this.settings.get('last_device')
- if (lastDevice !== '')
- this.deviceSelector.selectDevice(lastDevice)
- }// }}}
-
- // renderDevice creates the device specific elements and also updates them.
- renderDevice() {// {{{
- // 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.getElementById('app').appendChild(this.recordsTree)
- }
-
- // Top root folder doesn't have to be shown.
- const folders = Array.from(this.topFolder.subfolders.values())
- folders.sort(this.sortFolders)
-
- 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'))
- }// }}}
-
- // createFolders goes through the filtered records and create new folders based on the record labels.
- // It also populates the child records.
- createFolders() {// {{{
+ renderFolders() {// {{{
const records = this.filteredRecords()
records.sort(this.sortRecords)
@@ -260,9 +85,91 @@ export class Application {
currFolder.addRecord(rec)
}
}// }}}
+ sortFolders(a, b) {// {{{
+ const aLabels = a.labels().reverse()
+ const bLabels = b.labels().reverse()
- // removeEmptyFolders finds the leaf folders and recursively removes empty ones.
- // These are usually left from moving records away from them.
+ for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
+ if (aLabels[i] < bLabels[i])
+ return -1
+ if (aLabels[i] > bLabels[i])
+ return 1
+ }
+
+ if (a.length < b.length) return 1
+ if (a.length > b.length) return -1
+
+ return 0
+ }// }}}
+ sortRecords(a, b) {// {{{
+ const aLabels = a.labels().reverse()
+ const bLabels = b.labels().reverse()
+
+ for (let i = 0; i < aLabels.length && i < bLabels.length; i++) {
+ if (aLabels[i] < bLabels[i])
+ return -1
+ if (aLabels[i] > bLabels[i])
+ return 1
+ }
+
+ return 0
+ }// }}}
+ render() {// {{{
+ if (this.createIcon === null) {
+ this.createIcon = document.createElement('img')
+ this.createIcon.id = 'create-icon'
+ this.createIcon.src = `/images/${_VERSION}/icon_create.svg`
+ this.createIcon.addEventListener('click', () => new RecordDialog(new Record()).show())
+
+ this.settingsIcon = document.createElement('img')
+ this.settingsIcon.id = 'settings-icon'
+ 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())
+
+ 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(this.recordsTree)
+ }
+
+ // Top root folder doesn't have to be shown.
+ const folders = Array.from(this.topFolder.subfolders.values())
+ folders.sort(this.sortFolders)
+
+ 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
@@ -282,7 +189,6 @@ export class Application {
folder.div?.remove()
}
}// }}}
-
handlerKeys(event) {// {{{
let handled = true
@@ -300,11 +206,7 @@ export class Application {
break
case 'f':
- this.searchWidget.searchField.focus()
- break
-
- case 'd':
- new DeviceDialog(this.devices).render()
+ this.searchField.focus()
break
default:
@@ -327,145 +229,15 @@ export class Application {
else
document.body.classList.remove('boxed-folders')
}// }}}
-
- // search sets the search filter and re-renders the records tree.
search() {// {{{
+ this.searchFor = this.searchField.value.trim().toLowerCase()
this.recordsTree.remove()
this.topFolder = new Folder(this, null, 'root')
this.recordsTree = null
- this.createFolders()
- this.topFolder.openFolder(true)
- this.renderDevice()
- }// }}}
-}
-
-class SearchWidget {
- constructor() {// {{{
- _mbus.subscribe('device_selected', event => {
- if (event.detail.devName == '-')
- this.searchEl?.classList.remove('show')
- else
- this.searchEl?.classList.add('show')
- })
- }// }}}
- render() {// {{{
- if (this.searchEl)
- return
-
- this.searchEl = document.createElement('div')
- this.searchEl.id = 'search'
- this.searchEl.innerHTML = `
-
Search
-
-
- `
- this.searchField = this.searchEl.querySelector('.search-for')
- this.searchField.addEventListener('keydown', event => {
- if (event.key == 'Enter')
- this.search()
- })
-
- const searchButton = this.searchEl.querySelector('button.search')
- searchButton.addEventListener('click', () => this.search())
-
- return this.searchEl
- }// }}}
- search() {// {{{
- _mbus.dispatch('search', { search: this.searchField.value.trim().toLowerCase() })
- }// }}}
- value() {// {{{
- return this.searchField.value.trim().toLowerCase()
- }// }}}
- reset() {// {{{
- this.searchField.value = ''
- }// }}}
-}
-
-class Device {
- constructor(data) {// {{{
- this.data = data
- }// }}}
- name() {// {{{
- return this.data.Name?.toLowerCase() || ''
- }// }}}
- address() {// {{{
- return this.data.Address || ''
- }// }}}
- port() {// {{{
- return parseInt(this.data.Port || 0)
- }// }}}
- username() {// {{{
- return this.data.Username || ''
- }// }}}
- password() {// {{{
- return this.data.Password || ''
- }// }}}
- name() {// {{{
- return this.data.Name?.toLowerCase() || ''
- }// }}}
- async retrieveRecords() {// {{{
- const data = await fetch(`/device/${this.name()}/dns_records`)
- const json = await data.json()
- if (!json.OK)
- throw new Error(json.Error)
- return json.Records
- }// }}}
-}
-
-class DeviceSelectWidget {
- constructor(devices) {// {{{
- this.devices = devices
-
- this.div = null
- this.elDeviceSelect = null
- this.currentlySelected = null
- }// }}}
- setDevices(devices) {// {{{
- this.devices = devices
- }// }}}
- render() {// {{{
- if (this.div === null) {
- this.div = document.createElement('div')
- this.div.classList.add('device-select')
- this.div.innerHTML = `
- Select device
-
-
- `
-
- this.elDeviceSelect = this.div.querySelector('select')
- this.elDeviceSelect.addEventListener('change', () => this.notifyDeviceSelect())
- this.div.querySelector('img').addEventListener('click', ()=>new DeviceDialog(this.devices).render())
- }
- this.restockDeviceSelect()
- return this.div
- }// }}}
- restockDeviceSelect() {// {{{
- this.elDeviceSelect.replaceChildren()
- const emptyOption = document.createElement('option')
- emptyOption.innerText = "-"
- emptyOption.dataset.devicename = "-"
- this.elDeviceSelect.appendChild(emptyOption)
-
- this.devices.forEach(dev => {
- const option = document.createElement('option')
- option.innerText = dev.name()
- option.dataset.devicename = dev.name()
- this.elDeviceSelect.appendChild(option)
- })
-
- if (this.currentlySelected !== null)
- this.elDeviceSelect.value = this.currentlySelected
- }// }}}
- notifyDeviceSelect() {// {{{
- const devName = this.elDeviceSelect.value
- this.currentlySelected = devName
- _mbus.dispatch('device_selected', { devName })
- }// }}}
- selectDevice(devname) {// {{{
- this.elDeviceSelect.value = devname
- this.notifyDeviceSelect()
+ this.cleanFolders()
+ this.renderFolders()
+ this.render()
}// }}}
}
@@ -562,8 +334,10 @@ 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')
+ if (this.name() == '_no.domain') {
+ console.log('wut')
this.div.classList.add('no-domain')
+ }
}
@@ -774,7 +548,6 @@ class Record {
return [this.imgIcon, this.divFQDN, this.divType, this.divValue, this.divTTL, this.divActions]
}// }}}
mouseEnter() {// {{{
- this.imgIcon.classList.add('mouse-over')
this.divFQDN.classList.add('mouse-over')
this.divType.classList.add('mouse-over')
this.divValue.classList.add('mouse-over')
@@ -782,7 +555,6 @@ class Record {
this.divActions.classList.add('mouse-over')
}// }}}
mouseLeave() {// {{{
- this.imgIcon.classList.remove('mouse-over')
this.divFQDN.classList.remove('mouse-over')
this.divType.classList.remove('mouse-over')
this.divValue.classList.remove('mouse-over')
@@ -793,7 +565,7 @@ class Record {
save() {// {{{
const created = (this.id() == '')
- fetch(`/device/${_app.currentDevice.name()}/record`, {
+ fetch('/record/save', {
method: 'POST',
body: JSON.stringify(this.data),
})
@@ -812,10 +584,10 @@ class Record {
}
_app.cleanFolders()
- _app.createFolders()
- _app.renderDevice()
+ _app.renderFolders()
+ _app.render()
- // createFolders is setting the folder the record resides in.
+ // renderFolders is setting the folder the record resides in.
// It can now be expanded to the parent folder.
if (created) {
this.openParentFolders()
@@ -831,7 +603,7 @@ class Record {
if (!confirm(`Are you sure you want to delete ${this.name()}?`))
return
- fetch(`/device/${_app.currentDevice.name()}/record/${this.id()}`, { method: 'DELETE' })
+ fetch(`/record/delete/${this.id()}`)
.then(data => data.json())
.then(json => {
if (!json.OK) {
@@ -840,8 +612,8 @@ class Record {
}
_app.deleteRecord(this.id())
_app.cleanFolders()
- _app.createFolders()
- _app.renderDevice()
+ _app.renderFolders()
+ _app.render()
})
}// }}}
@@ -933,7 +705,6 @@ class Settings {
this.settings = new Map([
['boxed_folders', false],
['toplevel_open', false],
- ['last_device', ''],
])
// Read any configured settings from local storage, but keeping default value
@@ -997,117 +768,3 @@ class SettingsDialog {
this.dlg.close()
}// }}}
}
-
-class DeviceDialog {
- constructor(devices) {// {{{
- this.devices = devices
- this.device = null
- }// }}}
- render() {// {{{
- // Only one open at any one time.
- if (document.getElementById('device-dialog'))
- return
-
- this.dlg = document.createElement('dialog')
- this.dlg.id = 'device-dialog'
- this.dlg.innerHTML = `
-
-
-
-
-
Name
-
-
-
Address
-
-
-
Port
-
-
-
Username
-
-
-
Password
-
-
-
-
-
-
-
- `
-
- this.elDevices = this.dlg.querySelector('.devices')
- this.elFields = this.dlg.querySelector('.fields')
- this.elName = this.dlg.querySelector('.fields .name')
- this.elAddress = this.dlg.querySelector('.fields .address')
- this.elPort = this.dlg.querySelector('.fields .port')
- this.elUsername = this.dlg.querySelector('.fields .username')
- this.elPassword = this.dlg.querySelector('.fields .password')
-
- const devices = Array.from(this.devices.values())
- devices.sort((a, b)=>{
- if (a.name() < b.name()) return -1
- if (a.name() > b.name()) return 1
- return 0
- })
- devices.forEach(dev=>{
- const devEl = document.createElement('div')
- devEl.classList.add('device')
- devEl.innerText = dev.name()
- devEl.addEventListener('click', ()=>this.editDevice(dev, devEl))
- this.elDevices.appendChild(devEl)
- })
-
- this.dlg.querySelector('.update').addEventListener('click', ()=>this.updateDevice())
- this.dlg.addEventListener('close', ()=>this.dlg.remove())
-
- document.body.appendChild(this.dlg)
- this.dlg.showModal()
- }// }}}
-
- editDevice(dev, devEl) {// {{{
- this.device = dev
-
- this.elDevices.querySelectorAll('.device.selected').forEach(el=>el.classList.remove('selected'))
- devEl.classList.add('selected')
-
- this.elName.value = dev.name()
- this.elAddress.value = dev.address()
- this.elPort.value = dev.port()
- this.elUsername.value = dev.username()
- this.elPassword.value = dev.password()
- }// }}}
- async updateDevice() {// {{{
- const req = {
- CurrentName: this.device.name(),
- Device: {
- Name: this.elName.value,
- Address: this.elAddress.value,
- Port: parseInt(this.elPort.value),
- Username: this.elUsername.value,
- Password: this.elPassword.value,
- }
- }
-
- try {
- const data = await fetch('/device', {
- method: 'POST',
- body: JSON.stringify(req),
- })
- const json = await data.json()
-
- this.device.data.Name = json.Device.Name
- this.device.data.Address = json.Device.Address
- this.device.data.Port = json.Device.Port
- this.device.data.Username = json.Device.Username
-
- _mbus.dispatch('device_updated', { device: this.device })
- this.dlg.close()
-
- } catch(err) {
- console.error(err)
- alert(err)
- }
- }// }}}
-}
diff --git a/views/pages/index.gotmpl b/views/pages/index.gotmpl
index f774e4f..d045df4 100644
--- a/views/pages/index.gotmpl
+++ b/views/pages/index.gotmpl
@@ -1,6 +1,9 @@
{{ define "page" }}
+
+{{ .Data.Identity }}
+
{{ end }}
diff --git a/webserver.go b/webserver.go
index a87a3d3..fc916ec 100644
--- a/webserver.go
+++ b/webserver.go
@@ -24,7 +24,7 @@ var (
viewFS embed.FS
)
-func registerWebserverHandlers() { // {{{
+func registerWebserverHandlers() {
var err error
htmlEngine, err = HTMLTemplate.NewEngine(viewFS, staticFS, flagDev)
if err != nil {
@@ -33,32 +33,17 @@ func registerWebserverHandlers() { // {{{
}
http.HandleFunc("/", rootHandler)
- http.HandleFunc("GET /devices", actionDevices)
- http.HandleFunc("POST /device", actionDeviceUpdate)
- http.HandleFunc("GET /device/{dev}/dns_records", actionDNSRecords)
- http.HandleFunc("POST /device/{dev}/record", actionRecordSave)
- http.HandleFunc("DELETE /device/{dev}/record/{id}", actionRecordDelete)
-} // }}}
-func startWebserver() { // {{{
+ http.HandleFunc("/record/save", actionRecordSave)
+ http.HandleFunc("/record/delete/{id}", actionRecordDelete)
+}
+
+func startWebserver() {
listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
logger.Info("webserver", "listen", listen)
http.ListenAndServe(listen, nil)
-} // }}}
+}
-func httpError(w http.ResponseWriter, err error) { // {{{
- resp := struct {
- OK bool
- Error string
- }{
- false,
- err.Error(),
- }
-
- j, _ := json.Marshal(resp)
- w.Write(j)
-} // }}}
-
-func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{
+func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
page := HTMLTemplate.SimplePage{}
page.Layout = "main"
@@ -67,6 +52,21 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{
var err error
data := make(map[string]any)
+ data["Identity"], err = device.GetIdentity()
+ if err != nil {
+ w.Write([]byte(err.Error()))
+ return
+ }
+
+ var entries []DNSRecord
+ entries, err = device.StaticDNSEntries()
+ if err != nil {
+ w.Write([]byte(err.Error()))
+ return
+ }
+
+ data["DNSRecords"] = entries
+
data["VERSION"] = VERSION
page.Data = data
@@ -79,91 +79,25 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{
}
htmlEngine.StaticResource(w, r)
-} // }}}
-func actionDevices(w http.ResponseWriter, r *http.Request) { // {{{
- var devs []Device
- for _, dev := range config.Devices {
- dev.Password = ""
- devs = append(devs, dev)
- }
+}
- j, _ := json.Marshal(struct {
- OK bool
- Devices []Device
+func httpError(w http.ResponseWriter, err error) {
+ resp := struct {
+ OK bool
+ Error string
}{
- true,
- devs,
- })
+ false,
+ err.Error(),
+ }
+ j, _ := json.Marshal(resp)
w.Write(j)
-} // }}}
-func actionDeviceUpdate(w http.ResponseWriter, r *http.Request) { // {{{
- var req struct {
- CurrentName string
- Device Device
- }
-
- body, _ := io.ReadAll(r.Body)
- err := json.Unmarshal(body, &req)
- if err != nil {
- httpError(w, err)
- return
- }
-
- device, err := config.UpdateDevice(req.CurrentName, req.Device)
- if err != nil {
- httpError(w, err)
- return
- }
- device.Password = "" // don't leak unnecessarily
-
- j, _ := json.Marshal(struct {
- OK bool
- Device Device
- }{
- true,
- device,
- })
- w.Write(j)
-} // }}}
-
-func actionDNSRecords(w http.ResponseWriter, r *http.Request) { // {{{
- devname := r.PathValue("dev")
-
- device, err := routerosDevice(devname)
- if err != nil {
- httpError(w, err)
- return
- }
-
- var records []DNSRecord
- records, err = device.StaticDNSEntries()
- if err != nil {
- httpError(w, err)
- return
- }
-
- j, _ := json.Marshal(struct {
- OK bool
- Records []DNSRecord
- }{
- true,
- records,
- })
- w.Write(j)
-
-} // }}}
-func actionRecordSave(w http.ResponseWriter, r *http.Request) { // {{{
- devName := r.PathValue("dev")
- device, err := routerosDevice(devName)
- if err != nil {
- httpError(w, err)
- return
- }
+}
+func actionRecordSave(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var record DNSRecord
- err = json.Unmarshal(body, &record)
+ err := json.Unmarshal(body, &record)
if err != nil {
httpError(w, err)
return
@@ -183,22 +117,17 @@ func actionRecordSave(w http.ResponseWriter, r *http.Request) { // {{{
Record DNSRecord
}{true, record})
w.Write(j)
-} // }}}
-func actionRecordDelete(w http.ResponseWriter, r *http.Request) { // {{{
- devName := r.PathValue("dev")
- device, err := routerosDevice(devName)
- if err != nil {
- httpError(w, err)
- return
- }
+}
+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)
+ err := device.DeleteDNSEntry(id)
if err != nil {
httpError(w, err)
logger.Error("webserver", "op", "record_delete", "error", err)
@@ -206,7 +135,7 @@ func actionRecordDelete(w http.ResponseWriter, r *http.Request) { // {{{
}
j, _ := json.Marshal(struct {
- OK bool
+ OK bool
}{true})
w.Write(j)
-} // }}}
+}