Improve domain UX and migration startup
This commit is contained in:
@@ -1,6 +1,34 @@
|
||||
const db = require("./db");
|
||||
const migrations = require("./migrations");
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForDatabase({
|
||||
retries = 30,
|
||||
delayMs = 2000,
|
||||
} = {}) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
await db.query("SELECT 1");
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
console.log(`Database not ready yet (${attempt}/${retries})`);
|
||||
|
||||
if (attempt < retries) {
|
||||
await sleep(delayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function ensureMigrationsTable() {
|
||||
await db.query(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
@@ -21,6 +49,7 @@ async function getAppliedMigrationIds() {
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
await waitForDatabase();
|
||||
await ensureMigrationsTable();
|
||||
|
||||
const appliedIds = await getAppliedMigrationIds();
|
||||
@@ -43,4 +72,5 @@ async function runMigrations() {
|
||||
|
||||
module.exports = {
|
||||
runMigrations,
|
||||
waitForDatabase,
|
||||
};
|
||||
|
||||
@@ -52,6 +52,19 @@ button:hover {
|
||||
background: #1e40af;
|
||||
}
|
||||
|
||||
button.btn-small{
|
||||
font-size:11px;
|
||||
padding:4px 8px;
|
||||
}
|
||||
|
||||
button.btn-danger{
|
||||
background:#dc2626;
|
||||
}
|
||||
|
||||
button.btn-danger:hover{
|
||||
background:#b91c1c;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
padding: 6px;
|
||||
margin: 4px;
|
||||
@@ -129,7 +142,104 @@ input, textarea {
|
||||
margin:-6px 0 12px 0;
|
||||
}
|
||||
|
||||
.domain-name{
|
||||
font-weight:700;
|
||||
color:#0f172a;
|
||||
margin-bottom:6px;
|
||||
}
|
||||
|
||||
.domain-meta{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:6px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
|
||||
.meta-chip{
|
||||
font-size:12px;
|
||||
background:#eff6ff;
|
||||
color:#1e3a8a;
|
||||
padding:3px 8px;
|
||||
border-radius:999px;
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.meta-chip.muted{
|
||||
background:#f1f5f9;
|
||||
color:#475569;
|
||||
}
|
||||
|
||||
.dns-badge{
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
gap:6px;
|
||||
font-size:12px;
|
||||
font-weight:600;
|
||||
border-radius:999px;
|
||||
padding:4px 10px;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.dns-badge.ok{
|
||||
background:#dcfce7;
|
||||
color:#166534;
|
||||
}
|
||||
|
||||
.dns-badge.warn{
|
||||
background:#fef3c7;
|
||||
color:#92400e;
|
||||
}
|
||||
|
||||
.dns-badge.off{
|
||||
background:#fee2e2;
|
||||
color:#991b1b;
|
||||
}
|
||||
|
||||
.dns-detail{
|
||||
display:block;
|
||||
font-size:11px;
|
||||
color:#64748b;
|
||||
margin-top:4px;
|
||||
}
|
||||
|
||||
.domain-actions{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:4px;
|
||||
}
|
||||
|
||||
.subdomain-list{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:8px;
|
||||
}
|
||||
|
||||
.subdomain-card{
|
||||
background:#f8fafc;
|
||||
border:1px solid #e2e8f0;
|
||||
border-radius:10px;
|
||||
padding:8px 10px;
|
||||
}
|
||||
|
||||
.subdomain-row{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:flex-start;
|
||||
gap:8px;
|
||||
}
|
||||
|
||||
.subdomain-name{
|
||||
font-size:13px;
|
||||
font-weight:600;
|
||||
color:#0f172a;
|
||||
}
|
||||
|
||||
.subdomain-meta{
|
||||
display:flex;
|
||||
flex-wrap:wrap;
|
||||
gap:6px;
|
||||
margin-top:6px;
|
||||
}
|
||||
|
||||
.subdomain{
|
||||
margin-left:15px;
|
||||
@@ -235,3 +345,42 @@ body.dark .ip.public{
|
||||
body.dark .modal-meta{
|
||||
color:#94a3b8;
|
||||
}
|
||||
|
||||
body.dark .domain-name,
|
||||
body.dark .subdomain-name{
|
||||
color:#f8fafc;
|
||||
}
|
||||
|
||||
body.dark .meta-chip{
|
||||
background:#1d4ed8;
|
||||
color:#eff6ff;
|
||||
}
|
||||
|
||||
body.dark .meta-chip.muted{
|
||||
background:#334155;
|
||||
color:#cbd5e1;
|
||||
}
|
||||
|
||||
body.dark .subdomain-card{
|
||||
background:#111827;
|
||||
border-color:#374151;
|
||||
}
|
||||
|
||||
body.dark .dns-badge.ok{
|
||||
background:#14532d;
|
||||
color:#dcfce7;
|
||||
}
|
||||
|
||||
body.dark .dns-badge.warn{
|
||||
background:#78350f;
|
||||
color:#fde68a;
|
||||
}
|
||||
|
||||
body.dark .dns-badge.off{
|
||||
background:#7f1d1d;
|
||||
color:#fecaca;
|
||||
}
|
||||
|
||||
body.dark .dns-detail{
|
||||
color:#94a3b8;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,28 @@ async function getDNS(domain){
|
||||
|
||||
}
|
||||
|
||||
function dnsStatusMarkup(expectedIp, ips){
|
||||
|
||||
if(!ips.length){
|
||||
return `
|
||||
<span class="dns-badge off">Kein DNS</span>
|
||||
`
|
||||
}
|
||||
|
||||
const matches = expectedIp && ips.includes(expectedIp)
|
||||
const badgeClass = matches ? "ok" : "warn"
|
||||
const label = matches ? "DNS OK" : "DNS Abweichung"
|
||||
|
||||
return `
|
||||
<span class="dns-badge ${badgeClass}">${label}</span>
|
||||
<span class="dns-detail">${ips.join(", ")}</span>
|
||||
`
|
||||
}
|
||||
|
||||
function chipMarkup(label, muted = false){
|
||||
return `<span class="meta-chip${muted ? " muted" : ""}">${label}</span>`
|
||||
}
|
||||
|
||||
|
||||
/* =========================
|
||||
DOMAINS + SUBDOMAINS
|
||||
@@ -48,6 +70,7 @@ window.loadDomains = async function(){
|
||||
const domains = await api(API + "/domains")
|
||||
const subs = await api(API + "/subdomains")
|
||||
window.domainList = domains
|
||||
const resources = window.resources || []
|
||||
|
||||
const table = document.getElementById("domains")
|
||||
table.innerHTML = ""
|
||||
@@ -55,41 +78,58 @@ window.loadDomains = async function(){
|
||||
for(const d of domains){
|
||||
|
||||
const tr = document.createElement("tr")
|
||||
const domainSubs = subs.filter(s => s.domain_id === d.id)
|
||||
|
||||
// Subdomains Liste
|
||||
const sublist = subs
|
||||
.filter(s => s.domain_id === d.id)
|
||||
.map(s => `
|
||||
<div class="subdomain">
|
||||
↳ ${s.subdomain}.${s.domain_name}
|
||||
<span id="subdns-${s.id}">...</span>
|
||||
<span id="subserver-${s.id}" class="serverlink"></span>
|
||||
|
||||
<button onclick='openSubEdit(${JSON.stringify(s)})'>Edit</button>
|
||||
<button onclick="deleteSub(${s.id})">Delete</button>
|
||||
const sublist = domainSubs.length
|
||||
? `
|
||||
<div class="subdomain-list">
|
||||
${domainSubs.map(s => `
|
||||
<div class="subdomain-card">
|
||||
<div class="subdomain-row">
|
||||
<div>
|
||||
<div class="subdomain-name">${s.subdomain}.${s.domain_name}</div>
|
||||
<div class="subdomain-meta">
|
||||
${s.ip_address ? chipMarkup(s.ip_address, true) : chipMarkup("Keine IP", true)}
|
||||
<span id="subserver-${s.id}"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domain-actions">
|
||||
<button class="btn-small" onclick='openSubEdit(${JSON.stringify(s)})'>Edit</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteSub(${s.id})">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="subdns-${s.id}" style="margin-top:8px">...</div>
|
||||
</div>
|
||||
`).join("")}
|
||||
</div>
|
||||
`).join("")
|
||||
`
|
||||
: `<div class="dns-detail">Noch keine Subdomains</div>`
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<button onclick="moveDomain(${d.id}, 'up')">⬆</button>
|
||||
<button onclick="moveDomain(${d.id}, 'down')">⬇</button>
|
||||
<button class="btn-small" onclick="moveDomain(${d.id}, 'up')">⬆</button>
|
||||
<button class="btn-small" onclick="moveDomain(${d.id}, 'down')">⬇</button>
|
||||
</td>
|
||||
<td>
|
||||
${d.domain_name}
|
||||
<div class="domain-name">${d.domain_name}</div>
|
||||
<div class="domain-meta">
|
||||
${d.provider ? chipMarkup(d.provider) : ""}
|
||||
${d.ip_address ? chipMarkup(d.ip_address, true) : chipMarkup("Keine Ziel-IP", true)}
|
||||
${d.resource_name ? chipMarkup(d.resource_name) : chipMarkup("Kein Server", true)}
|
||||
</div>
|
||||
${sublist}
|
||||
</td>
|
||||
|
||||
<td><span class="provider">${d.provider || ""}</span></td>
|
||||
<td><span class="provider">${d.provider || "?"}</span></td>
|
||||
<td>${d.ip_address || ""}</td>
|
||||
<td>${d.resource_name || ""}</td>
|
||||
<td id="dns-${d.id}">...</td>
|
||||
<td>${d.yearly_cost || ""}</td>
|
||||
|
||||
<td>
|
||||
<button onclick='openDomainEdit(${JSON.stringify(d)})'>Edit</button>
|
||||
<button onclick="deleteDomain(${d.id})">Delete</button>
|
||||
<button onclick="openSubCreate(${d.id})">+ Subdomain</button>
|
||||
<td class="domain-actions">
|
||||
<button class="btn-small" onclick='openDomainEdit(${JSON.stringify(d)})'>Bearbeiten</button>
|
||||
<button class="btn-small" onclick="openSubCreate(${d.id})">Subdomain</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteDomain(${d.id})">Loeschen</button>
|
||||
</td>
|
||||
`
|
||||
|
||||
@@ -99,9 +139,7 @@ window.loadDomains = async function(){
|
||||
SERVER ZUORDNUNG SUBS
|
||||
========================= */
|
||||
|
||||
const resources = window.resources || []
|
||||
|
||||
subs.forEach(s => {
|
||||
domainSubs.forEach(s => {
|
||||
|
||||
const server = resources.find(r =>
|
||||
Array.isArray(r.ips) &&
|
||||
@@ -112,7 +150,7 @@ window.loadDomains = async function(){
|
||||
|
||||
const el = document.getElementById("subserver-"+s.id)
|
||||
if(el){
|
||||
el.innerText = " → " + server.name
|
||||
el.innerHTML = chipMarkup(server.name)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -128,23 +166,11 @@ window.loadDomains = async function(){
|
||||
|
||||
const ips = await getDNS(d.domain_name)
|
||||
|
||||
let status = "❌"
|
||||
|
||||
if(ips.length){
|
||||
|
||||
if(d.ip_address && ips.includes(d.ip_address)){
|
||||
status = "✅ " + ips.join(", ")
|
||||
}else{
|
||||
status = "⚠ " + ips.join(", ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.getElementById("dns-"+d.id).innerHTML = status
|
||||
document.getElementById("dns-"+d.id).innerHTML = dnsStatusMarkup(d.ip_address, ips)
|
||||
|
||||
}catch(e){
|
||||
|
||||
document.getElementById("dns-"+d.id).innerHTML = "❌"
|
||||
document.getElementById("dns-"+d.id).innerHTML = `<span class="dns-badge off">Kein DNS</span>`
|
||||
|
||||
}
|
||||
|
||||
@@ -153,9 +179,7 @@ window.loadDomains = async function(){
|
||||
DNS CHECK SUBDOMAINS
|
||||
========================= */
|
||||
|
||||
subs
|
||||
.filter(s => s.domain_id === d.id)
|
||||
.forEach(async s => {
|
||||
domainSubs.forEach(async s => {
|
||||
|
||||
const full = s.subdomain + "." + s.domain_name
|
||||
|
||||
@@ -163,23 +187,11 @@ window.loadDomains = async function(){
|
||||
|
||||
const ips = await getDNS(full)
|
||||
|
||||
let status = "❌"
|
||||
|
||||
if(ips.length){
|
||||
|
||||
if(s.ip_address && ips.includes(s.ip_address)){
|
||||
status = "✅ " + ips.join(", ")
|
||||
}else{
|
||||
status = "⚠ " + ips.join(", ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
document.getElementById("subdns-"+s.id).innerHTML = status
|
||||
document.getElementById("subdns-"+s.id).innerHTML = dnsStatusMarkup(s.ip_address, ips)
|
||||
|
||||
}catch(e){
|
||||
|
||||
document.getElementById("subdns-"+s.id).innerHTML = "❌"
|
||||
document.getElementById("subdns-"+s.id).innerHTML = `<span class="dns-badge off">Kein DNS</span>`
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user