/* Graphical editors for typed settings; serialization stays inside the client. */
'use strict';
const editorTypes = {string:'Text',integer:'Ganze Zahl',number:'Zahl',boolean:'Ja / Nein',object:'Gruppe',array:'Liste',null:'Nicht gesetzt'};
const handledEditorEvents = new WeakSet();
const editorLabels = {global:'Allgemein',network:'Netzwerk',disk_setup:'Systemdatenträger',filesystem:'Dateisystem',selection:'Plattenauswahl',zfs:'ZFS',raid:'RAID-Modus',filter:'Geräteauswahl',filter_match:'Verknüpfung',expected_count:'Erwartete Plattenanzahl',expected_serials:'Bestätigte Plattenkennungen',inventory_evidence:'Inventarnachweis',root_secret_id:'Root-Zugang',keyboard:'Tastatur',country:'Land',timezone:'Zeitzone',mailto:'E-Mail',fqdn:'Hostname',cidr:'IP-Adresse mit Präfix',gateway:'Gateway',dns:'DNS-Server',source:'Quelle',values:'Einstellungen',steps:'Schritte',parameters:'Parameter',secret_refs:'Geheimnisse',required:'Erforderlich',name:'Name',version:'Version',status:'Status',id:'Kennung',properties:'Felder',items:'Listenelemente',type:'Typ',description:'Beschreibung',default:'Standardwert',enum:'Auswahlwerte',additionalProperties:'Weitere Felder erlauben',minimum:'Minimum',maximum:'Maximum',minLength:'Mindestlänge',maxLength:'Maximallänge',minItems:'Mindesteinträge',maxItems:'Maximale Einträge',uniqueItems:'Eindeutige Einträge',pattern:'Formatregel'};
const editorLabel = key => editorLabels[key] || String(key).replace(/[_-]/g,' ');
Object.assign(editorLabels,{allowed_versions:'Erlaubte PVE-Versionen',dns_names:'Zu prüfende DNS-Namen',minimum_free_mb:'Freier Speicher in MiB',url:'Adresse',suite:'Distribution',components:'Komponenten',keyring:'Signaturschlüssel',packages:'Pakete',users:'Benutzer',authorized_keys:'Öffentliche SSH-Schlüssel',servers:'Zeitserver',enabled:'Aktiviert',path:'Pfad',content:'Inhalte',storage_ids:'Storage-Namen',require_time_sync:'Zeitsynchronisation erforderlich',resolved:'Wirksame Einstellungen',provenance:'Herkunft der Einstellungen',profiles:'Profile',reboot_budget:'Erlaubte Neustarts',timeout_seconds:'Zeitlimit in Sekunden',retry_safe:'Wiederholung erlaubt'});
const editorType = value => value===null?'null':Array.isArray(value)?'array':typeof value==='number'?(Number.isInteger(value)?'integer':'number'):typeof value;
const editorDefault = (type,schema={}) => Object.hasOwn(schema,'default')?structuredClone(schema.default):schema.enum?.length?structuredClone(schema.enum[0]):({object:{},array:[],boolean:false,integer:0,number:0,null:null,string:''}[type]??'');
function editorSchemaType(schema,value) {
const declared=Array.isArray(schema.type)?schema.type:[schema.type];
const actual=editorType(value);
return declared.includes(actual)?actual:declared.includes('number')&&actual==='integer'?'number':declared.find(type=>Object.hasOwn(editorTypes,type))||actual;
}
function dataEditor(name,value={},options={}) {
return `${dataNode(value,options.schema||{},options.label||editorLabel(name))}`;
}
function dataNode(value,schema={},label='Wert') {
schema=schema && typeof schema==='object'?schema:{};
const type=editorSchemaType(schema,value);
const declared=Array.isArray(schema.type)?schema.type:[schema.type];
const fixed=declared.filter(Boolean).length===1 || Array.isArray(schema.enum) || Object.hasOwn(schema,'const');
const choice=fixed?`${esc(editorTypes[type]||type)}`:``;
return `
${esc(label)}${choice}
${schema.description?`
${esc(schema.description)}
`:''}
${dataNodeContent(value,type,schema,label)}
`;
}
function dataNodeContent(value,type,schema,label) {
if(Object.hasOwn(schema,'const'))return `Fest vorgegeben: ${esc(String(schema.const))}
`;
if(Array.isArray(schema.enum)) {
const choices=[...schema.enum];
if(value!==undefined&&!choices.some(item=>json(item)===json(value)))choices.push(value);
return ``;
}
if(type==='object'||type==='array') {
const object=type==='object';
const properties=schema.properties||{};
const entries=object?Object.entries(value&&typeof value==='object'&&!Array.isArray(value)?value:{}):Array.isArray(value)?value.map((item,index)=>[String(index),item]):[];
if(object)for(const key of schema.required||[])if(!entries.some(([name])=>name===key))entries.push([key,editorDefault(editorSchemaType(properties[key]||{},''),properties[key]||{})]);
const children=entries.map(([key,item],index)=>dataEntry(key,item,object,object?properties[key]||{}:schema.items||{},index,(schema.required||[]).includes(key),object&&Object.hasOwn(properties,key))).join('');
const allowed=object?Object.keys(properties):[];
const picker=object&&allowed.length?``:'';
const addAllowed=!object||schema.additionalProperties!==false||allowed.length;
return `${children}
${addAllowed?`${picker}
`:''}`;
}
if(type==='boolean')return ``;
if(type==='null')return 'Kein Wert gesetzt.
';
if(type==='number'||type==='integer')return ``;
return ``;
}
function dataEntry(key,value,object,schema,index,required=false,known=false) {
const title=schema.title|| (object?editorLabel(key):`Eintrag ${index+1}`);
return `${object?`
`:''}
${dataNode(value,schema,title)}
${!object?'':''}${!required?'':'Pflichtfeld'}
`;
}
function readDataEditor(container,name) {
const editor=[...(container.matches?.('[data-editor]')?[container]:[]),...container.querySelectorAll('[data-editor]')].find(item=>item.dataset.editor===name);
if(!editor)throw new Error(`Das Eingabefeld „${editorLabel(name)}“ ist nicht verfügbar.`);
return readDataNode(editor.querySelector('[data-value-node]'));
}
function readDataNode(node) {
const type=node.dataset.valueKind;
const schema=JSON.parse(node.dataset.valueSchema||'{}');
if(Object.hasOwn(schema,'const'))return structuredClone(schema.const);
const content=node.querySelector(':scope > .data-node-content');
const enumInput=content.querySelector(':scope > label > [data-enum-value]');
if(enumInput)return JSON.parse(enumInput.value);
if(type==='object'||type==='array') {
const entries=[...content.querySelector(':scope > .data-children').children];
if(type==='array')return entries.map(entry=>readDataNode(entry.querySelector(':scope > .data-entry-value > [data-value-node]')));
const result={};
for(const entry of entries) {
const key=entry.querySelector(':scope > .data-key-label > [data-entry-key]').value.trim();
if(!key)throw new Error('Bitte jedem Feld einen Namen geben.');
if(Object.hasOwn(result,key))throw new Error(`Das Feld „${key}“ ist doppelt vorhanden.`);
Object.defineProperty(result,key,{value:readDataNode(entry.querySelector(':scope > .data-entry-value > [data-value-node]')),enumerable:true,writable:true,configurable:true});
}
return result;
}
if(type==='null')return null;
const input=content.querySelector('[data-scalar-value]');
if(type==='boolean')return input.checked;
if(type==='number'||type==='integer') {
const number=Number(input.value);
if(!input.value.trim()||!Number.isFinite(number)||(type==='integer'&&!Number.isInteger(number)))throw new Error('Bitte eine gültige Zahl eingeben.');
return number;
}
return input.value;
}
function wireDataEditors(container) {
if(container.dataset.dataEditorsBound)return;
container.dataset.dataEditorsBound='true';
container.addEventListener('click',event=>{
const button=event.target.closest('[data-edit-action]');
if(!button||!container.contains(button))return;
if(handledEditorEvents.has(event))return;
handledEditorEvents.add(event);
event.preventDefault();
const action=button.dataset.editAction;
const node=button.closest('[data-value-node]');
const schema=JSON.parse(node.dataset.valueSchema||'{}');
const entry=button.closest('[data-entry]');
if(action==='remove'){entry.remove();return;}
if(action==='up'){if(entry.previousElementSibling)entry.previousElementSibling.before(entry);return;}
if(action==='down'){if(entry.nextElementSibling)entry.nextElementSibling.after(entry);return;}
if(action==='add') {
const object=node.dataset.valueKind==='object';
const content=node.querySelector(':scope > .data-node-content');
const children=content.querySelector(':scope > .data-children');
const key=content.querySelector(':scope > .data-add [data-new-property]')?.value||'';
if(object&&key&&[...children.children].some(row=>row.querySelector('[data-entry-key]')?.value===key)){toast('Dieses Feld ist bereits vorhanden.',true);return;}
const childSchema=object?schema.properties?.[key]||{}:schema.items||{};
const type=editorSchemaType(childSchema,'');
children.insertAdjacentHTML('beforeend',dataEntry(key,editorDefault(type,childSchema),object,childSchema,children.children.length,(schema.required||[]).includes(key),object&&Object.hasOwn(schema.properties||{},key)));
children.lastElementChild.querySelector('input,select,textarea')?.focus();
}
});
container.addEventListener('change',event=>{
if(!event.target.matches('[data-value-type]'))return;
if(handledEditorEvents.has(event))return;
handledEditorEvents.add(event);
const node=event.target.closest('[data-value-node]');
const type=event.target.value;
node.dataset.valueKind=type;
node.querySelector(':scope > .data-node-content').innerHTML=dataNodeContent(editorDefault(type),type,JSON.parse(node.dataset.valueSchema||'{}'),'Wert');
});
}
function dataView(value) {
if(value===null||value===undefined)return 'Nicht gesetzt';
if(typeof value==='boolean')return `${value?'Ja':'Nein'}`;
if(Array.isArray(value))return value.length?`${value.map(item=>`- ${dataView(item)}
`).join('')}
`:'Keine Einträge';
if(typeof value==='object')return Object.keys(value).length?`${Object.entries(value).map(([key,item])=>`- ${esc(editorLabel(key))}
- ${dataView(item)}
`).join('')}
`:'Keine zusätzlichen Einstellungen';
return `${esc(value)}`;
}