feat: Servereinstellungen-Seite für Admins

- Neuer Endpoint GET/PUT /api/admin/server-settings
  - Liest appsettings.Production.json (Fallback: appsettings.json → Standardwert)
  - Schreibt immer in appsettings.Production.json
  - Definierte Settings: AnthropicToken, MaxIterations, PayloadLogging, PayloadLoggingGzip, KreaToken, KreaApiKey
- Neue Frontend-Seite 'server-settings' (Admin-only)
  - Dynamisches Formular basierend auf Setting-Definitionen vom Backend
  - Password-Felder mit Eye-Toggle, Checkboxen für Booleans, Number-Inputs
  - Hinweis dass Neustart erforderlich ist
- Dashboard: Neuer Link 'Servereinstellungen' für Admins
- appsettings.Production.json vom Publish ausgeschlossen (CopyToPublishDirectory=Never)
This commit is contained in:
Meleki committed 2026-03-06 22:32:33 +01:00
1 parent af2f821c72
commit bfd78278ab
8 files changed
+411 -3

No files matched your search

+21
View File
@@ -490,6 +490,27 @@ async function apiAdminGetLoginLink(userId) {
return resp.json();
}
// ── Server Settings API (Admin) ──
async function apiGetServerSettings() {
const resp = await fetch('/api/admin/server-settings');
if (!resp.ok) throw new Error('Fehler beim Laden der Servereinstellungen');
return resp.json();
}
async function apiSaveServerSettings(data) {
const resp = await fetch('/api/admin/server-settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error || 'Fehler beim Speichern');
}
return resp.json();
}
// ── Krea Image API ──
async function apiKreaStatus() {
+7 -1
View File
@@ -14,14 +14,20 @@ registerPage('dashboard', {
navigateTo('profile');
});
// Admin-Link (nur für Admins sichtbar)
// Admin-Links (nur für Admins sichtbar)
var adminLink = document.getElementById('dashboardAdminLink');
var serverSettingsLink = document.getElementById('dashboardServerSettingsLink');
if (currentUser && currentUser.isAdmin) {
adminLink.hidden = false;
adminLink.addEventListener('click', (e) => {
e.preventDefault();
navigateTo('admin');
});
serverSettingsLink.hidden = false;
serverSettingsLink.addEventListener('click', (e) => {
e.preventDefault();
navigateTo('server-settings');
});
}
// Globaler Click-Handler zum Schließen aller Card-Menüs
+145
View File
@@ -0,0 +1,145 @@
// ── Server Settings Page (Admin) ──
registerPage('server-settings', {
async init() {
if (!currentUser || !currentUser.isAdmin) {
navigateTo('dashboard');
return;
}
var container = document.getElementById('serverSettingsFields');
var saveBtn = document.getElementById('serverSettingsSaveBtn');
var messageEl = document.getElementById('serverSettingsMessage');
var inputs = {};
function showMessage(text, isError) {
messageEl.textContent = text;
messageEl.className = 'profile-message ' + (isError ? 'profile-message-error' : 'profile-message-success');
messageEl.hidden = false;
if (!isError) setTimeout(function() { messageEl.hidden = true; }, 4000);
}
// Settings laden und Formular aufbauen
try {
var result = await apiGetServerSettings();
var settings = result.settings;
for (var i = 0; i < settings.length; i++) {
var s = settings[i];
var field = document.createElement('div');
field.className = 'form-field';
var label = document.createElement('label');
label.textContent = s.label;
label.setAttribute('for', 'ss-' + s.key);
field.appendChild(label);
if (s.inputType === 'toggle') {
var toggleLabel = document.createElement('label');
toggleLabel.className = 'toggle-label';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.id = 'ss-' + s.key;
cb.checked = !!s.value;
inputs[s.key] = cb;
var span = document.createElement('span');
span.textContent = s.value ? 'Aktiviert' : 'Deaktiviert';
cb.addEventListener('change', (function(sp, checkbox) {
return function() { sp.textContent = checkbox.checked ? 'Aktiviert' : 'Deaktiviert'; };
})(span, cb));
toggleLabel.appendChild(cb);
toggleLabel.appendChild(span);
field.appendChild(toggleLabel);
} else if (s.inputType === 'password') {
var wrapper = document.createElement('div');
wrapper.className = 'input-with-btn';
var inp = document.createElement('input');
inp.type = 'password';
inp.id = 'ss-' + s.key;
inp.value = s.value || '';
inp.placeholder = s.label;
inp.autocomplete = 'off';
inputs[s.key] = inp;
var toggleBtn = document.createElement('button');
toggleBtn.type = 'button';
toggleBtn.className = 'icon-btn';
toggleBtn.innerHTML = '<i class="fa fa-eye"></i>';
toggleBtn.addEventListener('click', (function(input, btn) {
return function() {
if (input.type === 'password') {
input.type = 'text';
btn.innerHTML = '<i class="fa fa-eye-slash"></i>';
} else {
input.type = 'password';
btn.innerHTML = '<i class="fa fa-eye"></i>';
}
};
})(inp, toggleBtn));
wrapper.appendChild(inp);
wrapper.appendChild(toggleBtn);
field.appendChild(wrapper);
} else if (s.inputType === 'number') {
var inp = document.createElement('input');
inp.type = 'number';
inp.id = 'ss-' + s.key;
inp.value = s.value != null ? s.value : '';
inp.placeholder = s.label;
inputs[s.key] = inp;
field.appendChild(inp);
} else {
var inp = document.createElement('input');
inp.type = 'text';
inp.id = 'ss-' + s.key;
inp.value = s.value || '';
inp.placeholder = s.label;
inputs[s.key] = inp;
field.appendChild(inp);
}
container.appendChild(field);
}
// Speichern
saveBtn.addEventListener('click', async function() {
saveBtn.disabled = true;
messageEl.hidden = true;
try {
var data = {};
for (var j = 0; j < settings.length; j++) {
var def = settings[j];
var input = inputs[def.key];
if (!input) continue;
if (def.type === 'bool') {
data[def.key] = input.checked;
} else if (def.type === 'int') {
data[def.key] = parseInt(input.value, 10) || 0;
} else {
data[def.key] = input.value;
}
}
var result = await apiSaveServerSettings(data);
showMessage(result.hint || 'Einstellungen gespeichert', false);
} catch (err) {
showMessage(err.message, true);
} finally {
saveBtn.disabled = false;
}
});
} catch (err) {
container.innerHTML = '<p class="form-error">Fehler beim Laden: ' + esc(err.message) + '</p>';
}
},
destroy() {}
});