/* 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=>`
  1. ${dataView(item)}
  2. `).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)}`; }