Ein Computer im Spiel. Man tritt an ein Terminal, drueckt E, und bekommt eine
Oberflaeche mit Mail, Browser, Adressbuch, Kalender und weiteren Anwendungen.
Andere Resources haengen sich als App ein.
Auf ESX umgestellt:
- Fuenf harte Abhaengigkeiten zeigten auf Resources, die es hier nicht gibt.
Eine fehlende Abhaengigkeit verhindert den Start vollstaendig - die
Resource waere nie hochgekommen.
- Die Bruecke zum Framework neu geschrieben; Charakterdaten, Aktenverwaltung
und Immobilien ausgebaut, weil die zugehoerigen Systeme fehlen.
- pc_live_devices fehlte im Schema, wird vom Code aber gelesen. Ohne die
Tabelle ist kein fester PC ansprechbar. Ein PC braucht ausserdem zwei
Eintraege mit derselben Kennung: Standort in der Datenbank, Kamerafahrt in
der config.lua.
Mail ueberarbeitet:
- Postfaecher haengen an einer Anmeldung statt an der Person. Mehrere Leute
koennen dasselbe Firmenpostfach gleichzeitig offen haben.
- Ordner gehoeren zum Postfach, nicht zur Person. Eine Mail an ein geteiltes
Postfach wird je Empfaenger einmal gespeichert; damit das Einsortieren
trotzdem fuer alle gilt, tragen alle Kopien einer Zustellung dieselbe
Kennung. Gelesen und geloescht bleibt persoenlich - das ist keine
Eigenschaft der Nachricht, sondern der Person.
- Signaturen gehoeren ebenfalls zum Postfach.
- Kalender koennen privat, geteilt oder oeffentlich sein. Ein oeffentlicher
Termin haengt bewusst auch an einem Postfach, sonst koennte ihn spaeter
niemand mehr aendern oder absagen.
Neue Apps: Webhosting, Bleeter und dessen Verwaltung.
Behoben:
- prompt(), confirm() und alert() froren das NUI ein. FiveMs CEF hat keinen
Handler fuer die eingebauten Browserdialoge - der Aufruf oeffnet nichts und
kehrt nie zurueck. Ersetzt durch eigene Dialoge.
- Die Fenster tragen data-wid, keine id. Wer sie mit getElementById sucht,
schreibt ins Leere und das Fenster bleibt leer.
Enthaelt README.md mit Einrichtung und PLUGINS.md: eine Anleitung, wie man eine
eigene App baut und einhaengt, mit vollstaendigem Beispiel von der Tabelle bis
zum Schreibtischsymbol.
1489 lines
64 KiB
JavaScript
1489 lines
64 KiB
JavaScript
/**
|
||
* pc-live | Webhosting (ic-web)
|
||
*
|
||
* Drei Teile in dieser Datei:
|
||
* ICWebNet – Transport zum Server (Anfrage-Id → Promise)
|
||
* ICWebRender – Blockrenderer, wird auch vom Browser benutzt
|
||
* WebhostingApp – die App: Anmeldung, Seiteneditor, Zugänge, Verwaltung
|
||
*
|
||
* Die gesamte Verwaltung läuft über Anmeldungen. Der Anbieter meldet sich mit
|
||
* admin@liveinvader.ls an, legt Domänen an und richtet je Domäne einen Zugang
|
||
* ein. Ein Zugang ist zugleich ein Postfach.
|
||
*
|
||
* WICHTIG: Inhalte kommen von Spielern. Sie werden ausschliesslich über
|
||
* textContent und setAttribute gesetzt, niemals über innerHTML. Ein
|
||
* eingeschleustes <script> wäre hier kein Darstellungsfehler, sondern
|
||
* Codeausführung im PC-Interface.
|
||
*/
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
Transport
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
const ICWebNet = (() => {
|
||
const waiting = new Map();
|
||
let seq = 0;
|
||
|
||
function call(op, ...args) {
|
||
return new Promise((resolve) => {
|
||
const reqId = 'w' + (++seq);
|
||
// null im Array würde beim Weg durch Lua eine Lücke reissen.
|
||
const safeArgs = args.map(a => (a === undefined || a === null) ? '' : a);
|
||
|
||
const timer = setTimeout(() => {
|
||
if (waiting.delete(reqId)) {
|
||
resolve({ ok: false, error: 'Keine Antwort vom Server.' });
|
||
}
|
||
}, 10000);
|
||
|
||
waiting.set(reqId, (payload) => {
|
||
clearTimeout(timer);
|
||
resolve(payload || { ok: false, error: 'Leere Antwort.' });
|
||
});
|
||
|
||
fetchNui('icweb', { reqId, op, args: safeArgs });
|
||
});
|
||
}
|
||
|
||
function onResponse(data) {
|
||
if (!data) return;
|
||
const fn = waiting.get(data.reqId);
|
||
if (!fn) return; // abgelaufen oder doppelt
|
||
waiting.delete(data.reqId);
|
||
fn(data.payload);
|
||
}
|
||
|
||
return { call, onResponse };
|
||
})();
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
Blockrenderer
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
const ICWebRender = (() => {
|
||
/* Nur ic://-Ziele und einfache Seitennamen sind erlaubt. Alles andere
|
||
(javascript:, data:, http:) wird als Text angezeigt statt verlinkt. */
|
||
function safeTarget(raw) {
|
||
const t = String(raw || '').trim();
|
||
if (/^ic:\/\/[a-z0-9.\-\/]+$/i.test(t)) return t;
|
||
if (/^[a-z0-9\-]+$/i.test(t)) return t; // Seitenname auf derselben Domäne
|
||
return null;
|
||
}
|
||
|
||
function el(tag, className, text) {
|
||
const n = document.createElement(tag);
|
||
if (className) n.className = className;
|
||
if (text !== undefined) n.textContent = text;
|
||
return n;
|
||
}
|
||
|
||
function renderBlock(b, ctx) {
|
||
switch (b.type) {
|
||
case 'heading': {
|
||
const lvl = [1, 2, 3].includes(Number(b.level)) ? Number(b.level) : 2;
|
||
return el('h' + lvl, 'icweb-h icweb-h' + lvl, b.text || '');
|
||
}
|
||
|
||
case 'text':
|
||
return el('p', 'icweb-p', b.text || '');
|
||
|
||
case 'image': {
|
||
const fig = el('figure', 'icweb-fig');
|
||
const img = el('img', 'icweb-img');
|
||
// src nur über setAttribute; der Server hat https + Hostliste geprüft.
|
||
img.setAttribute('src', String(b.url || ''));
|
||
// Ohne Referer laden: manche Bildhoster sperren Hotlinks anhand
|
||
// der Herkunft, und die eines NUI kennen sie nicht.
|
||
img.setAttribute('referrerpolicy', 'no-referrer');
|
||
img.setAttribute('alt', String(b.alt || ''));
|
||
img.setAttribute('loading', 'lazy');
|
||
img.addEventListener('error', () => {
|
||
img.remove();
|
||
fig.appendChild(el('div', 'icweb-img-broken', '🖼 Bild nicht erreichbar'));
|
||
});
|
||
fig.appendChild(img);
|
||
if (b.caption) fig.appendChild(el('figcaption', 'icweb-cap', b.caption));
|
||
return fig;
|
||
}
|
||
|
||
case 'list': {
|
||
const ul = el('ul', 'icweb-ul');
|
||
(b.items || []).forEach(i => ul.appendChild(el('li', null, String(i))));
|
||
return ul;
|
||
}
|
||
|
||
case 'button': {
|
||
const target = safeTarget(b.target);
|
||
if (!target) return el('p', 'icweb-p', String(b.label || ''));
|
||
|
||
const a = el('a', 'icweb-btn', b.label || '');
|
||
a.setAttribute('role', 'button');
|
||
a.addEventListener('click', () => {
|
||
const url = target.startsWith('ic://')
|
||
? target
|
||
: 'ic://' + (ctx && ctx.domain ? ctx.domain + '/' + target : target);
|
||
if (typeof BrowserApp !== 'undefined') BrowserApp.navigate(url);
|
||
});
|
||
return a;
|
||
}
|
||
|
||
case 'contact': {
|
||
const box = el('div', 'icweb-contact');
|
||
[['✉', b.email], ['☎', b.phone], ['📍', b.address]].forEach(([icon, value]) => {
|
||
if (!value) return;
|
||
const row = el('div', 'icweb-contact-row');
|
||
row.appendChild(el('span', 'icweb-contact-icon', icon));
|
||
row.appendChild(el('span', null, String(value)));
|
||
box.appendChild(row);
|
||
});
|
||
return box.childNodes.length ? box : null;
|
||
}
|
||
|
||
case 'divider':
|
||
return el('hr', 'icweb-hr');
|
||
}
|
||
return null; // unbekannter Typ: nichts anzeigen
|
||
}
|
||
|
||
/** Baut eine ganze Seite. Gibt ein DocumentFragment zurück. */
|
||
function renderPage(site) {
|
||
const frag = document.createDocumentFragment();
|
||
const wrap = el('div', 'icweb-site icweb-theme-' + (site.theme || 'clean'));
|
||
|
||
const head = el('div', 'icweb-sitehead');
|
||
head.appendChild(el('div', 'icweb-sitetitle', site.title || site.domain));
|
||
head.appendChild(el('div', 'icweb-sitedomain', 'ic://' + site.domain));
|
||
wrap.appendChild(head);
|
||
|
||
if (site.nav && site.nav.length > 1) {
|
||
const nav = el('nav', 'icweb-nav');
|
||
site.nav.forEach(p => {
|
||
const a = el('a', 'icweb-navlink' + (p.slug === site.page.slug ? ' active' : ''),
|
||
p.title || p.slug);
|
||
a.addEventListener('click', () => {
|
||
if (typeof BrowserApp !== 'undefined') BrowserApp.navigate('ic://' + site.domain + '/' + p.slug);
|
||
});
|
||
nav.appendChild(a);
|
||
});
|
||
wrap.appendChild(nav);
|
||
}
|
||
|
||
const body = el('div', 'icweb-body');
|
||
(site.page.blocks || []).forEach(b => {
|
||
const node = renderBlock(b, { domain: site.domain });
|
||
if (node) body.appendChild(node);
|
||
});
|
||
if (!body.childNodes.length) {
|
||
body.appendChild(el('p', 'icweb-p icweb-muted', 'Diese Seite hat noch keinen Inhalt.'));
|
||
}
|
||
wrap.appendChild(body);
|
||
|
||
const foot = el('div', 'icweb-foot');
|
||
const rep = el('a', 'icweb-report', '⚑ Seite melden');
|
||
rep.addEventListener('click', () => openReport(site.domain, site.page.slug));
|
||
foot.appendChild(rep);
|
||
wrap.appendChild(foot);
|
||
|
||
frag.appendChild(wrap);
|
||
return frag;
|
||
}
|
||
|
||
/* ── Melden ─────────────────────────────────────────────── */
|
||
function openReport(domain, slug) {
|
||
modal('Seite melden', (box, close) => {
|
||
box.appendChild(el('div', 'icweb-modal-sub',
|
||
'ic://' + domain + ' — was stimmt mit dieser Seite nicht?'));
|
||
|
||
const ta = el('textarea', 'icweb-input');
|
||
ta.setAttribute('rows', '4');
|
||
ta.setAttribute('placeholder', 'Kurze Begründung');
|
||
box.appendChild(ta);
|
||
|
||
const row = el('div', 'icweb-modal-actions');
|
||
const cancel = el('button', 'icweb-btn-ghost', 'Abbrechen');
|
||
cancel.addEventListener('click', close);
|
||
const send = el('button', 'icweb-btn-primary', 'Melden');
|
||
send.addEventListener('click', async () => {
|
||
send.disabled = true;
|
||
const res = await ICWebNet.call('report', domain, slug || 'home', ta.value);
|
||
close();
|
||
Desktop.showNotification(res.ok ? '⚑ Meldung eingegangen. Danke.'
|
||
: '⚠ ' + (res.error || 'Meldung fehlgeschlagen.'));
|
||
});
|
||
row.append(cancel, send);
|
||
box.appendChild(row);
|
||
setTimeout(() => ta.focus(), 0);
|
||
});
|
||
}
|
||
|
||
/* Kleiner Dialogbaukasten, den auch die App benutzt. */
|
||
function modal(title, build, wide) {
|
||
const back = el('div', 'icweb-modal-back');
|
||
const box = el('div', 'icweb-modal' + (wide ? ' wide' : ''));
|
||
box.appendChild(el('div', 'icweb-modal-title', title));
|
||
|
||
const close = () => back.remove();
|
||
build(box, close);
|
||
|
||
back.appendChild(box);
|
||
back.addEventListener('click', (e) => { if (e.target === back) close(); });
|
||
document.getElementById('app').appendChild(back);
|
||
return close;
|
||
}
|
||
|
||
/* ── Ersatz für prompt() und confirm() ──────────────────
|
||
In FiveMs CEF gibt es keinen Handler für die eingebauten JS-Dialoge:
|
||
prompt() und confirm() öffnen nichts und lassen das NUI stehen. Deshalb
|
||
alles über den eigenen Dialog. */
|
||
|
||
/** Texteingabe. cb(wert) bei OK, gar nicht bei Abbruch. */
|
||
function promptText(title, opts, cb) {
|
||
opts = opts || {};
|
||
modal(title, (box, close) => {
|
||
if (opts.hint) box.appendChild(el('div', 'icweb-modal-sub', opts.hint));
|
||
|
||
const input = el('input', 'icweb-input');
|
||
input.setAttribute('type', opts.password ? 'password' : 'text');
|
||
if (opts.placeholder) input.setAttribute('placeholder', opts.placeholder);
|
||
input.value = opts.value || '';
|
||
box.appendChild(input);
|
||
|
||
const err = el('div', 'wh-login-error');
|
||
box.appendChild(err);
|
||
|
||
const submit = () => {
|
||
const value = input.value.trim();
|
||
if (!value) { err.textContent = 'Bitte etwas eingeben.'; return; }
|
||
close();
|
||
cb(value);
|
||
};
|
||
|
||
const row = el('div', 'icweb-modal-actions');
|
||
const cancel = el('button', 'icweb-btn-ghost', 'Abbrechen');
|
||
cancel.addEventListener('click', close);
|
||
const go = el('button', 'icweb-btn-primary', opts.okLabel || 'OK');
|
||
go.addEventListener('click', submit);
|
||
row.append(cancel, go);
|
||
box.appendChild(row);
|
||
|
||
input.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); });
|
||
setTimeout(() => input.focus(), 0);
|
||
});
|
||
}
|
||
|
||
/** Rückfrage. cb() nur bei Bestätigung. */
|
||
function confirmBox(title, text, cb, opts) {
|
||
opts = opts || {};
|
||
modal(title, (box, close) => {
|
||
// Mehrzeilige Texte behalten ihre Umbrüche.
|
||
const p = el('div', 'icweb-modal-sub', text || '');
|
||
p.style.whiteSpace = 'pre-wrap';
|
||
box.appendChild(p);
|
||
|
||
const row = el('div', 'icweb-modal-actions');
|
||
const cancel = el('button', 'icweb-btn-ghost', 'Abbrechen');
|
||
cancel.addEventListener('click', close);
|
||
const go = el('button', opts.danger ? 'icweb-btn-danger' : 'icweb-btn-primary',
|
||
opts.okLabel || 'Ja');
|
||
go.addEventListener('click', () => { close(); cb(); });
|
||
row.append(cancel, go);
|
||
box.appendChild(row);
|
||
});
|
||
}
|
||
|
||
return { renderPage, renderBlock, openReport, el, modal, promptText, confirmBox };
|
||
})();
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
Die App
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
const WebhostingApp = (() => {
|
||
const WIN_ID = 'app-webhosting';
|
||
const el = ICWebRender.el;
|
||
const modal = ICWebRender.modal;
|
||
|
||
/* Muss zum Schema in ic-web/shared/blocks.lua passen. Weicht es ab, fällt
|
||
das Feld beim Speichern serverseitig weg – der Server entscheidet. */
|
||
const BLOCK_TYPES = {
|
||
heading: { label: 'Überschrift', icon: 'H',
|
||
fields: [ { key: 'text', type: 'text', label: 'Text' },
|
||
{ key: 'level', type: 'select', label: 'Größe',
|
||
options: [[1, 'Groß'], [2, 'Mittel'], [3, 'Klein']], default: 2 } ] },
|
||
text: { label: 'Textabsatz', icon: '¶',
|
||
fields: [ { key: 'text', type: 'area', label: 'Text', rows: 5 } ] },
|
||
image: { label: 'Bild', icon: '🖼',
|
||
fields: [ { key: 'url', type: 'text', label: 'Bildadresse (https)' },
|
||
{ key: 'alt', type: 'text', label: 'Bildbeschreibung' },
|
||
{ key: 'caption', type: 'text', label: 'Bildunterschrift' } ] },
|
||
list: { label: 'Aufzählung', icon: '•',
|
||
fields: [ { key: 'items', type: 'lines', label: 'Ein Eintrag je Zeile', rows: 5 } ] },
|
||
button: { label: 'Schaltfläche', icon: '▭',
|
||
fields: [ { key: 'label', type: 'text', label: 'Beschriftung' },
|
||
{ key: 'target', type: 'text', label: 'Ziel (Seitenname oder ic://…)' } ] },
|
||
contact: { label: 'Kontaktbox', icon: '☎',
|
||
fields: [ { key: 'email', type: 'text', label: 'E-Mail' },
|
||
{ key: 'phone', type: 'text', label: 'Telefon' },
|
||
{ key: 'address', type: 'text', label: 'Anschrift' } ] },
|
||
divider: { label: 'Trennlinie', icon: '—', fields: [] },
|
||
};
|
||
|
||
let info = null; // { session, themes, tlds, imageHosts, provider, selfService }
|
||
let current = null; // geladene Domäne im Editor
|
||
let currentSlug = 'home';
|
||
let view = 'domains';
|
||
let winEl = null; // das Fensterelement von WindowManager.create
|
||
|
||
const session = () => (info && info.session) || null;
|
||
const isProvider = () => { const s = session(); return s && s.role === 'superadmin'; };
|
||
const mayManage = () => { const s = session(); return s && (s.role === 'superadmin' || s.role === 'admin'); };
|
||
|
||
/* ── Fenster ──────────────────────────────────────────── */
|
||
async function open() {
|
||
const win = WindowManager.create({
|
||
id: WIN_ID, title: 'Webhosting', icon: '🌍', width: 1020, height: 680,
|
||
content: `
|
||
<div class="wh-root">
|
||
<div class="wh-side" id="wh-side"></div>
|
||
<div class="wh-main" id="wh-main"></div>
|
||
</div>`,
|
||
});
|
||
if (!win) return;
|
||
|
||
winEl = win;
|
||
await refreshSession();
|
||
}
|
||
|
||
async function refreshSession() {
|
||
setMain(loading('Verbinde…'));
|
||
const res = await ICWebNet.call('session');
|
||
info = res.ok ? res.data : null;
|
||
|
||
renderSide();
|
||
if (!info) return setMain(errorBox('Der Dienst ist nicht erreichbar.'));
|
||
if (!session()) return showLogin();
|
||
if (session().mustChange) return showForcedPassword();
|
||
view = 'domains';
|
||
renderSide();
|
||
showDomains();
|
||
}
|
||
|
||
// Der WindowManager vergibt keine id auf dem Fenster, sondern data-wid.
|
||
// Deshalb wird im zurueckgegebenen Element gesucht, nicht im Dokument.
|
||
function root(id) {
|
||
if (!winEl || !winEl.isConnected) {
|
||
winEl = document.querySelector('[data-wid="' + WIN_ID + '"]');
|
||
}
|
||
return winEl ? winEl.querySelector('#' + id) : null;
|
||
}
|
||
function setMain(node) {
|
||
const m = root('wh-main');
|
||
if (m) m.replaceChildren(node);
|
||
}
|
||
function loading(txt) {
|
||
const d = el('div', 'wh-empty');
|
||
d.appendChild(el('div', 'wh-empty-icon', '⏳'));
|
||
d.appendChild(el('div', null, txt));
|
||
return d;
|
||
}
|
||
function errorBox(txt) {
|
||
const d = el('div', 'wh-empty');
|
||
d.appendChild(el('div', 'wh-empty-icon', '⚠'));
|
||
d.appendChild(el('div', null, txt));
|
||
return d;
|
||
}
|
||
function notify(res, okText) {
|
||
Desktop.showNotification(res.ok ? '✔ ' + okText : '⚠ ' + (res.error || 'Fehlgeschlagen.'));
|
||
return res.ok;
|
||
}
|
||
|
||
/* ── Anmeldung ────────────────────────────────────────── */
|
||
function showLogin() {
|
||
const wrap = el('div', 'wh-login');
|
||
|
||
wrap.appendChild(el('div', 'wh-login-logo', '🌍'));
|
||
wrap.appendChild(el('div', 'wh-login-title', (info && info.provider) || 'IC Webhosting'));
|
||
wrap.appendChild(el('div', 'wh-login-sub',
|
||
'Melde dich mit deinem Zugang an. Zugänge vergibt die Administration.'));
|
||
|
||
const form = el('div', 'wh-form wh-login-form');
|
||
const user = field(form, 'Benutzername', 'text', '', 'name@domäne.ls');
|
||
const pass = field(form, 'Passwort', 'password', '');
|
||
|
||
const err = el('div', 'wh-login-error');
|
||
form.appendChild(err);
|
||
|
||
const go = el('button', 'icweb-btn-primary', 'Anmelden');
|
||
const submit = async () => {
|
||
err.textContent = '';
|
||
if (!user.value || !pass.value) {
|
||
err.textContent = 'Bitte Benutzername und Passwort eingeben.';
|
||
return;
|
||
}
|
||
go.disabled = true;
|
||
go.textContent = 'Prüfe…';
|
||
const res = await ICWebNet.call('login', user.value, pass.value);
|
||
go.disabled = false;
|
||
go.textContent = 'Anmelden';
|
||
pass.value = '';
|
||
|
||
if (!res.ok) { err.textContent = res.error || 'Anmeldung fehlgeschlagen.'; return; }
|
||
await refreshSession();
|
||
};
|
||
go.addEventListener('click', submit);
|
||
[user, pass].forEach(i =>
|
||
i.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); }));
|
||
form.appendChild(go);
|
||
|
||
wrap.appendChild(form);
|
||
wrap.appendChild(el('div', 'wh-login-foot',
|
||
'Mit der Anmeldung wird auch das Postfach dieses Zugangs geöffnet. '
|
||
+ 'Mehrere Personen können denselben Zugang benutzen.'));
|
||
|
||
setMain(wrap);
|
||
setTimeout(() => user.focus(), 0);
|
||
}
|
||
|
||
function showForcedPassword() {
|
||
const wrap = el('div', 'wh-login');
|
||
wrap.appendChild(el('div', 'wh-login-logo', '🔑'));
|
||
wrap.appendChild(el('div', 'wh-login-title', 'Passwort ändern'));
|
||
wrap.appendChild(el('div', 'wh-login-sub',
|
||
'Dieser Zugang benutzt noch das vergebene Startpasswort. Bitte vergib ein eigenes.'));
|
||
|
||
const form = el('div', 'wh-form wh-login-form');
|
||
const p1 = field(form, 'Neues Passwort', 'password', '');
|
||
const p2 = field(form, 'Wiederholen', 'password', '');
|
||
const err = el('div', 'wh-login-error');
|
||
form.appendChild(err);
|
||
|
||
const go = el('button', 'icweb-btn-primary', 'Passwort setzen');
|
||
go.addEventListener('click', async () => {
|
||
err.textContent = '';
|
||
if (p1.value !== p2.value) { err.textContent = 'Die Eingaben stimmen nicht überein.'; return; }
|
||
const res = await ICWebNet.call('changePassword', session().id, p1.value);
|
||
if (!res.ok) { err.textContent = res.error || 'Fehlgeschlagen.'; return; }
|
||
Desktop.showNotification('✔ Passwort geändert.');
|
||
await refreshSession();
|
||
});
|
||
form.appendChild(go);
|
||
|
||
const skip = el('a', 'wh-login-skip', 'Später ändern');
|
||
skip.addEventListener('click', () => { info.session.mustChange = false; renderSide(); showDomains(); });
|
||
form.appendChild(skip);
|
||
|
||
wrap.appendChild(form);
|
||
setMain(wrap);
|
||
setTimeout(() => p1.focus(), 0);
|
||
}
|
||
|
||
/* ── Seitenleiste ─────────────────────────────────────── */
|
||
function renderSide() {
|
||
const side = root('wh-side');
|
||
if (!side) return;
|
||
side.replaceChildren();
|
||
|
||
side.appendChild(el('div', 'wh-side-title', 'Webhosting'));
|
||
|
||
if (!session()) {
|
||
side.appendChild(el('div', 'wh-side-note', 'Nicht angemeldet'));
|
||
return;
|
||
}
|
||
|
||
const entries = [{ id: 'domains', icon: '🌍', label: isProvider() ? 'Alle Seiten' : 'Meine Seite' }];
|
||
if (mayManage()) entries.push({ id: 'accounts', icon: '👥', label: 'Zugänge' });
|
||
if (isProvider()) {
|
||
entries.push({ id: 'admin', icon: '🛠', label: 'Verwaltung' });
|
||
entries.push({ id: 'reports', icon: '⚑', label: 'Meldungen' });
|
||
}
|
||
entries.push({ id: 'password', icon: '🔑', label: 'Mein Passwort' });
|
||
|
||
entries.forEach(e => {
|
||
const b = el('div', 'wh-nav' + (view === e.id ? ' active' : ''));
|
||
b.appendChild(el('span', 'wh-nav-icon', e.icon));
|
||
b.appendChild(el('span', null, e.label));
|
||
b.addEventListener('click', () => {
|
||
view = e.id;
|
||
renderSide();
|
||
({ domains: showDomains, accounts: showAccounts, admin: showAdmin,
|
||
reports: showReports, password: showOwnPassword })[e.id]();
|
||
});
|
||
side.appendChild(b);
|
||
});
|
||
|
||
const foot = el('div', 'wh-side-user');
|
||
foot.appendChild(el('div', 'wh-side-username', session().username));
|
||
foot.appendChild(el('div', 'wh-side-role',
|
||
{ superadmin: 'Anbieter', admin: 'Domänenadmin', editor: 'Redakteur' }[session().role]
|
||
|| session().role));
|
||
const out = el('a', 'wh-side-logout', 'Abmelden');
|
||
out.addEventListener('click', async () => {
|
||
await ICWebNet.call('logout');
|
||
Desktop.showNotification('Abgemeldet. Das Postfach ist wieder geschlossen.');
|
||
await refreshSession();
|
||
});
|
||
foot.appendChild(out);
|
||
side.appendChild(foot);
|
||
}
|
||
|
||
/* ── Domänen ──────────────────────────────────────────── */
|
||
async function showDomains() {
|
||
setMain(loading('Lade…'));
|
||
const res = await ICWebNet.call('listMine');
|
||
const domains = (res.ok && Array.isArray(res.data)) ? res.data : [];
|
||
|
||
const wrap = el('div', 'wh-page');
|
||
wrap.appendChild(header(isProvider() ? 'Alle Seiten' : 'Meine Seite',
|
||
isProvider() ? 'Jede Domäne auf dem Server. Du darfst überall bearbeiten.'
|
||
: 'Die Seiten deiner Domäne.'));
|
||
|
||
if (!domains.length) {
|
||
const e = el('div', 'wh-empty');
|
||
e.appendChild(el('div', 'wh-empty-icon', '🌍'));
|
||
e.appendChild(el('div', null, isProvider()
|
||
? 'Noch keine Domäne angelegt.'
|
||
: 'Zu diesem Zugang gehört keine Domäne.'));
|
||
if (isProvider()) {
|
||
const b = el('button', 'icweb-btn-primary', 'Domäne anlegen');
|
||
b.addEventListener('click', () => { view = 'admin'; renderSide(); showAdmin(); });
|
||
e.appendChild(b);
|
||
}
|
||
wrap.appendChild(e);
|
||
return setMain(wrap);
|
||
}
|
||
|
||
const grid = el('div', 'wh-cards');
|
||
domains.forEach(d => {
|
||
const card = el('div', 'wh-card');
|
||
card.appendChild(el('div', 'wh-card-domain', d.domain));
|
||
card.appendChild(el('div', 'wh-card-title', d.title || ''));
|
||
|
||
const tags = el('div', 'wh-tags');
|
||
tags.appendChild(el('span', 'wh-tag ' + (d.published ? 'ok' : 'warn'),
|
||
d.published ? 'veröffentlicht' : 'nicht veröffentlicht'));
|
||
if (d.blocked) tags.appendChild(el('span', 'wh-tag bad', 'gesperrt'));
|
||
card.appendChild(tags);
|
||
|
||
const actions = el('div', 'wh-card-actions');
|
||
const edit = el('button', 'icweb-btn-primary', 'Bearbeiten');
|
||
edit.addEventListener('click', () => openDomain(d.domain));
|
||
const visit = el('button', 'icweb-btn-ghost', 'Ansehen');
|
||
visit.addEventListener('click', () => BrowserApp.open('ic://' + d.domain));
|
||
actions.append(edit, visit);
|
||
card.appendChild(actions);
|
||
grid.appendChild(card);
|
||
});
|
||
|
||
wrap.appendChild(grid);
|
||
setMain(wrap);
|
||
}
|
||
|
||
async function openDomain(domain, tab) {
|
||
view = 'domains';
|
||
renderSide();
|
||
setMain(loading('Lade ' + domain + '…'));
|
||
|
||
const res = await ICWebNet.call('getEditable', domain);
|
||
if (!res.ok) return setMain(errorBox(res.error || 'Nicht verfügbar.'));
|
||
|
||
current = res.data;
|
||
currentSlug = (current.pages[0] && current.pages[0].slug) || 'home';
|
||
renderDomain(tab || 'pages');
|
||
}
|
||
|
||
function renderDomain(tab) {
|
||
const wrap = el('div', 'wh-page');
|
||
|
||
const head = el('div', 'wh-domainhead');
|
||
const left = el('div');
|
||
left.appendChild(el('div', 'wh-card-domain', current.domain));
|
||
left.appendChild(el('div', 'wh-card-title', current.title || ''));
|
||
head.appendChild(left);
|
||
|
||
const tags = el('div', 'wh-tags');
|
||
tags.appendChild(el('span', 'wh-tag ' + (current.published ? 'ok' : 'warn'),
|
||
current.published ? 'veröffentlicht' : 'Entwurf'));
|
||
if (current.blocked) tags.appendChild(el('span', 'wh-tag bad', 'gesperrt'));
|
||
head.appendChild(tags);
|
||
wrap.appendChild(head);
|
||
|
||
if (current.blocked) {
|
||
wrap.appendChild(el('div', 'wh-banner',
|
||
'Diese Seite wurde gesperrt. Änderungen sind nicht möglich – wende dich an die Administration.'));
|
||
}
|
||
|
||
const tabs = el('div', 'wh-tabs');
|
||
const list = [['pages', 'Seiten'], ['settings', 'Einstellungen']];
|
||
list.forEach(([id, label]) => {
|
||
const t = el('div', 'wh-tab' + (tab === id ? ' active' : ''), label);
|
||
t.addEventListener('click', () => renderDomain(id));
|
||
tabs.appendChild(t);
|
||
});
|
||
const back = el('div', 'wh-tab-back', '← Übersicht');
|
||
back.addEventListener('click', () => showDomains());
|
||
tabs.appendChild(back);
|
||
wrap.appendChild(tabs);
|
||
|
||
const body = el('div', 'wh-tabbody');
|
||
wrap.appendChild(body);
|
||
setMain(wrap);
|
||
|
||
if (tab === 'pages') renderPagesTab(body);
|
||
if (tab === 'settings') renderSettingsTab(body);
|
||
}
|
||
|
||
/* ── Reiter: Seiten ───────────────────────────────────── */
|
||
function renderPagesTab(body) {
|
||
body.replaceChildren();
|
||
const cols = el('div', 'wh-split');
|
||
|
||
const list = el('div', 'wh-pagelist');
|
||
current.pages.forEach(p => {
|
||
const item = el('div', 'wh-pageitem' + (p.slug === currentSlug ? ' active' : ''));
|
||
item.appendChild(el('div', 'wh-pageitem-title', p.title || p.slug));
|
||
item.appendChild(el('div', 'wh-pageitem-slug', '/' + p.slug));
|
||
item.addEventListener('click', () => { currentSlug = p.slug; renderPagesTab(body); });
|
||
list.appendChild(item);
|
||
});
|
||
|
||
const add = el('button', 'icweb-btn-ghost', '+ Neue Seite');
|
||
add.addEventListener('click', () => {
|
||
ICWebRender.promptText('Neue Seite', {
|
||
hint: 'Nur Buchstaben, Ziffern und Bindestrich. Erreichbar unter ic://'
|
||
+ current.domain + '/name',
|
||
placeholder: 'impressum',
|
||
okLabel: 'Anlegen',
|
||
}, (slug) => {
|
||
const clean = slug.toLowerCase().replace(/\s+/g, '');
|
||
if (current.pages.some(p => p.slug === clean)) {
|
||
return Desktop.showNotification('⚠ Diese Seite gibt es schon.');
|
||
}
|
||
current.pages.push({ slug: clean, title: slug, blocks: [] });
|
||
currentSlug = clean;
|
||
renderPagesTab(body);
|
||
});
|
||
});
|
||
list.appendChild(add);
|
||
cols.appendChild(list);
|
||
|
||
const page = current.pages.find(p => p.slug === currentSlug);
|
||
const edit = el('div', 'wh-editor');
|
||
|
||
if (!page) {
|
||
edit.appendChild(el('div', 'wh-hint', 'Keine Seite ausgewählt.'));
|
||
cols.appendChild(edit);
|
||
return body.appendChild(cols);
|
||
}
|
||
|
||
const titleRow = el('div', 'wh-field');
|
||
titleRow.appendChild(el('label', 'wh-label', 'Seitentitel'));
|
||
const titleIn = el('input', 'icweb-input');
|
||
titleIn.value = page.title || '';
|
||
titleIn.addEventListener('input', () => { page.title = titleIn.value; });
|
||
titleRow.appendChild(titleIn);
|
||
edit.appendChild(titleRow);
|
||
|
||
const blocksBox = el('div', 'wh-blocks');
|
||
edit.appendChild(blocksBox);
|
||
drawBlocks(blocksBox, page);
|
||
|
||
const addBar = el('div', 'wh-addbar');
|
||
addBar.appendChild(el('span', 'wh-addbar-label', 'Block hinzufügen:'));
|
||
Object.entries(BLOCK_TYPES).forEach(([type, def]) => {
|
||
const b = el('button', 'wh-addbtn');
|
||
b.appendChild(el('span', 'wh-addbtn-icon', def.icon));
|
||
b.appendChild(el('span', null, def.label));
|
||
b.addEventListener('click', () => {
|
||
page.blocks.push(newBlock(type));
|
||
drawBlocks(blocksBox, page);
|
||
});
|
||
addBar.appendChild(b);
|
||
});
|
||
edit.appendChild(addBar);
|
||
|
||
const actions = el('div', 'wh-actions');
|
||
const save = el('button', 'icweb-btn-primary', 'Seite speichern');
|
||
save.disabled = current.blocked;
|
||
save.addEventListener('click', async () => {
|
||
save.disabled = true;
|
||
const res = await ICWebNet.call('savePage', current.domain, page.slug,
|
||
page.title, toWire(page.blocks));
|
||
save.disabled = current.blocked;
|
||
notify(res, 'Seite gespeichert.');
|
||
});
|
||
actions.appendChild(save);
|
||
|
||
const preview = el('button', 'icweb-btn-ghost', 'Vorschau');
|
||
preview.addEventListener('click', () => showPreview(page));
|
||
actions.appendChild(preview);
|
||
|
||
if (page.slug !== 'home') {
|
||
const del = el('button', 'icweb-btn-danger', 'Seite löschen');
|
||
del.addEventListener('click', () => {
|
||
ICWebRender.confirmBox('Seite löschen',
|
||
'Seite "' + page.slug + '" wirklich löschen?', async () => {
|
||
const res = await ICWebNet.call('deletePage', current.domain, page.slug);
|
||
if (notify(res, 'Seite gelöscht.')) openDomain(current.domain, 'pages');
|
||
}, { danger: true, okLabel: 'Löschen' });
|
||
});
|
||
actions.appendChild(del);
|
||
}
|
||
edit.appendChild(actions);
|
||
|
||
cols.appendChild(edit);
|
||
body.appendChild(cols);
|
||
}
|
||
|
||
function newBlock(type) {
|
||
const b = { type };
|
||
(BLOCK_TYPES[type].fields || []).forEach(f => {
|
||
b[f.key] = f.default !== undefined ? f.default : '';
|
||
});
|
||
return b;
|
||
}
|
||
|
||
/* Blocks für die Übertragung aufbereiten (Zeilen → Array). */
|
||
function toWire(blocks) {
|
||
return blocks.map(b => {
|
||
const out = { type: b.type };
|
||
(BLOCK_TYPES[b.type].fields || []).forEach(f => {
|
||
if (f.type === 'lines') {
|
||
const raw = Array.isArray(b[f.key]) ? b[f.key].join('\n') : String(b[f.key] || '');
|
||
out[f.key] = raw.split('\n').map(s => s.trim()).filter(s => s.length);
|
||
} else if (f.type === 'select') {
|
||
out[f.key] = Number(b[f.key]);
|
||
} else {
|
||
out[f.key] = String(b[f.key] || '');
|
||
}
|
||
});
|
||
return out;
|
||
});
|
||
}
|
||
|
||
function drawBlocks(box, page) {
|
||
box.replaceChildren();
|
||
|
||
if (!page.blocks.length) {
|
||
return box.appendChild(el('div', 'wh-hint',
|
||
'Noch keine Inhalte. Füge unten einen Block hinzu.'));
|
||
}
|
||
|
||
page.blocks.forEach((b, idx) => {
|
||
const def = BLOCK_TYPES[b.type];
|
||
if (!def) return;
|
||
|
||
const card = el('div', 'wh-block');
|
||
const head = el('div', 'wh-block-head');
|
||
head.appendChild(el('span', 'wh-block-icon', def.icon));
|
||
head.appendChild(el('span', 'wh-block-label', def.label));
|
||
|
||
const tools = el('div', 'wh-block-tools');
|
||
const up = el('button', 'wh-icon-btn', '▲');
|
||
up.disabled = idx === 0;
|
||
up.addEventListener('click', () => {
|
||
[page.blocks[idx - 1], page.blocks[idx]] = [page.blocks[idx], page.blocks[idx - 1]];
|
||
drawBlocks(box, page);
|
||
});
|
||
const down = el('button', 'wh-icon-btn', '▼');
|
||
down.disabled = idx === page.blocks.length - 1;
|
||
down.addEventListener('click', () => {
|
||
[page.blocks[idx + 1], page.blocks[idx]] = [page.blocks[idx], page.blocks[idx + 1]];
|
||
drawBlocks(box, page);
|
||
});
|
||
const rm = el('button', 'wh-icon-btn danger', '✕');
|
||
rm.addEventListener('click', () => {
|
||
page.blocks.splice(idx, 1);
|
||
drawBlocks(box, page);
|
||
});
|
||
tools.append(up, down, rm);
|
||
head.appendChild(tools);
|
||
card.appendChild(head);
|
||
|
||
def.fields.forEach(f => {
|
||
const row = el('div', 'wh-field');
|
||
row.appendChild(el('label', 'wh-label', f.label));
|
||
|
||
let input;
|
||
if (f.type === 'area' || f.type === 'lines') {
|
||
input = el('textarea', 'icweb-input');
|
||
input.setAttribute('rows', String(f.rows || 4));
|
||
input.value = Array.isArray(b[f.key]) ? b[f.key].join('\n') : (b[f.key] || '');
|
||
} else if (f.type === 'select') {
|
||
input = el('select', 'icweb-input');
|
||
f.options.forEach(([v, label]) => {
|
||
const o = el('option', null, label);
|
||
o.value = String(v);
|
||
input.appendChild(o);
|
||
});
|
||
input.value = String(b[f.key] ?? f.default);
|
||
} else {
|
||
input = el('input', 'icweb-input');
|
||
input.value = b[f.key] || '';
|
||
}
|
||
|
||
input.addEventListener('input', () => { b[f.key] = input.value; });
|
||
input.addEventListener('change', () => { b[f.key] = input.value; });
|
||
row.appendChild(input);
|
||
|
||
if (f.key === 'url') {
|
||
row.appendChild(el('div', 'wh-hint',
|
||
'Erlaubte Bildquellen: ' + ((info && info.imageHosts) || []).join(', ')));
|
||
}
|
||
card.appendChild(row);
|
||
});
|
||
|
||
box.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function showPreview(page) {
|
||
modal('Vorschau', (box, close) => {
|
||
const view = el('div', 'wh-preview');
|
||
view.appendChild(ICWebRender.renderPage({
|
||
domain: current.domain, title: current.title, theme: current.theme,
|
||
nav: current.pages.map(p => ({ slug: p.slug, title: p.title })),
|
||
page: { slug: page.slug, title: page.title, blocks: toWire(page.blocks) },
|
||
}));
|
||
box.appendChild(view);
|
||
|
||
const row = el('div', 'icweb-modal-actions');
|
||
const b = el('button', 'icweb-btn-primary', 'Schließen');
|
||
b.addEventListener('click', close);
|
||
row.appendChild(b);
|
||
box.appendChild(row);
|
||
}, true);
|
||
}
|
||
|
||
/* ── Reiter: Einstellungen ────────────────────────────── */
|
||
function renderSettingsTab(body) {
|
||
body.replaceChildren();
|
||
|
||
const form = el('div', 'wh-form');
|
||
const title = field(form, 'Anzeigename', 'text', current.title || '');
|
||
|
||
const themeRow = el('div', 'wh-field');
|
||
themeRow.appendChild(el('label', 'wh-label', 'Gestaltung'));
|
||
const theme = el('select', 'icweb-input');
|
||
((info && info.themes) || ['clean']).forEach(t => {
|
||
const o = el('option', null, t);
|
||
o.value = t;
|
||
theme.appendChild(o);
|
||
});
|
||
theme.value = current.theme;
|
||
themeRow.appendChild(theme);
|
||
form.appendChild(themeRow);
|
||
|
||
const pubRow = el('div', 'wh-field wh-check');
|
||
const pub = el('input');
|
||
pub.setAttribute('type', 'checkbox');
|
||
pub.checked = current.published;
|
||
pubRow.appendChild(pub);
|
||
pubRow.appendChild(el('label', 'wh-label', 'Veröffentlicht (für alle sichtbar)'));
|
||
form.appendChild(pubRow);
|
||
|
||
const save = el('button', 'icweb-btn-primary', 'Speichern');
|
||
save.disabled = current.blocked;
|
||
save.addEventListener('click', async () => {
|
||
const res = await ICWebNet.call('updateSite', current.domain, {
|
||
title: title.value, theme: theme.value, published: pub.checked,
|
||
});
|
||
if (notify(res, 'Einstellungen gespeichert.')) openDomain(current.domain, 'settings');
|
||
});
|
||
form.appendChild(save);
|
||
form.appendChild(el('div', 'wh-hint',
|
||
'Solange die Seite nicht veröffentlicht ist, sehen sie nur angemeldete Bearbeiter.'));
|
||
|
||
body.appendChild(form);
|
||
}
|
||
|
||
/* ── Zugänge ──────────────────────────────────────────── */
|
||
let accountDomain = null;
|
||
|
||
async function showAccounts() {
|
||
setMain(loading('Lade Zugänge…'));
|
||
|
||
// Der Anbieter wählt die Domäne, alle anderen haben genau ihre.
|
||
if (!isProvider()) accountDomain = session().domain;
|
||
if (isProvider() && !accountDomain) {
|
||
const res = await ICWebNet.call('adminListSites');
|
||
const sites = (res.ok && res.data) || [];
|
||
if (!sites.length) return setMain(errorBox('Noch keine Domäne angelegt.'));
|
||
accountDomain = sites[0].domain;
|
||
}
|
||
|
||
const [accs, sitesRes] = await Promise.all([
|
||
ICWebNet.call('listAccounts', accountDomain),
|
||
isProvider() ? ICWebNet.call('adminListSites') : Promise.resolve({ ok: false }),
|
||
]);
|
||
|
||
const wrap = el('div', 'wh-page');
|
||
wrap.appendChild(header('Zugänge',
|
||
'Ein Zugang ist Benutzername, Passwort und Postfach in einem. '
|
||
+ 'Domänenadmins dürfen Zugänge anlegen, Redakteure nur Seiten pflegen.'));
|
||
|
||
if (isProvider() && sitesRes.ok) {
|
||
const row = el('div', 'wh-field');
|
||
row.appendChild(el('label', 'wh-label', 'Domäne'));
|
||
const sel = el('select', 'icweb-input');
|
||
sitesRes.data.forEach(s => {
|
||
const o = el('option', null, s.domain);
|
||
o.value = s.domain;
|
||
sel.appendChild(o);
|
||
});
|
||
sel.value = accountDomain;
|
||
sel.addEventListener('change', () => { accountDomain = sel.value; showAccounts(); });
|
||
row.appendChild(sel);
|
||
wrap.appendChild(row);
|
||
}
|
||
|
||
if (!accs.ok) {
|
||
wrap.appendChild(el('div', 'wh-hint', accs.error || 'Keine Berechtigung.'));
|
||
return setMain(wrap);
|
||
}
|
||
|
||
const table = el('div', 'wh-table');
|
||
if (!accs.data.length) table.appendChild(el('div', 'wh-hint', 'Noch keine Zugänge.'));
|
||
|
||
accs.data.forEach(a => {
|
||
const row = el('div', 'wh-row');
|
||
const infoCol = el('div');
|
||
infoCol.appendChild(el('div', 'wh-row-title', a.username));
|
||
infoCol.appendChild(el('div', 'wh-row-sub',
|
||
(a.display_name || '—') + ' · letzte Anmeldung: ' + (a.last_login || 'nie')));
|
||
row.appendChild(infoCol);
|
||
|
||
const right = el('div', 'wh-row-right');
|
||
right.appendChild(el('span', 'wh-tag',
|
||
{ superadmin: 'Anbieter', admin: 'Admin', editor: 'Redakteur' }[a.role] || a.role));
|
||
if (!a.active) right.appendChild(el('span', 'wh-tag bad', 'gesperrt'));
|
||
if (a.must_change) right.appendChild(el('span', 'wh-tag warn', 'Startpasswort'));
|
||
|
||
const own = session().username === a.username;
|
||
|
||
const pw = el('button', 'icweb-btn-ghost', 'Passwort');
|
||
pw.addEventListener('click', () => askPassword(a));
|
||
right.appendChild(pw);
|
||
|
||
if (!own && a.role !== 'superadmin') {
|
||
const role = el('button', 'icweb-btn-ghost',
|
||
a.role === 'admin' ? '→ Redakteur' : '→ Admin');
|
||
role.addEventListener('click', async () => {
|
||
const res = await ICWebNet.call('setAccountRole', a.id,
|
||
a.role === 'admin' ? 'editor' : 'admin');
|
||
if (notify(res, 'Rolle geändert.')) showAccounts();
|
||
});
|
||
right.appendChild(role);
|
||
|
||
const lock = el('button', 'icweb-btn-ghost', a.active ? 'Sperren' : 'Freigeben');
|
||
lock.addEventListener('click', async () => {
|
||
const res = await ICWebNet.call('setAccountActive', a.id, !a.active);
|
||
if (notify(res, a.active ? 'Gesperrt.' : 'Freigegeben.')) showAccounts();
|
||
});
|
||
right.appendChild(lock);
|
||
|
||
const del = el('button', 'icweb-btn-danger', 'Löschen');
|
||
del.addEventListener('click', () => {
|
||
ICWebRender.confirmBox('Zugang löschen',
|
||
'Zugang ' + a.username + ' löschen?\n'
|
||
+ 'Das Postfach mit den Nachrichten bleibt bestehen.', async () => {
|
||
const res = await ICWebNet.call('deleteAccount', a.id);
|
||
if (notify(res, 'Zugang gelöscht.')) showAccounts();
|
||
}, { danger: true, okLabel: 'Löschen' });
|
||
});
|
||
right.appendChild(del);
|
||
}
|
||
|
||
row.appendChild(right);
|
||
table.appendChild(row);
|
||
});
|
||
wrap.appendChild(table);
|
||
|
||
/* Anlegen */
|
||
wrap.appendChild(el('div', 'wh-subhead', 'Zugang anlegen'));
|
||
const form = el('div', 'wh-form wh-form-inline');
|
||
|
||
const nameRow = el('div', 'wh-field');
|
||
nameRow.appendChild(el('label', 'wh-label', 'Benutzername'));
|
||
const nameWrap = el('div', 'wh-inline');
|
||
const nameIn = el('input', 'icweb-input');
|
||
nameIn.setAttribute('placeholder', 'presse');
|
||
nameWrap.appendChild(nameIn);
|
||
nameWrap.appendChild(el('span', 'wh-inline-suffix', '@' + accountDomain));
|
||
nameRow.appendChild(nameWrap);
|
||
form.appendChild(nameRow);
|
||
|
||
const display = field(form, 'Anzeigename', 'text', '', 'z. B. Redaktion');
|
||
const pass = field(form, 'Startpasswort', 'text', '',
|
||
'mindestens 6 Zeichen – wird weitergegeben');
|
||
|
||
const roleRow = el('div', 'wh-field');
|
||
roleRow.appendChild(el('label', 'wh-label', 'Rolle'));
|
||
const role = el('select', 'icweb-input');
|
||
[['editor', 'Redakteur – darf Seiten pflegen'],
|
||
['admin', 'Domänenadmin – darf zusätzlich Zugänge verwalten']].forEach(([v, l]) => {
|
||
const o = el('option', null, l);
|
||
o.value = v;
|
||
role.appendChild(o);
|
||
});
|
||
roleRow.appendChild(role);
|
||
form.appendChild(roleRow);
|
||
|
||
const create = el('button', 'icweb-btn-primary', 'Zugang anlegen');
|
||
create.addEventListener('click', async () => {
|
||
const username = (nameIn.value || '').trim().toLowerCase() + '@' + accountDomain;
|
||
const res = await ICWebNet.call('createAccount', accountDomain, username,
|
||
pass.value, role.value, display.value);
|
||
if (!notify(res, 'Zugang angelegt.')) return;
|
||
showCredentials('Zugang angelegt', username, pass.value);
|
||
showAccounts();
|
||
});
|
||
form.appendChild(create);
|
||
form.appendChild(el('div', 'wh-hint',
|
||
'Mit dem Zugang entsteht auch das Postfach ' + (nameIn.value || 'name') + '@' + accountDomain
|
||
+ '. Wer die Daten kennt, kann sich anmelden – auch mehrere gleichzeitig.'));
|
||
|
||
wrap.appendChild(form);
|
||
setMain(wrap);
|
||
}
|
||
|
||
function askPassword(account) {
|
||
modal('Passwort setzen: ' + account.username, (box, close) => {
|
||
const form = el('div', 'wh-form');
|
||
const p = field(form, 'Neues Passwort', 'text', '', 'mindestens 6 Zeichen');
|
||
box.appendChild(form);
|
||
|
||
const row = el('div', 'icweb-modal-actions');
|
||
const cancel = el('button', 'icweb-btn-ghost', 'Abbrechen');
|
||
cancel.addEventListener('click', close);
|
||
const go = el('button', 'icweb-btn-primary', 'Setzen');
|
||
go.addEventListener('click', async () => {
|
||
const res = await ICWebNet.call('setAccountPassword', account.id, p.value);
|
||
if (notify(res, 'Passwort gesetzt.')) { close(); showAccounts(); }
|
||
});
|
||
row.append(cancel, go);
|
||
box.appendChild(row);
|
||
setTimeout(() => p.focus(), 0);
|
||
});
|
||
}
|
||
|
||
function showCredentials(title, username, password) {
|
||
modal(title, (box, close) => {
|
||
box.appendChild(el('div', 'icweb-modal-sub',
|
||
'Diese Daten werden nur jetzt angezeigt. Notiere sie und gib sie weiter.'));
|
||
|
||
const grid = el('div', 'wh-cred');
|
||
grid.appendChild(el('div', 'wh-cred-label', 'Benutzername'));
|
||
grid.appendChild(el('div', 'wh-cred-value', username));
|
||
grid.appendChild(el('div', 'wh-cred-label', 'Passwort'));
|
||
grid.appendChild(el('div', 'wh-cred-value', password));
|
||
box.appendChild(grid);
|
||
|
||
box.appendChild(el('div', 'wh-hint',
|
||
'Beim ersten Anmelden wird zum Ändern des Passworts aufgefordert.'));
|
||
|
||
const row = el('div', 'icweb-modal-actions');
|
||
const b = el('button', 'icweb-btn-primary', 'Verstanden');
|
||
b.addEventListener('click', close);
|
||
row.appendChild(b);
|
||
box.appendChild(row);
|
||
});
|
||
}
|
||
|
||
/* ── Mein Passwort ────────────────────────────────────── */
|
||
function showOwnPassword() {
|
||
const wrap = el('div', 'wh-page');
|
||
wrap.appendChild(header('Mein Passwort',
|
||
'Gilt für ' + session().username + '. Wer das Passwort kennt, hat auch Zugriff '
|
||
+ 'auf das Postfach dieses Zugangs.'));
|
||
|
||
const form = el('div', 'wh-form');
|
||
const p1 = field(form, 'Neues Passwort', 'password', '');
|
||
const p2 = field(form, 'Wiederholen', 'password', '');
|
||
const err = el('div', 'wh-login-error');
|
||
form.appendChild(err);
|
||
|
||
const go = el('button', 'icweb-btn-primary', 'Passwort ändern');
|
||
go.addEventListener('click', async () => {
|
||
err.textContent = '';
|
||
if (p1.value !== p2.value) { err.textContent = 'Die Eingaben stimmen nicht überein.'; return; }
|
||
const res = await ICWebNet.call('changePassword', session().id, p1.value);
|
||
if (!res.ok) { err.textContent = res.error || 'Fehlgeschlagen.'; return; }
|
||
p1.value = ''; p2.value = '';
|
||
Desktop.showNotification('✔ Passwort geändert.');
|
||
});
|
||
form.appendChild(go);
|
||
wrap.appendChild(form);
|
||
setMain(wrap);
|
||
}
|
||
|
||
/* ── Verwaltung (nur Anbieter) ────────────────────────── */
|
||
async function showAdmin() {
|
||
setMain(loading('Lade Verwaltung…'));
|
||
|
||
const [sites, jobs] = await Promise.all([
|
||
ICWebNet.call('adminListSites'),
|
||
ICWebNet.call('adminListJobs'),
|
||
]);
|
||
|
||
const wrap = el('div', 'wh-page');
|
||
wrap.appendChild(header('Verwaltung',
|
||
'Domänen anlegen, übertragen, sperren und löschen. '
|
||
+ 'Mit jeder Domäne entsteht ein Zugang für ihren Admin.'));
|
||
|
||
if (!sites.ok) {
|
||
wrap.appendChild(el('div', 'wh-hint', sites.error || 'Keine Berechtigung.'));
|
||
return setMain(wrap);
|
||
}
|
||
|
||
/* Anlegen */
|
||
const box = el('div', 'wh-form wh-form-inline');
|
||
box.appendChild(el('div', 'wh-subhead', 'Domäne anlegen'));
|
||
const dom = field(box, 'Domäne (name.endung)', 'text', '', 'z. B. weazel-news.ls');
|
||
const title = field(box, 'Anzeigename', 'text', '', 'z. B. Weazel News');
|
||
|
||
const ownerRow = el('div', 'wh-field');
|
||
ownerRow.appendChild(el('label', 'wh-label', 'Besitzer (Job)'));
|
||
const owner = el('select', 'icweb-input');
|
||
((jobs.ok && jobs.data) || []).forEach(j => {
|
||
const o = el('option', null, j.label + ' · ' + j.name);
|
||
o.value = j.name;
|
||
owner.appendChild(o);
|
||
});
|
||
ownerRow.appendChild(owner);
|
||
box.appendChild(ownerRow);
|
||
|
||
const adminUser = field(box, 'Zugang für den Domänenadmin', 'text', 'admin', 'admin');
|
||
const adminPass = field(box, 'Startpasswort (leer = wird erzeugt)', 'text', '');
|
||
|
||
const create = el('button', 'icweb-btn-primary', 'Anlegen');
|
||
create.addEventListener('click', async () => {
|
||
create.disabled = true;
|
||
const res = await ICWebNet.call('adminRegister', dom.value, title.value,
|
||
owner.value, adminUser.value, adminPass.value);
|
||
create.disabled = false;
|
||
if (!notify(res, 'Domäne angelegt.')) return;
|
||
|
||
const c = res.data && res.data.credentials;
|
||
if (c) showCredentials('Domäne ' + res.data.domain + ' angelegt', c.username, c.password);
|
||
showAdmin();
|
||
});
|
||
box.appendChild(create);
|
||
box.appendChild(el('div', 'wh-hint',
|
||
'Die Domäne wird zugleich als Mail-Domäne angelegt. '
|
||
+ 'Der Zugang des Domänenadmins ist sein Postfach.'));
|
||
wrap.appendChild(box);
|
||
|
||
/* Liste */
|
||
wrap.appendChild(el('div', 'wh-subhead', 'Domänen (' + sites.data.length + ')'));
|
||
const table = el('div', 'wh-table');
|
||
if (!sites.data.length) table.appendChild(el('div', 'wh-hint', 'Noch keine Domäne registriert.'));
|
||
|
||
sites.data.forEach(s => {
|
||
const row = el('div', 'wh-row');
|
||
const col = el('div');
|
||
col.appendChild(el('div', 'wh-row-title', s.domain));
|
||
col.appendChild(el('div', 'wh-row-sub',
|
||
(s.title || '—') + ' · ' + s.owner_id + ' · '
|
||
+ s.pages + ' Seiten, ' + s.accounts + ' Zugänge'));
|
||
row.appendChild(col);
|
||
|
||
const right = el('div', 'wh-row-right');
|
||
right.appendChild(el('span', 'wh-tag ' + (s.published ? 'ok' : 'warn'),
|
||
s.published ? 'live' : 'Entwurf'));
|
||
if (s.blocked) right.appendChild(el('span', 'wh-tag bad', 'gesperrt'));
|
||
|
||
const openBtn = el('button', 'icweb-btn-ghost', 'Bearbeiten');
|
||
openBtn.addEventListener('click', () => openDomain(s.domain));
|
||
right.appendChild(openBtn);
|
||
|
||
const accBtn = el('button', 'icweb-btn-ghost', 'Zugänge');
|
||
accBtn.addEventListener('click', () => {
|
||
accountDomain = s.domain; view = 'accounts'; renderSide(); showAccounts();
|
||
});
|
||
right.appendChild(accBtn);
|
||
|
||
const blockBtn = el('button', 'icweb-btn-ghost', s.blocked ? 'Freigeben' : 'Sperren');
|
||
blockBtn.addEventListener('click', async () => {
|
||
const res = await ICWebNet.call('adminSetBlocked', s.domain, !s.blocked);
|
||
if (notify(res, s.blocked ? 'Freigegeben.' : 'Gesperrt.')) showAdmin();
|
||
});
|
||
right.appendChild(blockBtn);
|
||
|
||
const ownBtn = el('button', 'icweb-btn-ghost', 'Besitzer');
|
||
ownBtn.addEventListener('click', () => {
|
||
ICWebRender.promptText('Besitzer ändern', {
|
||
hint: 'Jobname, dem ' + s.domain + ' künftig gehört.',
|
||
value: s.owner_id, okLabel: 'Übertragen',
|
||
}, async (job) => {
|
||
const res = await ICWebNet.call('adminSetOwner', s.domain, job);
|
||
if (notify(res, 'Besitzer geändert.')) showAdmin();
|
||
});
|
||
});
|
||
right.appendChild(ownBtn);
|
||
|
||
const delBtn = el('button', 'icweb-btn-danger', 'Löschen');
|
||
delBtn.addEventListener('click', () => {
|
||
ICWebRender.confirmBox('Domäne löschen',
|
||
'Domäne ' + s.domain + ' mit allen Seiten und Zugängen löschen?\n'
|
||
+ 'Die Postfächer und ihre Nachrichten bleiben bestehen.', async () => {
|
||
const res = await ICWebNet.call('adminDeleteSite', s.domain);
|
||
if (notify(res, 'Domäne gelöscht.')) showAdmin();
|
||
}, { danger: true, okLabel: 'Löschen' });
|
||
});
|
||
right.appendChild(delBtn);
|
||
|
||
row.appendChild(right);
|
||
table.appendChild(row);
|
||
});
|
||
|
||
wrap.appendChild(table);
|
||
setMain(wrap);
|
||
}
|
||
|
||
/* ── Meldungen ────────────────────────────────────────── */
|
||
async function showReports() {
|
||
setMain(loading('Lade Meldungen…'));
|
||
const res = await ICWebNet.call('adminListReports');
|
||
|
||
const wrap = el('div', 'wh-page');
|
||
wrap.appendChild(header('Meldungen',
|
||
'Gemeldete Seiten. Bilder werden extern verlinkt – Meldungen sind der Weg, '
|
||
+ 'unpassende Inhalte zu finden.'));
|
||
|
||
if (!res.ok) {
|
||
wrap.appendChild(el('div', 'wh-hint', res.error || 'Keine Berechtigung.'));
|
||
return setMain(wrap);
|
||
}
|
||
|
||
const table = el('div', 'wh-table');
|
||
if (!res.data.length) table.appendChild(el('div', 'wh-hint', 'Keine offenen Meldungen.'));
|
||
|
||
res.data.forEach(r => {
|
||
const row = el('div', 'wh-row');
|
||
const col = el('div');
|
||
col.appendChild(el('div', 'wh-row-title',
|
||
r.domain + (r.page_slug ? '/' + r.page_slug : '')));
|
||
col.appendChild(el('div', 'wh-row-sub', r.reason || '(ohne Begründung)'));
|
||
row.appendChild(col);
|
||
|
||
const right = el('div', 'wh-row-right');
|
||
const look = el('button', 'icweb-btn-ghost', 'Ansehen');
|
||
look.addEventListener('click', () =>
|
||
BrowserApp.open('ic://' + r.domain + (r.page_slug ? '/' + r.page_slug : '')));
|
||
right.appendChild(look);
|
||
|
||
const block = el('button', 'icweb-btn-ghost', 'Sperren');
|
||
block.addEventListener('click', async () => {
|
||
if (notify(await ICWebNet.call('adminSetBlocked', r.domain, true), 'Gesperrt.')) showReports();
|
||
});
|
||
right.appendChild(block);
|
||
|
||
const done = el('button', 'icweb-btn-primary', 'Erledigt');
|
||
done.addEventListener('click', async () => {
|
||
if (notify(await ICWebNet.call('adminSetReportStatus', r.id, 'reviewed'),
|
||
'Meldung abgeschlossen.')) showReports();
|
||
});
|
||
right.appendChild(done);
|
||
|
||
const dismiss = el('button', 'icweb-btn-ghost', 'Verwerfen');
|
||
dismiss.addEventListener('click', async () => {
|
||
if (notify(await ICWebNet.call('adminSetReportStatus', r.id, 'dismissed'),
|
||
'Meldung verworfen.')) showReports();
|
||
});
|
||
right.appendChild(dismiss);
|
||
|
||
row.appendChild(right);
|
||
table.appendChild(row);
|
||
});
|
||
|
||
wrap.appendChild(table);
|
||
setMain(wrap);
|
||
}
|
||
|
||
/* ── Kleinkram ────────────────────────────────────────── */
|
||
function header(title, sub) {
|
||
const h = el('div', 'wh-head');
|
||
h.appendChild(el('div', 'wh-head-title', title));
|
||
if (sub) h.appendChild(el('div', 'wh-head-sub', sub));
|
||
return h;
|
||
}
|
||
|
||
function field(parent, label, type, value, placeholder) {
|
||
const row = el('div', 'wh-field');
|
||
row.appendChild(el('label', 'wh-label', label));
|
||
const input = el('input', 'icweb-input');
|
||
input.setAttribute('type', type || 'text');
|
||
if (placeholder) input.setAttribute('placeholder', placeholder);
|
||
input.value = value || '';
|
||
row.appendChild(input);
|
||
parent.appendChild(row);
|
||
return input;
|
||
}
|
||
|
||
/* ── Stile ────────────────────────────────────────────── */
|
||
let stylesDone = false;
|
||
function injectStyles() {
|
||
if (stylesDone) return;
|
||
stylesDone = true;
|
||
|
||
const s = document.createElement('style');
|
||
s.textContent = `
|
||
.wh-root{display:flex;flex:1;min-height:0;height:100%;overflow:hidden}
|
||
.wh-side{width:200px;flex-shrink:0;background:var(--bg-sidebar);border-right:1px solid var(--border);padding:12px 8px;overflow-y:auto;display:flex;flex-direction:column}
|
||
.wh-side-title{font-size:.68rem;text-transform:uppercase;letter-spacing:.12em;color:var(--text-dim);padding:0 8px 10px}
|
||
.wh-nav{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:var(--radius);cursor:pointer;font-size:.8rem;color:var(--text-muted)}
|
||
.wh-nav:hover{background:rgba(255,255,255,.04);color:var(--text)}
|
||
.wh-nav.active{background:rgba(255,255,255,.07);color:var(--accent)}
|
||
.wh-nav-icon{width:16px;text-align:center}
|
||
.wh-side-note{margin-top:14px;padding:6px 10px;font-size:.63rem;color:var(--text-dim);border-top:1px solid var(--border)}
|
||
.wh-side-user{margin-top:auto;padding:10px 10px 4px;border-top:1px solid var(--border)}
|
||
.wh-side-username{font-size:.7rem;color:var(--text);word-break:break-all}
|
||
.wh-side-role{font-size:.62rem;color:var(--text-dim);margin-top:2px}
|
||
.wh-side-logout{display:inline-block;margin-top:7px;font-size:.68rem;color:var(--text-muted);cursor:pointer}
|
||
.wh-side-logout:hover{color:#ff8a80}
|
||
.wh-main{flex:1;min-width:0;overflow-y:auto;padding:18px 22px}
|
||
.wh-page{display:flex;flex-direction:column;gap:14px}
|
||
.wh-head-title{font-size:1.15rem;font-weight:700;color:var(--text)}
|
||
.wh-head-sub{font-size:.75rem;color:var(--text-muted);margin-top:3px;max-width:75ch;line-height:1.5}
|
||
.wh-subhead{font-size:.68rem;text-transform:uppercase;letter-spacing:.11em;color:var(--text-dim);margin-top:14px;padding-bottom:6px;border-bottom:1px solid var(--border)}
|
||
.wh-empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:50px 0;color:var(--text-muted);font-size:.82rem}
|
||
.wh-empty-icon{font-size:2rem}
|
||
.wh-hint{font-size:.72rem;color:var(--text-muted);line-height:1.55;max-width:78ch}
|
||
.wh-banner{background:rgba(229,57,53,.1);border:1px solid rgba(229,57,53,.35);border-radius:var(--radius);padding:10px 12px;font-size:.76rem;color:#ff8a80}
|
||
|
||
.wh-login{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:6px;text-align:center}
|
||
.wh-login-logo{font-size:2.4rem}
|
||
.wh-login-title{font-size:1.3rem;font-weight:700;color:var(--accent)}
|
||
.wh-login-sub{font-size:.76rem;color:var(--text-muted);max-width:46ch;line-height:1.5}
|
||
.wh-login-form{width:320px;margin-top:14px;text-align:left}
|
||
.wh-login-error{font-size:.72rem;color:#ff5252;min-height:1em}
|
||
.wh-login-skip{font-size:.7rem;color:var(--text-dim);cursor:pointer;text-align:center;margin-top:4px}
|
||
.wh-login-skip:hover{color:var(--text)}
|
||
.wh-login-foot{margin-top:18px;font-size:.66rem;color:var(--text-dim);max-width:52ch;line-height:1.55}
|
||
|
||
.wh-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px}
|
||
.wh-card{background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px;display:flex;flex-direction:column;gap:7px}
|
||
.wh-card-domain{font-weight:700;color:var(--accent);font-size:.92rem}
|
||
.wh-card-title{font-size:.76rem;color:var(--text-muted)}
|
||
.wh-card-actions{display:flex;gap:6px;margin-top:4px}
|
||
.wh-tags{display:flex;gap:5px;flex-wrap:wrap}
|
||
.wh-tag{font-size:.61rem;padding:2px 7px;border-radius:9px;border:1px solid var(--border);color:var(--text-muted);white-space:nowrap}
|
||
.wh-tag.ok{color:#4caf50;border-color:rgba(76,175,80,.35)}
|
||
.wh-tag.warn{color:#ffb74d;border-color:rgba(255,183,77,.35)}
|
||
.wh-tag.bad{color:#ff5252;border-color:rgba(255,82,82,.4)}
|
||
|
||
.wh-domainhead{display:flex;justify-content:space-between;align-items:flex-start}
|
||
.wh-tabs{display:flex;gap:2px;border-bottom:1px solid var(--border)}
|
||
.wh-tab{padding:8px 14px;font-size:.78rem;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent}
|
||
.wh-tab:hover{color:var(--text)}
|
||
.wh-tab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
||
.wh-tab-back{margin-left:auto;padding:8px 4px;font-size:.72rem;color:var(--text-dim);cursor:pointer}
|
||
.wh-tab-back:hover{color:var(--text)}
|
||
.wh-tabbody{padding-top:6px;display:flex;flex-direction:column;gap:12px}
|
||
|
||
.wh-split{display:flex;gap:14px;align-items:flex-start}
|
||
.wh-pagelist{width:180px;flex-shrink:0;display:flex;flex-direction:column;gap:4px}
|
||
.wh-pageitem{padding:8px 10px;border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;background:var(--bg-sidebar)}
|
||
.wh-pageitem:hover{border-color:var(--accent)}
|
||
.wh-pageitem.active{border-color:var(--accent);background:rgba(255,255,255,.05)}
|
||
.wh-pageitem-title{font-size:.78rem;color:var(--text);font-weight:600}
|
||
.wh-pageitem-slug{font-size:.64rem;color:var(--text-dim)}
|
||
.wh-editor{flex:1;min-width:0;display:flex;flex-direction:column;gap:10px}
|
||
.wh-blocks{display:flex;flex-direction:column;gap:8px}
|
||
.wh-block{border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-sidebar);padding:10px 12px;display:flex;flex-direction:column;gap:8px}
|
||
.wh-block-head{display:flex;align-items:center;gap:8px}
|
||
.wh-block-icon{width:18px;text-align:center;color:var(--accent)}
|
||
.wh-block-label{font-size:.73rem;font-weight:600;color:var(--text-muted);flex:1}
|
||
.wh-block-tools{display:flex;gap:3px}
|
||
.wh-icon-btn{background:transparent;border:1px solid var(--border);color:var(--text-muted);border-radius:4px;width:22px;height:22px;font-size:.62rem;cursor:pointer}
|
||
.wh-icon-btn:hover:not(:disabled){border-color:var(--accent);color:var(--accent)}
|
||
.wh-icon-btn:disabled{opacity:.3;cursor:default}
|
||
.wh-icon-btn.danger:hover{border-color:#ff5252;color:#ff5252}
|
||
|
||
.wh-addbar{display:flex;flex-wrap:wrap;gap:5px;align-items:center;padding-top:4px;border-top:1px dashed var(--border)}
|
||
.wh-addbar-label{font-size:.68rem;color:var(--text-dim);margin-right:4px}
|
||
.wh-addbtn{display:flex;align-items:center;gap:5px;background:transparent;border:1px solid var(--border);color:var(--text-muted);border-radius:var(--radius);padding:5px 9px;font-size:.7rem;cursor:pointer}
|
||
.wh-addbtn:hover{border-color:var(--accent);color:var(--accent)}
|
||
.wh-addbtn-icon{color:var(--accent)}
|
||
.wh-actions{display:flex;gap:6px;padding-top:6px}
|
||
|
||
.wh-form{display:flex;flex-direction:column;gap:10px;max-width:520px}
|
||
.wh-form-inline{max-width:none;background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:14px}
|
||
.wh-field{display:flex;flex-direction:column;gap:4px}
|
||
.wh-check{flex-direction:row;align-items:center;gap:8px}
|
||
.wh-label{font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim)}
|
||
.wh-inline{display:flex;align-items:center;gap:6px}
|
||
.wh-inline-suffix{font-size:.78rem;color:var(--text-dim);white-space:nowrap}
|
||
.icweb-input{background:var(--bg-window,#15171c);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);padding:7px 9px;font-size:.79rem;font-family:inherit;width:100%;box-sizing:border-box}
|
||
.icweb-input:focus{outline:none;border-color:var(--accent)}
|
||
|
||
.wh-table{display:flex;flex-direction:column;gap:5px}
|
||
.wh-row{display:flex;justify-content:space-between;align-items:center;gap:12px;background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px}
|
||
.wh-row-title{font-size:.82rem;font-weight:600;color:var(--text)}
|
||
.wh-row-sub{font-size:.68rem;color:var(--text-dim);margin-top:2px}
|
||
.wh-row-right{display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}
|
||
|
||
.wh-cred{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
|
||
.wh-cred-label{font-size:.68rem;text-transform:uppercase;letter-spacing:.08em;color:var(--text-dim)}
|
||
.wh-cred-value{font-family:monospace;font-size:.86rem;color:var(--accent);word-break:break-all}
|
||
|
||
.icweb-btn-primary,.icweb-btn-ghost,.icweb-btn-danger{border-radius:var(--radius);padding:6px 12px;font-size:.74rem;font-family:inherit;cursor:pointer;border:1px solid var(--border);background:transparent;color:var(--text-muted)}
|
||
.icweb-btn-primary{background:var(--accent);border-color:var(--accent);color:#08131c;font-weight:600}
|
||
.icweb-btn-primary:disabled{opacity:.45;cursor:default}
|
||
.icweb-btn-ghost:hover{border-color:var(--accent);color:var(--accent)}
|
||
.icweb-btn-danger{color:#ff5252;border-color:rgba(255,82,82,.35)}
|
||
.icweb-btn-danger:hover{background:rgba(255,82,82,.1)}
|
||
|
||
.icweb-modal-back{position:absolute;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;z-index:9500}
|
||
.icweb-modal{background:var(--bg-window,#15171c);border:1px solid var(--border);border-radius:8px;padding:18px;width:430px;max-width:88%;display:flex;flex-direction:column;gap:10px;box-shadow:var(--shadow)}
|
||
.icweb-modal.wide{width:740px;max-height:80%;overflow-y:auto}
|
||
.icweb-modal-title{font-size:.95rem;font-weight:700;color:var(--text)}
|
||
.icweb-modal-sub{font-size:.73rem;color:var(--text-muted);line-height:1.5}
|
||
.icweb-modal-actions{display:flex;justify-content:flex-end;gap:6px}
|
||
.wh-preview{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
||
|
||
/* ── Gerenderte IC-Seiten ────────────────────────────── */
|
||
.icweb-site{padding:22px 26px;line-height:1.6;font-size:.82rem;color:var(--text)}
|
||
.icweb-sitehead{border-bottom:1px solid var(--border);padding-bottom:12px;margin-bottom:16px}
|
||
.icweb-sitetitle{font-size:1.4rem;font-weight:700;color:var(--accent)}
|
||
.icweb-sitedomain{font-size:.68rem;color:var(--text-dim);margin-top:2px}
|
||
.icweb-nav{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:16px}
|
||
.icweb-navlink{padding:5px 11px;border:1px solid var(--border);border-radius:14px;font-size:.72rem;color:var(--text-muted);cursor:pointer}
|
||
.icweb-navlink:hover{border-color:var(--accent);color:var(--accent)}
|
||
.icweb-navlink.active{background:var(--accent);border-color:var(--accent);color:#08131c;font-weight:600}
|
||
.icweb-body{display:flex;flex-direction:column;gap:12px}
|
||
.icweb-h{margin:6px 0 0;color:var(--text);line-height:1.3}
|
||
.icweb-h1{font-size:1.45rem;font-weight:700}
|
||
.icweb-h2{font-size:1.15rem;font-weight:700}
|
||
.icweb-h3{font-size:.98rem;font-weight:600}
|
||
.icweb-p{margin:0;white-space:pre-wrap;color:var(--text-muted)}
|
||
.icweb-muted{color:var(--text-dim);font-style:italic}
|
||
.icweb-fig{margin:0}
|
||
.icweb-img{max-width:100%;border-radius:var(--radius);display:block}
|
||
.icweb-img-broken{padding:24px;text-align:center;color:var(--text-dim);border:1px dashed var(--border);border-radius:var(--radius);font-size:.74rem}
|
||
.icweb-cap{font-size:.68rem;color:var(--text-dim);margin-top:5px}
|
||
.icweb-ul{margin:0;padding-left:20px;color:var(--text-muted)}
|
||
.icweb-ul li{margin:3px 0}
|
||
.icweb-btn{align-self:flex-start;background:var(--accent);color:#08131c;font-weight:600;padding:7px 15px;border-radius:var(--radius);font-size:.76rem;cursor:pointer}
|
||
.icweb-contact{background:var(--bg-sidebar);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px;display:flex;flex-direction:column;gap:6px}
|
||
.icweb-contact-row{display:flex;gap:9px;align-items:center;font-size:.78rem;color:var(--text-muted)}
|
||
.icweb-contact-icon{width:16px;text-align:center;color:var(--accent)}
|
||
.icweb-hr{border:none;border-top:1px solid var(--border);margin:6px 0}
|
||
.icweb-foot{margin-top:22px;padding-top:10px;border-top:1px solid var(--border);display:flex;justify-content:flex-end}
|
||
.icweb-report{font-size:.66rem;color:var(--text-dim);cursor:pointer}
|
||
.icweb-report:hover{color:#ff8a80}
|
||
|
||
/* Themes: nur Akzentfarbe und Kanten, keine eigenen Layouts */
|
||
.icweb-theme-dark{background:#0c0d10}
|
||
.icweb-theme-gov .icweb-sitetitle{color:#7ab8ff}
|
||
.icweb-theme-gov .icweb-navlink.active,.icweb-theme-gov .icweb-btn{background:#7ab8ff;border-color:#7ab8ff}
|
||
.icweb-theme-business .icweb-sitetitle{color:#d4a656}
|
||
.icweb-theme-business .icweb-navlink.active,.icweb-theme-business .icweb-btn{background:#d4a656;border-color:#d4a656}
|
||
`;
|
||
document.head.appendChild(s);
|
||
}
|
||
|
||
// Stile sofort einspielen, nicht erst beim Oeffnen: der Mailclient benutzt
|
||
// denselben Dialogbaukasten (Postfach hinzufuegen), und der soll auch dann
|
||
// aussehen wie vorgesehen, wenn die Webhosting-App nie geoeffnet wurde.
|
||
injectStyles();
|
||
|
||
return { open, openDomain };
|
||
})();
|