working resman version

This commit is contained in:
root
2026-03-06 18:30:03 +01:00
parent ae97a3441f
commit 883dc6856d
4 changed files with 424 additions and 334 deletions
+264 -334
View File
@@ -1,379 +1,272 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ResMan</title> <title>ResMan</title>
<style> <style>
body{font-family:Arial;margin:40px;background:#f2f2f2}
body{ h1,h2{margin-top:30px}
font-family:Arial, Helvetica, sans-serif;
background:#0f172a;
color:#e5e7eb;
margin:0;
padding:20px;
}
h1{
margin-bottom:20px;
}
button{
background:#3b82f6;
border:none;
padding:8px 14px;
color:white;
border-radius:6px;
cursor:pointer;
}
button:hover{
background:#2563eb;
}
button.delete{
background:#ef4444;
}
button.delete:hover{
background:#dc2626;
}
.card{
background:#1e293b;
padding:15px;
border-radius:10px;
margin-bottom:15px;
border:1px solid #334155;
}
table{ table{
width:100%; border-collapse:collapse;
border-collapse:collapse; width:100%;
margin-top:10px; background:white;
margin-bottom:20px
} }
th,td{ th,td{
padding:10px; border:1px solid #ddd;
border-bottom:1px solid #334155; padding:8px
} }
th{ th{
text-align:left; background:#333;
color:white
} }
tr:hover{ button{
background:#273449; padding:5px 10px;
margin-right:4px;
cursor:pointer
} }
.modal{ input{
position:fixed; padding:4px
top:0;
left:0;
width:100%;
height:100%;
background:rgba(0,0,0,0.6);
display:flex;
align-items:center;
justify-content:center;
} }
.modal-content{ .form{
background:#1e293b; background:white;
padding:25px; padding:10px;
border-radius:10px; margin-bottom:20px
max-height:90vh;
overflow:auto;
width:700px;
border:1px solid #334155;
} }
input,select,textarea{
width:100%;
padding:8px;
margin-bottom:10px;
border-radius:6px;
border:1px solid #334155;
background:#0f172a;
color:#e5e7eb;
}
label{
font-size:14px;
}
.form-grid{
display:grid;
grid-template-columns:1fr 1fr;
gap:10px;
}
.ip-list{
margin-top:10px;
}
.ip-item{
display:flex;
justify-content:space-between;
padding:6px;
border-bottom:1px solid #334155;
}
</style> </style>
</head> </head>
<body> <body>
<h1>Resource Manager</h1> <h1>ResMan</h1>
<button onclick="openCreate()"> New Resource</button> <h2>Create Resource</h2>
<div class="form">
<input id="r_name" placeholder="Name">
<input id="r_provider" placeholder="Provider">
<input id="r_cpu" placeholder="CPU">
<input id="r_ram" placeholder="RAM">
<input id="r_disk" placeholder="Disk">
<button onclick="createResource()">Create</button>
</div>
<h2>Active Resources</h2> <h2>Active Resources</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th>Name</th> <th>Name</th>
<th>Provider</th> <th>Provider</th>
<th>Produkt</th>
<th>CPU</th> <th>CPU</th>
<th>RAM</th> <th>RAM</th>
<th>Disk</th> <th>Disk</th>
<th>IPs</th> <th>IPs</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="activeResources"></tbody> <tbody id="resources"></tbody>
</table> </table>
<h2>Cancelled Resources</h2> <h2>Cancelled Resources</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>ID</th>
<th>Name</th> <th>Name</th>
<th>Provider</th> <th>Provider</th>
<th>Produkt</th>
<th>CPU</th>
<th>RAM</th>
<th>Disk</th>
<th>IPs</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="cancelledResources"></tbody> <tbody id="cancelled"></tbody>
</table> </table>
<h2>Domains</h2>
<!-- MODAL --> <div class="form">
<div class="modal" id="resourceModal"> <input id="d_name" placeholder="Domain">
<input id="d_provider" placeholder="Provider">
<input id="d_ip" placeholder="IP">
<input id="d_cost" placeholder="Yearly €">
<div class="modal-content"> <button onclick="createDomain()">Create</button>
<h2 id="modalTitle"></h2>
<input id="name" placeholder="Name">
<input id="produkt" placeholder="Produkt">
<input id="provider" placeholder="Provider">
<input id="art" placeholder="Art">
<input id="cpu" placeholder="CPU">
<input id="ram" placeholder="RAM">
<input id="disk" placeholder="Disk">
<input id="os" placeholder="OS">
<input id="ipv6_net" placeholder="IPv6 Netz">
<input id="providername" placeholder="Provider Name">
<input id="kosten_monat" placeholder="Kosten Monat">
<input id="kosten_jahr" placeholder="Kosten Jahr">
<input id="laufzeit_monate" placeholder="Laufzeit Monate">
<input id="bestelldatum" type="date">
<input id="kuendbar_ab" type="date">
<select id="status">
<option value="aktiv">aktiv</option>
<option value="gekündigt">gekündigt</option>
</select>
<input id="kuendigungsdatum" type="date">
<textarea id="bemerkung" placeholder="Bemerkung"></textarea>
<button onclick="saveResource()">Save</button>
<button onclick="closeModal()">Cancel</button>
</div> </div>
</div> <table>
<thead>
<tr>
<th>ID</th>
<th>Domain</th>
<th>Provider</th>
<th>IP</th>
<th>Server</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="domains"></tbody>
</table>
<h2>Domain → Server Mapping</h2>
<table>
<thead>
<tr>
<th>Domain</th>
<th>IP</th>
<th>Server</th>
</tr>
</thead>
<tbody id="mapping"></tbody>
</table>
<script> <script>
const API="/resman/api/resources" const API="/resman/api"
let editId=null
function val(v){ /* LOAD RESOURCES */
if(v==="" || v===undefined) return null async function loadResources(){
return v const res=await fetch(API+"/resources/active")
const data=await res.json()
} const table=document.getElementById("resources")
table.innerHTML=""
data.forEach(r=>{
const ips=r.ips.map(ip=>ip.ip).join("<br>")
function getForm(){ const tr=document.createElement("tr")
return{ tr.innerHTML=`
name:document.getElementById("name").value, <td>${r.id}</td>
<td>${r.name}</td>
<td>${r.provider||""}</td>
<td>${r.cpu||""}</td>
<td>${r.ram||""}</td>
<td>${r.disk||""}</td>
produkt:val(document.getElementById("produkt").value), <td>
provider:val(document.getElementById("provider").value), ${ips}
art:val(document.getElementById("art").value), <br>
cpu:val(document.getElementById("cpu").value), <input id="ip_${r.id}" placeholder="new ip">
<button onclick="addIP(${r.id})">Add</button>
ram:val(document.getElementById("ram").value), </td>
disk:val(document.getElementById("disk").value), <td>
os:val(document.getElementById("os").value), <button onclick="editResource(${r.id})">Edit</button>
ipv6_net:val(document.getElementById("ipv6_net").value), <button onclick="deleteResource(${r.id})">Delete</button>
providername:val(document.getElementById("providername").value), </td>
kosten_monat:val(document.getElementById("kosten_monat").value), `
kosten_jahr:val(document.getElementById("kosten_jahr").value), table.appendChild(tr)
laufzeit_monate:val(document.getElementById("laufzeit_monate").value),
bestelldatum:val(document.getElementById("bestelldatum").value),
kuendbar_ab:val(document.getElementById("kuendbar_ab").value),
status:document.getElementById("status").value,
kuendigungsdatum:val(document.getElementById("kuendigungsdatum").value),
bemerkung:val(document.getElementById("bemerkung").value)
}
}
function openCreate(){
editId=null
document.getElementById("modalTitle").innerText="New Resource"
document.getElementById("resourceModal").style.display="flex"
}
function editResource(r){
editId=r.id
document.getElementById("modalTitle").innerText="Edit Resource"
Object.keys(r).forEach(k=>{
const el=document.getElementById(k)
if(el) el.value=r[k] || ""
}) })
document.getElementById("resourceModal").style.display="flex"
} }
function closeModal(){ /* CANCELLED */
document.getElementById("resourceModal").style.display="none" async function loadCancelled(){
} const res=await fetch(API+"/resources/cancelled")
const data=await res.json()
const table=document.getElementById("cancelled")
table.innerHTML=""
data.forEach(r=>{
async function saveResource(){ const tr=document.createElement("tr")
const data=getForm() tr.innerHTML=`
<td>${r.id}</td>
<td>${r.name}</td>
<td>${r.provider||""}</td>
`
if(editId){ table.appendChild(tr)
await fetch(API+"/"+editId,{
method:"PUT",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(data)
}) })
}else{ }
await fetch(API,{
/* CREATE RESOURCE */
async function createResource(){
const body={
name:document.getElementById("r_name").value,
provider:document.getElementById("r_provider").value,
cpu:document.getElementById("r_cpu").value,
ram:document.getElementById("r_ram").value,
disk:document.getElementById("r_disk").value
}
await fetch(API+"/resources",{
method:"POST", method:"POST",
headers:{"Content-Type":"application/json"}, headers:{'Content-Type':'application/json'},
body:JSON.stringify(data) body:JSON.stringify(body)
}) })
}
closeModal()
loadResources() loadResources()
} }
/* DELETE RESOURCE */
async function deleteResource(id){ async function deleteResource(id){
if(!confirm("Delete resource?")) return if(!confirm("delete resource?")) return
await fetch(API+"/"+id,{method:"DELETE"}) await fetch(API+"/resources/"+id,{method:"DELETE"})
loadResources() loadResources()
@@ -381,21 +274,20 @@ loadResources()
async function addIP(resourceId){ /* EDIT RESOURCE */
const ip=prompt("IP") async function editResource(id){
if(!ip) return const name=prompt("Name")
if(!name) return
const type=prompt("Type (public/vlan/ipv6)") await fetch(API+"/resources/"+id,{
await fetch(API+"/"+resourceId+"/ips",{ method:"PUT",
method:"POST", headers:{'Content-Type':'application/json'},
headers:{"Content-Type":"application/json"}, body:JSON.stringify({name})
body:JSON.stringify({ip,type})
}) })
@@ -405,15 +297,17 @@ loadResources()
async function editIP(ipId){ /* IP MANAGER */
const ip=prompt("New IP") async function addIP(resource_id){
await fetch("/resman/api/ips/"+ipId,{ const ip=document.getElementById("ip_"+resource_id).value
method:"PUT", await fetch(API+"/resources/"+resource_id+"/ips",{
headers:{"Content-Type":"application/json"}, method:"POST",
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ip}) body:JSON.stringify({ip})
@@ -425,108 +319,144 @@ loadResources()
async function deleteIP(ipId){ /* DOMAINS */
await fetch("/resman/api/ips/"+ipId,{method:"DELETE"}) async function loadDomains(){
loadResources() const res=await fetch(API+"/domains")
} const data=await res.json()
const table=document.getElementById("domains")
table.innerHTML=""
function renderIPs(r){ data.forEach(d=>{
let html="" const tr=document.createElement("tr")
if(Array.isArray(r.ips)){ tr.innerHTML=`
r.ips.forEach(ip=>{ <td>${d.id}</td>
html+=` <td>${d.domain_name}</td>
<div class="ip"> <td>${d.provider||""}</td>
${ip.ip} (${ip.type}) <td>${d.ip_address||""}</td>
<button onclick="editIP(${ip.id})">Edit</button> <td>${d.resource_name||""}</td>
<button onclick="deleteIP(${ip.id})">Delete</button>
</div>
`
})
}
html+=`<button onclick="addIP(${r.id})">Add IP</button>`
return html
}
function renderRow(r){
return `
<tr>
<td>${r.name}</td>
<td>${r.provider||""}</td>
<td>${r.produkt||""}</td>
<td>${r.cpu||""}</td>
<td>${r.ram||""}</td>
<td>${r.disk||""}</td>
<td>${renderIPs(r)}</td>
<td> <td>
<button onclick='editResource(${JSON.stringify(r)})'>Edit</button> <button onclick="deleteDomain(${d.id})">Delete</button>
<button onclick="deleteResource(${r.id})">Delete</button>
</td> </td>
</tr> `
table.appendChild(tr)
})
}
/* CREATE DOMAIN */
async function createDomain(){
const body={
domain_name:document.getElementById("d_name").value,
provider:document.getElementById("d_provider").value,
ip_address:document.getElementById("d_ip").value,
yearly_cost:document.getElementById("d_cost").value
}
await fetch(API+"/domains",{
method:"POST",
headers:{'Content-Type':'application/json'},
body:JSON.stringify(body)
})
loadDomains()
loadMapping()
}
/* DELETE DOMAIN */
async function deleteDomain(id){
if(!confirm("delete domain?")) return
await fetch(API+"/domains/"+id,{method:"DELETE"})
loadDomains()
loadMapping()
}
/* DOMAIN MAPPING */
async function loadMapping(){
const res=await fetch(API+"/domainmap")
const data=await res.json()
const table=document.getElementById("mapping")
table.innerHTML=""
data.forEach(m=>{
const tr=document.createElement("tr")
tr.innerHTML=`
<td>${m.domain_name}</td>
<td>${m.ip_address}</td>
<td>${m.server_name||""}</td>
` `
} table.appendChild(tr)
async function loadResources(){
const active=await fetch(API+"/active").then(r=>r.json())
const cancelled=await fetch(API+"/cancelled").then(r=>r.json())
const activeTable=document.getElementById("activeResources")
const cancelledTable=document.getElementById("cancelledResources")
activeTable.innerHTML=""
cancelledTable.innerHTML=""
active.forEach(r=>{
activeTable.innerHTML+=renderRow(r)
})
cancelled.forEach(r=>{
cancelledTable.innerHTML+=renderRow(r)
}) })
} }
/* INIT */
loadResources() loadResources()
loadCancelled()
loadDomains()
loadMapping()
</script> </script>
</body> </body>
+33
View File
@@ -0,0 +1,33 @@
const express = require('express')
const router = express.Router()
const db = require('../db')
router.get('/', async (req, res) => {
try {
const [rows] = await db.query(`
SELECT
d.id AS domain_id,
d.domain_name,
d.ip_address,
r.id AS resource_id,
r.name AS server_name
FROM domains d
LEFT JOIN resource_ips ip ON d.ip_address = ip.ip
LEFT JOIN resources r ON ip.resource_id = r.id
ORDER BY d.domain_name
`)
res.json(rows)
} catch(err){
console.error(err)
res.status(500).json({error:"DB error"})
}
})
module.exports = router
+120
View File
@@ -0,0 +1,120 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
// GET ALL DOMAINS
router.get('/', async (req, res) => {
try {
const [rows] = await pool.query(`
SELECT
d.id,
d.domain_name,
d.provider,
d.ip_address,
d.yearly_cost,
d.notes,
r.name AS resource_name
FROM domains d
LEFT JOIN resource_ips ip ON d.ip_address = ip.ip
LEFT JOIN resources r ON ip.resource_id = r.id
ORDER BY d.domain_name
`);
res.json(rows);
} catch (err) {
console.error("GET domains error:", err);
res.status(500).json({ error: "DB error" });
}
});
// GET SINGLE DOMAIN
router.get('/:id', async (req, res) => {
try {
const [rows] = await pool.query(
`SELECT * FROM domains WHERE id = ?`,
[req.params.id]
);
if (rows.length === 0) {
return res.status(404).json({ error: "Domain not found" });
}
res.json(rows[0]);
} catch (err) {
console.error("GET domain error:", err);
res.status(500).json({ error: "DB error" });
}
});
// CREATE DOMAIN
router.post('/', async (req, res) => {
try {
const { domain_name, provider, ip_address, yearly_cost, notes } = req.body;
const [result] = await pool.query(
`INSERT INTO domains
(domain_name, provider, ip_address, yearly_cost, notes)
VALUES (?, ?, ?, ?, ?)`,
[domain_name, provider, ip_address, yearly_cost, notes]
);
res.json({
message: "Domain created",
id: result.insertId
});
} catch (err) {
console.error("CREATE domain error:", err);
res.status(500).json({ error: "DB error" });
}
});
// UPDATE DOMAIN
router.put('/:id', async (req, res) => {
try {
const { domain_name, provider, ip_address, yearly_cost, notes } = req.body;
await pool.query(
`UPDATE domains SET
domain_name = ?,
provider = ?,
ip_address = ?,
yearly_cost = ?,
notes = ?
WHERE id = ?`,
[domain_name, provider, ip_address, yearly_cost, notes, req.params.id]
);
res.json({ message: "Domain updated" });
} catch (err) {
console.error("UPDATE domain error:", err);
res.status(500).json({ error: "DB error" });
}
});
// DELETE DOMAIN
router.delete('/:id', async (req, res) => {
try {
await pool.query(
`DELETE FROM domains WHERE id = ?`,
[req.params.id]
);
res.json({ message: "Domain deleted" });
} catch (err) {
console.error("DELETE domain error:", err);
res.status(500).json({ error: "DB error" });
}
});
module.exports = router;
+7
View File
@@ -4,6 +4,8 @@ const path = require("path");
const resourceRoutes = require("./routes/resources"); const resourceRoutes = require("./routes/resources");
const ipRoutes = require("./routes/ips"); const ipRoutes = require("./routes/ips");
const domainsRoutes = require('./routes/domains');
const domainMapping = require('./routes/domainMapping')
const app = express(); const app = express();
@@ -29,6 +31,11 @@ app.use("/resman/api/resources", resourceRoutes);
app.use("/resman/api", ipRoutes); app.use("/resman/api", ipRoutes);
app.use('/resman/api/domains', domainsRoutes);
app.use('/resman/api/domainmap', domainMapping)
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error("EXPRESS ERROR:"); console.error("EXPRESS ERROR:");