Erstelle die Grundstruktur für die Ceph Max Storage Rechner-Anwendung mit Flask und HTMX. Füge Konfigurationsdateien, Routen, Modelle, Templates und statische Dateien hinzu. Implementiere grundlegende Funktionen zur Berechnung der Speichernutzung und zur PDF-Generierung. Integriere CSRF-Schutz und Logging. Stelle sicher, dass die Anwendung modular und wartbar ist.

This commit is contained in:
Samuel Müller
2025-03-26 08:40:32 +01:00
commit f00c1f48e9
26 changed files with 1744 additions and 0 deletions

104
app/static/css/styles.css Normal file
View File

@ -0,0 +1,104 @@
/* Zusätzliche Stile für den Ceph-Rechner */
/* Sanfter Farbübergang im Hintergrund */
body {
background: linear-gradient(135deg, #f0f4f8 0%, #d9e2ec 100%);
}
/* Grundlegende Formular-Styles */
.form-control, input, select, textarea {
background-color: #ffffff !important;
border: 1px solid #e2e8f0 !important;
color: #2d3748 !important;
}
.form-control:focus, input:focus, select:focus, textarea:focus {
background-color: #ffffff !important;
border-color: #4299e1 !important;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25) !important;
}
.form-control::placeholder, input::placeholder, select::placeholder, textarea::placeholder {
color: #a0aec0 !important;
}
select option {
background-color: #ffffff !important;
color: #2d3748 !important;
}
/* Bessere Lesbarkeit für Ergebnisse */
.result-value {
font-variant-numeric: tabular-nums;
}
/* Hover-Effekte für Karten */
.card-hover {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* Light Mode Anpassungen */
@media (prefers-color-scheme: light) {
.form-control, input, select, textarea {
background-color: #ffffff !important;
border: 1px solid #e2e8f0 !important;
color: #2d3748 !important;
}
.form-control:focus, input:focus, select:focus, textarea:focus {
background-color: #ffffff !important;
border-color: #4299e1 !important;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.25) !important;
}
.form-control::placeholder, input::placeholder, select::placeholder, textarea::placeholder {
color: #a0aec0 !important;
}
select option {
background-color: #ffffff !important;
color: #2d3748 !important;
}
}
/* Darkmode Anpassungen */
@media (prefers-color-scheme: dark) {
body {
background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%);
}
/* Eingabefelder im Darkmode */
.form-control, input, select, textarea {
background-color: #2d3748 !important;
border: 1px solid #4a5568 !important;
color: #e2e8f0 !important;
}
.form-control:focus, input:focus, select:focus, textarea:focus {
background-color: #2d3748 !important;
border-color: #63b3ed !important;
box-shadow: 0 0 0 3px rgba(99, 179, 237, 0.25) !important;
}
.form-control::placeholder, input::placeholder, select::placeholder, textarea::placeholder {
color: #718096 !important;
}
select option {
background-color: #2d3748 !important;
color: #e2e8f0 !important;
}
}
/* Responsive Anpassungen */
@media (max-width: 640px) {
.container {
padding-left: 1rem;
padding-right: 1rem;
}
}

230
app/static/js/script.js Normal file
View File

@ -0,0 +1,230 @@
function cephCalculator() {
return {
replicationType: 'replication',
replicas: 3,
minSize: 2,
k: 4,
m: 2,
storageUnit: 'TB',
nodes: [
{
osd_count: 4,
osd_size_gb: 1000
}
],
result: {
max_usage_percent: 0,
max_usage_gb: 0,
max_usage_tb: 0,
raw_total: 0
},
isCalculating: false,
init() {
const savedConfig = localStorage.getItem('lastConfig');
if (savedConfig) {
const config = JSON.parse(savedConfig);
Object.assign(this, config);
}
},
addNode() {
this.nodes.push({
osd_count: 4,
osd_size_gb: this.storageUnit === 'TB' ? 1000 : 1
});
},
removeNode(nodeIndex) {
this.nodes.splice(nodeIndex, 1);
if (this.nodes.length === 0) {
this.addNode();
}
},
getOsdSizeInGB(size) {
return this.storageUnit === 'TB' ? size * 1024 : size;
},
calculateCapacity() {
const data = {
replication_type: this.replicationType,
replicas: parseInt(this.replicas),
k: parseInt(this.k),
m: parseInt(this.m),
nodes: this.nodes.map(node => ({
osd_count: parseInt(node.osd_count),
osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb))
})),
min_size: parseInt(this.minSize),
storage_unit: this.storageUnit
};
this.isCalculating = true;
fetch('/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
this.result = data;
})
.catch(error => {
console.error('Fehler bei der Berechnung:', error);
alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.');
})
.finally(() => {
this.isCalculating = false;
});
},
exportConfig() {
const config = {
replicationType: this.replicationType,
replicas: this.replicas,
minSize: this.minSize,
k: this.k,
m: this.m,
storageUnit: this.storageUnit,
nodes: this.nodes,
result: this.result
};
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ceph-config.json';
a.click();
URL.revokeObjectURL(url);
},
importConfig(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const config = JSON.parse(e.target.result);
Object.assign(this, config);
// Berechne automatisch nach dem Import
this.calculateCapacity();
} catch (error) {
alert('Fehler beim Importieren der Konfiguration. Bitte überprüfen Sie das Dateiformat.');
}
};
reader.readAsText(file);
},
generatePDF() {
if (!this.result.raw_total) {
alert('Bitte führen Sie zuerst eine Berechnung durch.');
return;
}
// Validiere die Daten vor dem Senden
if (!this.nodes || this.nodes.length === 0) {
alert('Bitte fügen Sie mindestens einen Node hinzu.');
return;
}
// Validiere die Node-Daten
for (let i = 0; i < this.nodes.length; i++) {
const node = this.nodes[i];
if (!node.osd_count || node.osd_count < 1) {
alert(`Bitte geben Sie eine gültige Anzahl von OSDs für Node ${i + 1} ein.`);
return;
}
if (!node.osd_size_gb || node.osd_size_gb <= 0) {
alert(`Bitte geben Sie eine gültige OSD-Größe für Node ${i + 1} ein.`);
return;
}
}
const data = {
replication_type: this.replicationType,
replicas: parseInt(this.replicas),
min_size: parseInt(this.minSize),
k: parseInt(this.k),
m: parseInt(this.m),
storage_unit: this.storageUnit,
nodes: this.nodes.map(node => ({
osd_count: parseInt(node.osd_count),
osd_size_gb: parseFloat(this.getOsdSizeInGB(parseFloat(node.osd_size_gb)))
})),
result: {
raw_total: parseFloat(this.result.raw_total),
max_usage_percent: parseFloat(this.result.max_usage_percent),
max_usage_gb: parseFloat(this.result.max_usage_gb),
max_usage_tb: parseFloat(this.result.max_usage_tb),
node_failure_tolerance: Boolean(this.result.node_failure_tolerance),
node_failure_info: String(this.result.node_failure_info || ''),
multi_failure_tolerance: Boolean(this.result.multi_failure_tolerance),
max_failure_nodes: parseInt(this.result.max_failure_nodes || 0),
osd_failure_tolerance: Boolean(this.result.osd_failure_tolerance),
osd_failure_info: String(this.result.osd_failure_info || '')
}
};
// Zeige Ladeindikator
const button = document.querySelector('button[onclick="generatePDF()"]');
const originalText = button.innerHTML;
button.innerHTML = '<span class="flex items-center"><svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>Generiere PDF...</span>';
button.disabled = true;
// Hole CSRF-Token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (!csrfToken) {
alert('CSRF-Token nicht gefunden. Bitte laden Sie die Seite neu.');
button.innerHTML = originalText;
button.disabled = false;
return;
}
fetch('/generate-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
})
.then(response => {
if (!response.ok) {
return response.json().then(err => {
throw new Error(err.error || `HTTP error! status: ${response.status}`);
});
}
return response.blob();
})
.then(blob => {
if (!blob || blob.size === 0) {
throw new Error('Die generierte PDF-Datei ist leer');
}
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ceph-report.pdf';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
})
.catch(error => {
console.error('Fehler bei der PDF-Generierung:', error);
alert(`Fehler bei der PDF-Generierung: ${error.message}`);
})
.finally(() => {
// Stelle den Button wieder her
button.innerHTML = originalText;
button.disabled = false;
});
}
};
}

