initial version resource manager

This commit is contained in:
root
2026-03-06 15:29:03 +01:00
parent 10e1c5bc8b
commit ae97a3441f
3 changed files with 587 additions and 237 deletions
+132 -10
View File
@@ -1,36 +1,158 @@
const pool = require("../db");
/* ACTIVE */
exports.getActive = async (req,res)=>{
const [rows] = await pool.query(
"SELECT * FROM resources WHERE status != 'gekündigt'"
);
const [rows] = await pool.query(`
SELECT
r.*,
JSON_ARRAYAGG(
JSON_OBJECT(
'id', ip.id,
'ip', ip.ip,
'type', ip.type,
'comment', ip.comment
)
) AS ips
FROM resources r
LEFT JOIN resource_ips ip ON r.id = ip.resource_id
WHERE r.status != 'gekündigt'
GROUP BY r.id
`);
rows.forEach(r=>{
/* JSON string → array */
if(typeof r.ips === "string"){
try{
r.ips = JSON.parse(r.ips);
}catch{
r.ips=[];
}
}
/* remove null entries */
if(r.ips && r.ips[0] && r.ips[0].id === null){
r.ips=[];
}
});
res.json(rows);
};
/* CANCELLED */
exports.getCancelled = async (req,res)=>{
const [rows] = await pool.query(
"SELECT * FROM resources WHERE status = 'gekündigt'"
);
const [rows] = await pool.query(`
SELECT
r.*,
JSON_ARRAYAGG(
JSON_OBJECT(
'id', ip.id,
'ip', ip.ip,
'type', ip.type,
'comment', ip.comment
)
) AS ips
FROM resources r
LEFT JOIN resource_ips ip ON r.id = ip.resource_id
WHERE r.status = 'gekündigt'
GROUP BY r.id
`);
rows.forEach(r=>{
if(typeof r.ips === "string"){
try{
r.ips = JSON.parse(r.ips);
}catch{
r.ips=[];
}
}
if(r.ips && r.ips[0] && r.ips[0].id === null){
r.ips=[];
}
});
res.json(rows);
};
/* CREATE */
exports.create = async (req,res)=>{
await pool.query("INSERT INTO resources SET ?", req.body);
res.json({ message: "Inserted" });
const clean = v => v === "" ? null : v;
const data = {
name: req.body.name,
produkt: clean(req.body.produkt),
provider: clean(req.body.provider),
art: clean(req.body.art),
cpu: clean(req.body.cpu),
ram: clean(req.body.ram),
disk: clean(req.body.disk),
os: clean(req.body.os),
ipv6_net: clean(req.body.ipv6_net),
providername: clean(req.body.providername),
kosten_monat: clean(req.body.kosten_monat),
kosten_jahr: clean(req.body.kosten_jahr),
laufzeit_monate: clean(req.body.laufzeit_monate),
bestelldatum: clean(req.body.bestelldatum),
kuendbar_ab: clean(req.body.kuendbar_ab),
status: clean(req.body.status),
kuendigungsdatum: clean(req.body.kuendigungsdatum),
bemerkung: clean(req.body.bemerkung)
};
await pool.query("INSERT INTO resources SET ?",data);
res.json({message:"created"});
};
/* UPDATE */
exports.update = async (req,res)=>{
const clean = v => v === "" ? null : v;
Object.keys(req.body).forEach(k=>{
req.body[k]=clean(req.body[k]);
});
await pool.query(
"UPDATE resources SET ? WHERE id=?",
[req.body,req.params.id]
);
res.json({ message: "Updated" });
res.json({message:"updated"});
};
/* DELETE */
exports.remove = async (req,res)=>{
await pool.query(
"DELETE FROM resources WHERE id=?",
[req.params.id]
);
res.json({ message: "Deleted" });
res.json({message:"deleted"});
};
+419 -212
View File
@@ -1,326 +1,533 @@
```html
<!DOCTYPE html>
<html lang="de">
<html>
<head>
<meta charset="UTF-8">
<title>Resource Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<meta charset="UTF-8">
<title>ResMan</title>
<style>
body{
background:#f5f6fa;
font-family:Arial, Helvetica, sans-serif;
background:#0f172a;
color:#e5e7eb;
margin:0;
padding:20px;
}
.card{
h1{
margin-bottom:20px;
}
.ip{
font-size:0.9em;
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{
width:100%;
border-collapse:collapse;
margin-top:10px;
}
th,td{
padding:10px;
border-bottom:1px solid #334155;
}
th{
text-align:left;
}
tr:hover{
background:#273449;
}
.modal{
position:fixed;
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{
background:#1e293b;
padding:25px;
border-radius:10px;
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>
</head>
<body>
<div class="container mt-4">
<h1>Resource Manager</h1>
<h2>Resource Manager</h2>
<button onclick="openCreate()"> New Resource</button>
<div class="card">
<div class="card-header">
Neue Resource
</div>
<h2>Active Resources</h2>
<div class="card-body">
<table>
<div class="row g-2">
<thead>
<div class="col">
<input id="name" class="form-control" placeholder="Name">
</div>
<tr>
<th>Name</th>
<th>Provider</th>
<th>Produkt</th>
<th>CPU</th>
<th>RAM</th>
<th>Disk</th>
<th>IPs</th>
<th>Actions</th>
</tr>
<div class="col">
<input id="customer" class="form-control" placeholder="Customer">
</div>
</thead>
<div class="col">
<select id="status" class="form-select">
<tbody id="activeResources"></tbody>
</table>
<h2>Cancelled Resources</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Provider</th>
<th>Produkt</th>
<th>CPU</th>
<th>RAM</th>
<th>Disk</th>
<th>IPs</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="cancelledResources"></tbody>
</table>
<!-- MODAL -->
<div class="modal" id="resourceModal">
<div class="modal-content">
<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>
</div>
<div class="col">
<button class="btn btn-primary" onclick="createResource()">Create</button>
</div>
<input id="kuendigungsdatum" type="date">
<textarea id="bemerkung" placeholder="Bemerkung"></textarea>
<button onclick="saveResource()">Save</button>
<button onclick="closeModal()">Cancel</button>
</div>
</div>
</div>
<h4>Aktive Ressourcen</h4>
<div id="activeResources"></div>
<h4 class="mt-4">Gekündigte Ressourcen</h4>
<div id="cancelledResources"></div>
</div>
<script>
const API="/resman/api";
const API="/resman/api/resources"
function statusBadge(status){
let editId=null
if(status==="gekündigt"){
return `<span class="badge bg-dark">Gekündigt</span>`;
}
return `<span class="badge bg-success">Aktiv</span>`;
}
function renderResource(r){
function val(v){
return `
if(v==="" || v===undefined) return null
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h5>${r.name || ""}</h5>
${statusBadge(r.status)}
<br>
<small>${r.customer || ""}</small>
</div>
<div>
<button class="btn btn-sm btn-danger" onclick="deleteResource(${r.id})">
Delete
</button>
</div>
</div>
<hr>
<strong>IPs</strong>
<div id="ips-${r.id}" class="mb-2"></div>
<div class="input-group input-group-sm">
<input class="form-control" placeholder="IP" id="ip-${r.id}">
<select class="form-select" id="type-${r.id}">
<option value="public">public</option>
<option value="private">private</option>
</select>
<input class="form-control" placeholder="comment" id="comment-${r.id}">
<button class="btn btn-primary" onclick="addIP(${r.id})">
Add
</button>
</div>
</div>
</div>
`;
return v
}
async function loadResources(){
const activeDiv=document.getElementById("activeResources");
const cancelledDiv=document.getElementById("cancelledResources");
activeDiv.innerHTML="";
cancelledDiv.innerHTML="";
function getForm(){
try{
return{
const res=await fetch(`${API}/resources/active`);
const active=await res.json();
name:document.getElementById("name").value,
active.forEach(r=>{
activeDiv.innerHTML+=renderResource(r);
produkt:val(document.getElementById("produkt").value),
setTimeout(()=>loadIPs(r.id),100);
});
provider:val(document.getElementById("provider").value),
}catch(e){
console.error(e);
}
art:val(document.getElementById("art").value),
try{
cpu:val(document.getElementById("cpu").value),
const res=await fetch(`${API}/resources/cancelled`);
const cancelled=await res.json();
ram:val(document.getElementById("ram").value),
cancelled.forEach(r=>{
cancelledDiv.innerHTML+=renderResource(r);
disk:val(document.getElementById("disk").value),
setTimeout(()=>loadIPs(r.id),100);
});
os:val(document.getElementById("os").value),
ipv6_net:val(document.getElementById("ipv6_net").value),
providername:val(document.getElementById("providername").value),
kosten_monat:val(document.getElementById("kosten_monat").value),
kosten_jahr:val(document.getElementById("kosten_jahr").value),
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)
}catch(e){
console.error(e);
}
}
async function createResource(){
const name=document.getElementById("name").value;
const customer=document.getElementById("customer").value;
const status=document.getElementById("status").value;
await fetch(`${API}/resources`,{
method:"POST",
headers:{
"Content-Type":"application/json"
},
body:JSON.stringify({
name,
customer,
status
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("name").value="";
document.getElementById("customer").value="";
loadResources();
document.getElementById("resourceModal").style.display="flex"
}
function closeModal(){
document.getElementById("resourceModal").style.display="none"
}
async function saveResource(){
const data=getForm()
if(editId){
await fetch(API+"/"+editId,{
method:"PUT",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(data)
})
}else{
await fetch(API,{
method:"POST",
headers:{"Content-Type":"application/json"},
body:JSON.stringify(data)
})
}
closeModal()
loadResources()
}
async function deleteResource(id){
await fetch(`${API}/resources/${id}`,{
method:"DELETE"
});
if(!confirm("Delete resource?")) return
await fetch(API+"/"+id,{method:"DELETE"})
loadResources()
}
async function addIP(resourceId){
const ip=document.getElementById("ip-"+resourceId).value;
const type=document.getElementById("type-"+resourceId).value;
const comment=document.getElementById("comment-"+resourceId).value;
const ip=prompt("IP")
if(!ip){
alert("IP fehlt");
return;
}
if(!ip) return
const type=prompt("Type (public/vlan/ipv6)")
await fetch(API+"/"+resourceId+"/ips",{
await fetch("/resman/api/resources/"+resourceId+"/ips",{
method:"POST",
headers:{
"Content-Type":"application/json"
},
body:JSON.stringify({
ip:ip,
type:type,
comment:comment
headers:{"Content-Type":"application/json"},
body:JSON.stringify({ip,type})
})
});
loadResources();
loadResources()
}
loadResources();
async function editIP(ipId){
const ip=prompt("New IP")
await fetch("/resman/api/ips/"+ipId,{
method:"PUT",
headers:{"Content-Type":"application/json"},
body:JSON.stringify({ip})
})
loadResources()
}
async function loadIPs(resourceId){
const container=document.getElementById("ips-"+resourceId);
const res=await fetch(`${API}/resources/${resourceId}/ips`);
const data=await res.json();
async function deleteIP(ipId){
let html="";
await fetch("/resman/api/ips/"+ipId,{method:"DELETE"})
data.forEach(ip=>{
loadResources()
}
function renderIPs(r){
let html=""
if(Array.isArray(r.ips)){
r.ips.forEach(ip=>{
html+=`
<div class="d-flex justify-content-between border rounded p-1 mb-1 ip">
<div class="ip">
<div>
<strong>${ip.ip}</strong>
<span class="text-muted">(${ip.type})</span>
<small>${ip.comment || ""}</small>
</div>
${ip.ip} (${ip.type})
<button class="btn btn-sm btn-danger"
onclick="deleteIP(${ip.id},${resourceId})">
X
</button>
<button onclick="editIP(${ip.id})">Edit</button>
<button onclick="deleteIP(${ip.id})">Delete</button>
</div>
`;
`
});
container.innerHTML=html;
}
async function addIP(resourceId){
const ip=document.getElementById("ip-"+resourceId).value;
const type=document.getElementById("type-"+resourceId).value;
const comment=document.getElementById("comment-"+resourceId).value;
await fetch(`${API}/resources/${resourceId}/ips`,{
method:"POST",
headers:{
"Content-Type":"application/json"
},
body:JSON.stringify({
ip,
type,
comment
})
});
document.getElementById("ip-"+resourceId).value="";
document.getElementById("comment-"+resourceId).value="";
loadIPs(resourceId);
}
async function deleteIP(ipId,resourceId){
html+=`<button onclick="addIP(${r.id})">Add IP</button>`
await fetch(`${API}/ips/${ipId}`,{
method:"DELETE"
});
loadIPs(resourceId);
return html
}
document.addEventListener("DOMContentLoaded", () => {
loadResources();
});
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>
<button onclick='editResource(${JSON.stringify(r)})'>Edit</button>
<button onclick="deleteResource(${r.id})">Delete</button>
</td>
</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)
})
}
loadResources()
</script>
</body>
</html>
+21
View File
@@ -7,6 +7,17 @@ const ipRoutes = require("./routes/ips");
const app = express();
process.on("uncaughtException", (err) => {
console.error("UNCAUGHT EXCEPTION");
console.error(err);
});
process.on("unhandledRejection", (err) => {
console.error("UNHANDLED PROMISE REJECTION");
console.error(err);
});
app.use(cors());
app.use(express.json());
@@ -18,6 +29,16 @@ app.use("/resman/api/resources", resourceRoutes);
app.use("/resman/api", ipRoutes);
app.use((err, req, res, next) => {
console.error("EXPRESS ERROR:");
console.error(err);
res.status(500).json({
error: "Internal Server Error"
});
});
app.listen(3000, () => {
console.log("ResMan running on port 3000");