diff --git a/README.md b/README.md index c08dd7b..1dc6c7c 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,29 @@ infra.js ```bash git clone cd resman -cd backend -npm install -npm run migrate -npm start +docker compose up -d --build ``` +### 2. Migrationen + Beim Start des Backends werden ausstehende Migrationen automatisch ausgefuehrt. -Manuell kannst du sie jederzeit mit `npm run migrate` im `backend`-Ordner starten. + +Manuell kannst du sie im Container jederzeit starten: + +```bash +docker compose exec backend npm run migrate +``` + +### 3. Backend neu bauen / neu starten + +```bash +docker compose up -d --build backend +``` + +### 4. Erreichbarkeit + +- Frontend/API intern: `http://127.0.0.1:3000/resman/` +- Fuer produktive Nutzung ist ein Reverse Proxy davor vorgesehen 🗄️ Datenbank Schema diff --git a/backend/public/css/style.css b/backend/public/css/style.css index 2e1e2cb..8ff918b 100644 --- a/backend/public/css/style.css +++ b/backend/public/css/style.css @@ -123,6 +123,12 @@ input, textarea { background: #f4f4f4; } +.modal-meta{ + font-size:13px; + color:#64748b; + margin:-6px 0 12px 0; +} + .subdomain{ @@ -225,3 +231,7 @@ body.dark .ip.public{ background:#16a34a; color:white; } + +body.dark .modal-meta{ + color:#94a3b8; +} diff --git a/backend/public/index.html b/backend/public/index.html index 5bbea6c..44fcea6 100644 --- a/backend/public/index.html +++ b/backend/public/index.html @@ -257,6 +257,7 @@ Domains jährlich: 0 €
@@ -302,6 +303,7 @@ Domains jährlich: 0 €
diff --git a/backend/public/js/api.js b/backend/public/js/api.js index 29901dc..c16d9bd 100644 --- a/backend/public/js/api.js +++ b/backend/public/js/api.js @@ -1,6 +1,7 @@ const API = "/resman/api" -async function api(url, options = {}) { +async function api(url, options = {}, config = {}) { + const showToast = config.showError !== false const res = await fetch(url, options) let data @@ -14,10 +15,35 @@ async function api(url, options = {}) { if (!res.ok) { const msg = data.error || "Unbekannter Fehler" - showError(msg) + if (showToast) { + showError(msg) + } - throw new Error(msg) + const error = new Error(msg) + error.status = res.status + error.data = data + + throw error } return data -} \ No newline at end of file +} + +window.runAction = async function(action, onSuccess) { + try { + await action() + + if (onSuccess) { + await onSuccess() + } + + return true + } catch (error) { + if (error && error.silent) { + return false + } + + console.error(error) + return false + } +} diff --git a/backend/public/js/domains.js b/backend/public/js/domains.js index 5254952..0768d8d 100644 --- a/backend/public/js/domains.js +++ b/backend/public/js/domains.js @@ -15,7 +15,7 @@ async function getDNS(domain){ try{ - const res = await api(API + "/dns/" + domain) + const res = await api(API + "/dns/" + domain, {}, { showError: false }) const ips = res.ips || [] dnsCache[domain] = { @@ -47,6 +47,7 @@ window.loadDomains = async function(){ const domains = await api(API + "/domains") const subs = await api(API + "/subdomains") + window.domainList = domains const table = document.getElementById("domains") table.innerHTML = "" @@ -208,28 +209,34 @@ window.saveDomain = async function(){ notes: domain_notes.value } - if(id){ + await runAction( + async () => { + if(id){ - await api(API+"/domains/"+id,{ - method:"PUT", - headers:{'Content-Type':'application/json'}, - body:JSON.stringify(data) - }) + await api(API+"/domains/"+id,{ + method:"PUT", + headers:{'Content-Type':'application/json'}, + body:JSON.stringify(data) + }) - }else{ + }else{ - await api(API+"/domains",{ - method:"POST", - headers:{'Content-Type':'application/json'}, - body:JSON.stringify(data) - }) + await api(API+"/domains",{ + method:"POST", + headers:{'Content-Type':'application/json'}, + body:JSON.stringify(data) + }) - } - - closeDomainModal() - loadDomains() - loadMapping() - loadCosts() + } + }, + async () => { + closeDomainModal() + await loadDomains() + await loadMapping() + loadCosts() + loadInfrastructure() + } + ) } @@ -238,29 +245,36 @@ window.deleteDomain = async function(id){ if(!confirm("Domain löschen?")) return - await api(API + "/domains/" + id, { - method: "DELETE" - }) - - loadDomains() + await runAction( + async () => { + await api(API + "/domains/" + id, { + method: "DELETE" + }) + }, + async () => { + await loadDomains() + await loadMapping() + loadCosts() + loadInfrastructure() + } + ) } window.moveDomain = async function(id, direction){ - - try{ - - await api(API + "/domains/" + id + "/move", { - method: "POST", - headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ direction }) - }) - - loadDomains() - loadMapping() - - }catch(e){ - console.log(e) - } + await runAction( + async () => { + await api(API + "/domains/" + id + "/move", { + method: "POST", + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ direction }) + }) + }, + async () => { + await loadDomains() + await loadMapping() + loadInfrastructure() + } + ) } @@ -277,26 +291,33 @@ window.saveSubdomain = async function(){ const data = { domain_id, subdomain, ip_address } - if(id){ + await runAction( + async () => { + if(id){ - await api(API+"/subdomains/"+id,{ - method:"PUT", - headers:{'Content-Type':'application/json'}, - body:JSON.stringify(data) - }) + await api(API+"/subdomains/"+id,{ + method:"PUT", + headers:{'Content-Type':'application/json'}, + body:JSON.stringify(data) + }) - }else{ + }else{ - await api(API+"/subdomains",{ - method:"POST", - headers:{'Content-Type':'application/json'}, - body:JSON.stringify(data) - }) + await api(API+"/subdomains",{ + method:"POST", + headers:{'Content-Type':'application/json'}, + body:JSON.stringify(data) + }) - } - - closeSubModal() - loadDomains() + } + }, + async () => { + closeSubModal() + await loadDomains() + await loadMapping() + loadInfrastructure() + } + ) } @@ -305,11 +326,18 @@ window.deleteSub = async function(id){ if(!confirm("Subdomain löschen?")) return - await api(API + "/subdomains/" + id, { - method: "DELETE" - }) - - loadDomains() + await runAction( + async () => { + await api(API + "/subdomains/" + id, { + method: "DELETE" + }) + }, + async () => { + await loadDomains() + await loadMapping() + loadInfrastructure() + } + ) } diff --git a/backend/public/js/modals.js b/backend/public/js/modals.js index d6a7410..cd0144d 100644 --- a/backend/public/js/modals.js +++ b/backend/public/js/modals.js @@ -126,6 +126,7 @@ document.getElementById("serverDetailModal").style.display="none" window.openDomainCreate = function(){ document.getElementById("domainModalTitle").innerText = "Create Domain" + document.getElementById("domainModalMeta").innerText = "Neue Domain anlegen" document.getElementById("domain_id").value = "" document.getElementById("domain_name").value = "" @@ -133,8 +134,10 @@ window.openDomainCreate = function(){ document.getElementById("domain_ip").value = "" document.getElementById("domain_cost").value = "" document.getElementById("domain_notes").value = "" + document.getElementById("domainSaveBtn").innerText = "Create" openModal("domainModal") + document.getElementById("domain_name").focus() } @@ -142,6 +145,7 @@ window.openDomainCreate = function(){ window.openDomainEdit = function(d){ document.getElementById("domainModalTitle").innerText="Edit Domain" +document.getElementById("domainModalMeta").innerText=d.domain_name || "" document.getElementById("domain_id").value=d.id @@ -150,8 +154,10 @@ document.getElementById("domain_provider").value=d.provider || "" document.getElementById("domain_ip").value=d.ip_address || "" document.getElementById("domain_cost").value=d.yearly_cost || "" document.getElementById("domain_notes").value=d.notes || "" +document.getElementById("domainSaveBtn").innerText = "Update" document.getElementById("domainModal").style.display="block" +document.getElementById("domain_name").focus() } @@ -166,17 +172,23 @@ window.closeDomainModal = function(){ ========================= */ window.openSubCreate = function(domainId){ +const domains = window.domainList || [] +const domain = domains.find(d => d.id == domainId) +const domainName = domain?.domain_name || "Unbekannte Domain" document.getElementById("sub_id").value = "" // RESET !! document.getElementById("sub_domain_id").value = domainId document.getElementById("sub_name").value = "" -document.getElementById("sub_ip").value = "" +document.getElementById("sub_ip").value = domain?.ip_address || "" document.querySelector("#subdomainModal h3").innerText = "Subdomain erstellen" +document.getElementById("subdomainModalMeta").innerText = "Fuer " + domainName +document.getElementById("subdomainSaveBtn").innerText = "Create" document.getElementById("subdomainModal").style.display="block" +document.getElementById("sub_name").focus() } @@ -187,6 +199,7 @@ window.closeSubModal = function(){ window.openSubEdit = function(s){ +const fullName = s.subdomain + "." + s.domain_name document.getElementById("sub_id").value = s.id document.getElementById("sub_domain_id").value = s.domain_id @@ -195,8 +208,11 @@ document.getElementById("sub_name").value = s.subdomain document.getElementById("sub_ip").value = s.ip_address document.querySelector("#subdomainModal h3").innerText = "Subdomain bearbeiten" +document.getElementById("subdomainModalMeta").innerText = fullName +document.getElementById("subdomainSaveBtn").innerText = "Update" document.getElementById("subdomainModal").style.display="block" +document.getElementById("sub_name").focus() } diff --git a/backend/public/js/resources.js b/backend/public/js/resources.js index 369298b..a548052 100644 --- a/backend/public/js/resources.js +++ b/backend/public/js/resources.js @@ -111,20 +111,35 @@ window.saveResource = async function(){ bemerkung:bemerkung.value } - if(id){ - await api(API+"/resources/"+id,{method:"PUT",headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}) - }else{ - await api(API+"/resources",{method:"POST",headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}) - } - - closeModal() - loadResources() + await runAction( + async () => { + if(id){ + await api(API+"/resources/"+id,{method:"PUT",headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}) + }else{ + await api(API+"/resources",{method:"POST",headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}) + } + }, + async () => { + closeModal() + await loadResources() + loadInfrastructure() + loadCosts() + } + ) } window.deleteResource = async function(id){ if(!confirm("delete resource?")) return - await api(API+"/resources/"+id,{method:"DELETE"}) - loadResources() + await runAction( + async () => { + await api(API+"/resources/"+id,{method:"DELETE"}) + }, + async () => { + await loadResources() + loadInfrastructure() + loadCosts() + } + ) } window.checkServerStatus = async function(resource){ @@ -187,46 +202,51 @@ window.saveIP = async function(){ const type = document.getElementById("new_type").value const comment = document.getElementById("new_comment").value - if(id){ - // 🔥 UPDATE - await api(API + "/ips/" + id, { - method: "PUT", - headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ip, type, comment}) - }) + await runAction( + async () => { + if(id){ + await api(API + "/ips/" + id, { + method: "PUT", + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ip, type, comment}) + }) - delete ipField.dataset.editId + delete ipField.dataset.editId - }else{ - // 🔥 CREATE + }else{ + try{ + const result = await api(API + "/ipcheck/" + ip, {}, { showError: false }) - try{ - const result = await api(API + "/ipcheck/" + ip) - - if(result.length){ - if(!confirm("IP existiert bereits. Trotzdem speichern?")){ - return + if(result.length){ + if(!confirm("IP existiert bereits. Trotzdem speichern?")){ + const error = new Error("IP save cancelled") + error.silent = true + throw error + } + } + }catch(e){ + if(e.silent) throw e + console.warn("IP check fehlgeschlagen", e) } + + await api(API + "/resources/" + resourceId + "/ips", { + method: "POST", + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ip, type, comment}) + }) } - }catch(e){ - console.log("Save IP Fehler:", e) + }, + async () => { + ipField.value = "" + document.getElementById("new_type").value = "" + document.getElementById("new_comment").value = "" + + await loadIPs(resourceId) + await loadResources() + loadInfrastructure() + cancelIPEdit() } - - await api(API + "/resources/" + resourceId + "/ips", { - method: "POST", - headers: {'Content-Type':'application/json'}, - body: JSON.stringify({ip, type, comment}) - }) - } - - // Felder reset - ipField.value = "" - document.getElementById("new_type").value = "" - document.getElementById("new_comment").value = "" - - loadIPs(resourceId) - loadResources() - cancelIPEdit() + ) } @@ -234,12 +254,18 @@ window.deleteIP = async function(id, resourceId){ if(!confirm("IP löschen?")) return - await api(API + "/ips/" + id, { - method: "DELETE" - }) - - loadIPs(resourceId) // Modal neu laden - loadResources() // Tabelle aktualisieren + await runAction( + async () => { + await api(API + "/ips/" + id, { + method: "DELETE" + }) + }, + async () => { + await loadIPs(resourceId) + await loadResources() + loadInfrastructure() + } + ) } @@ -317,19 +343,18 @@ window.cancelIPEdit = function(){ } window.moveResource = async function(id, direction){ - - try { - - await api(API + "/resources/" + id + "/move", { - method: "POST", - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ direction }) - }) - - loadResources() - - } catch (e) { - console.log(e) - } - -} \ No newline at end of file + await runAction( + async () => { + await api(API + "/resources/" + id + "/move", { + method: "POST", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ direction }) + }) + }, + async () => { + await loadResources() + loadInfrastructure() + loadCosts() + } + ) +}