167
app/static/js/script.js.bak Normal file
View File

@ -0,0 +1,167 @@
function cephCalculator() {
return {
replicationType: 'replication',
replicas: 3,
minSize: 2,
k: 4,
m: 2,
storageUnit: 'TB',
nodes: [
{
osd_count: 4,
osd_size_gb: 1000
}
],
result: {
max_usage_percent: 0,
max_usage_gb: 0,
max_usage_tb: 0,
raw_total: 0
},
isCalculating: false,
init() {
const savedConfig = localStorage.getItem('lastConfig');
if (savedConfig) {
const config = JSON.parse(savedConfig);
Object.assign(this, config);
}
},
addNode() {
this.nodes.push({
osd_count: 4,
osd_size_gb: this.storageUnit === 'TB' ? 1000 : 1
});
},
removeNode(nodeIndex) {
this.nodes.splice(nodeIndex, 1);
if (this.nodes.length === 0) {
this.addNode();
}
},
getOsdSizeInGB(size) {
return this.storageUnit === 'TB' ? size * 1024 : size;
},
calculateCapacity() {
const data = {
replication_type: this.replicationType,
replicas: parseInt(this.replicas),
k: parseInt(this.k),
m: parseInt(this.m),
nodes: this.nodes.map(node => ({
osd_count: parseInt(node.osd_count),
osd_size_gb: this.getOsdSizeInGB(parseFloat(node.osd_size_gb))
})),
min_size: parseInt(this.minSize),
storage_unit: this.storageUnit
};
this.isCalculating = true;
fetch('/calculate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
this.result = data;
})
.catch(error => {
console.error('Fehler bei der Berechnung:', error);
alert('Es ist ein Fehler bei der Berechnung aufgetreten. Bitte versuchen Sie es erneut.');
})
.finally(() => {
this.isCalculating = false;
});
},
exportConfig() {
const config = {
replicationType: this.replicationType,
replicas: this.replicas,
minSize: this.minSize,
k: this.k,
m: this.m,
storageUnit: this.storageUnit,
nodes: this.nodes,
result: this.result
};
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ceph-config.json';
a.click();
URL.revokeObjectURL(url);
},
importConfig(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const config = JSON.parse(e.target.result);
Object.assign(this, config);
// Berechne automatisch nach dem Import
this.calculateCapacity();
} catch (error) {
alert('Fehler beim Importieren der Konfiguration. Bitte überprüfen Sie das Dateiformat.');
}
};
reader.readAsText(file);
},
generatePDF() {
if (!this.result.raw_total) {
alert('Bitte führen Sie zuerst eine Berechnung durch.');
return;
}
const data = {
replication_type: this.replicationType,
replicas: this.replicas,
min_size: this.minSize,
k: this.k,
m: this.m,
storage_unit: this.storageUnit,
nodes: this.nodes,
result: this.result
};
fetch('/generate-pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
},
body: JSON.stringify(data)
})
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ceph-report.pdf';
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
})
.catch(error => {
console.error('Fehler bei der PDF-Generierung:', error);
alert('Es ist ein Fehler bei der PDF-Generierung aufgetreten. Bitte versuchen Sie es erneut.');
});
}
};
}