@ -56,54 +56,54 @@ tailwind.config = {
.log-section { color:#22d3ee; font-weight:600 }
.log-section { color:#22d3ee; font-weight:600 }
< / style >
< / style >
< / head >
< / head >
< body class = "bg-bg dark:bg-bg-dark text-tx dark:text-tx-dark text- sm h-screen overflow-hidden">
< body class = "bg-bg dark:bg-bg-dark text-tx dark:text-tx-dark text- [15px] h-screen overflow-hidden">
<!-- Topbar -->
<!-- Topbar -->
< header class = "h-1 2 px-4 flex items-center gap-2 bg-s1 dark:bg-s1-dark border-b border-bd dark:border-bd-dark fixed inset-x-0 top-0 z-40">
< header class = "h-1 6 px-3 sm:px-5 flex items-center gap-2 sm:gap-3 bg-s1 dark:bg-s1-dark border-b border-bd dark:border-bd-dark fixed inset-x-0 top-0 z-40">
< button @ click = " sidebarOpen = !sidebarOpen" class = "w-8 h-8 grid place-items-center rounded-lg hover:bg-s2 dark:hover:bg-s2-dark" > < span class = "mdi mdi-menu text-lg "> < / span > < / button >
< button @ click = " toggleSidebar()" :title = "sidebarOpen ? t('hide_menu') : t('show_menu')" class = "w-11 h-11 grid place-items-center rounded-xl hover:bg-s2 dark:hover:bg-s2-dark active:scale-95 transition" > < span class = "mdi text-2xl" :class = "sidebarOpen ? 'mdi-backburger' : 'mdi-menu' "> < / span > < / button >
< span class = "font-extrabold text-wh dark:text-wh-dark"> Server< span class = "text-ac dark:text-ac-dark" > Up< / span > < / span >
< span class = "font-extrabold text-lg text-wh dark:text-wh-dark"> Server< span class = "text-ac dark:text-ac-dark" > Up< / span > < / span >
< span class = " ml-2 text-[10 px] font-mono text-t2 dark:text-t2-dark" x-text = "'v'+version" > < / span >
< span class = " text-[11 px] font-mono text-t2 dark:text-t2-dark" x-text = "'v'+version" > < / span >
< div class = "flex-1" > < / div >
< div class = "flex-1" > < / div >
< div class = "hidden sm:flex items-center gap- 3 text-[11px] font-mono text-t2 dark:text-t2-dark">
< div class = "hidden sm:flex items-center gap- 4 text-xs font-mono text-t2 dark:text-t2-dark">
< span class = "flex items-center gap-1 ">
< span class = "flex items-center gap-1 .5 ">
< span class = "w- 1.5 h-1.5 rounded-full" :class = "docker.running>0 ? 'bg-ok shadow-[0_0_5 px_#4ade80]' : 'bg-dm dark:bg-dm-dark'"> < / span >
< span class = "w- 2 h-2 rounded-full" :class = "docker.running>0 ? 'bg-ok shadow-[0_0_6 px_#4ade80]' : 'bg-dm dark:bg-dm-dark'"> < / span >
< span class = "mdi mdi-play-circle-outline "> < / span > < span x-text = "docker.running ?? '– '" > < / span >
< span class = "mdi mdi-play-circle-outline text-base "> < / span > < span x-text = "docker.running ?? '– '" > < / span >
< / span >
< / span >
< span class = "flex items-center gap-1 "> < span class = "mdi mdi-layers-outline "> < / span > < span x-text = "docker.images ?? '– '" > < / span > < / span >
< span class = "flex items-center gap-1 .5 "> < span class = "mdi mdi-layers-outline text-base "> < / span > < span x-text = "docker.images ?? '– '" > < / span > < / span >
< / div >
< / div >
< button @ click = "termOpen = !termOpen" class= "px-2 py-1 rounded-md border border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:text-ac hover:border-ac flex items-center gap-1 text-xs "> < span class = "mdi mdi-console "> < / span > < / button >
< button @ click = "termOpen = !termOpen" :title= "t('log')" class = "btn-icon "> < span class = "mdi mdi-console text-xl "> < / span > < / button >
< button @ click = "toggleTheme()" class= "px-2 py-1 rounded-md hover:bg-s2 dark:hover:bg-s2-dark "> < span class = "mdi " :class = "theme==='dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'" > < / span > < / button >
< button @ click = "toggleTheme()" :title= "t('theme')" class = "btn-icon "> < span class = "mdi text-xl " :class = "theme==='dark' ? 'mdi-weather-sunny' : 'mdi-weather-night'" > < / span > < / button >
< / header >
< / header >
<!-- Sidebar -->
<!-- Sidebar -->
< aside class = "fixed top-1 2 bottom-0 left-0 w-56 bg-s1 dark:bg-s1-dark border-r border-bd dark:border-bd-dark transition-transform z-30 overflow-y-auto"
< aside class = "fixed top-1 6 bottom-0 left-0 w-72 max-w-[85vw] bg-s1 dark:bg-s1-dark border-r border-bd dark:border-bd-dark transition-transform duration-200 ease-out z-30 overflow-y-auto"
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0 '">
:class="sidebarOpen ? 'translate-x-0' : '-translate-x-full'">
< nav class = "p- 2 space-y-0 .5">
< nav class = "p- 3 space-y-1 .5">
< template x-for = "item in navItems" :key = "item.id" >
< template x-for = "item in navItems" :key = "item.id" >
< a href = "#" @ click . prevent = "goto(item.id)"
< a href = "#" @ click . prevent = "goto(item.id)"
class="flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-xs font-medium transition"
class="flex items-center gap-3 px-4 py-3.5 rounded-xl cursor-pointer text-sm font-semibold transition"
:class="page === item.id ? 'bg-ac/10 text-ac dark:text-ac-dark border border-ac/20 ' : 'text-t2 dark:text-t2-dark hover:bg-s2 dark:hover:bg-s2-dark hover:text-tx dark:hover:text-tx-dark border border-transparent'">
:class="page === item.id ? 'bg-ac/10 text-ac dark:text-ac-dark border border-ac/30 shadow-sm ' : 'text-t2 dark:text-t2-dark hover:bg-s2 dark:hover:bg-s2-dark hover:text-tx dark:hover:text-tx-dark border border-transparent'">
< span class = "mdi text- base w-5 text-center" :class = "item.icon" > < / span >
< span class = "mdi text- xl w-6 text-center" :class = "item.icon" > < / span >
< span x-text = "t(item.label)" > < / span >
< span x-text = "t(item.label)" > < / span >
< / a >
< / a >
< / template >
< / template >
< template x-if = "modules.length > 0" >
< template x-if = "modules.length > 0" >
< div class = "pt- 3 pb-1 px-3 text-[8 px] font-bold tracking-widest uppercase text-dm dark:text-dm-dark" x-text = "t('modules')" > Modules< / div >
< div class = "pt- 4 pb-1 px-4 text-[10 px] font-bold tracking-widest uppercase text-dm dark:text-dm-dark" x-text = "t('modules')" > Modules< / div >
< / template >
< / template >
< template x-for = "m in modules" :key = "m.id" >
< template x-for = "m in modules" :key = "m.id" >
< a href = "#" @ click . prevent = "goto('mod:'+m.id)"
< a href = "#" @ click . prevent = "goto('mod:'+m.id)"
class="flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer text-xs font-medium transition text-t2 dark:text-t2-dark hover:bg-s2 dark:hover:bg-s2-dark">
class="flex items-center gap-3 px-4 py-3.5 rounded-xl cursor-pointer text-sm font-semibold transition text-t2 dark:text-t2-dark hover:bg-s2 dark:hover:bg-s2-dark">
< span class = "text- base w-5 text-center" x-text = "m.icon || '🧩'" > < / span >
< span class = "text- xl w-6 text-center" x-text = "m.icon || '🧩'" > < / span >
< span x-text = "m.name" > < / span >
< span x-text = "m.name" > < / span >
< / a >
< / a >
< / template >
< / template >
< / nav >
< / nav >
< / aside >
< / aside >
< div x-show = "sidebarOpen " @ click = "sidebarOpen=false" class = " md:hidden fixed inset-0 bg-black/40 z-20" x-cloak > < / div >
< div x-show = "sidebarOpen && isMobile " @ click = "sidebarOpen=false" class = " fixed inset-0 top-16 bg-black/50 backdrop-blur-sm z-20" x-cloak > < / div >
<!-- Main -->
<!-- Main -->
< main class = " md:ml-56 pt-12 h-screen overflow-y-auto ">
< main class = " pt-16 h-screen overflow-y-auto transition-[margin] duration-200 ease-out" :class = "(sidebarOpen && !isMobile) ? 'md:ml-72' : 'ml-0' ">
< div class = "p- 5 pb-48">
< div class = "p- 4 sm:p-6 max-w-6xl mx-auto pb-48">
<!-- Dashboard -->
<!-- Dashboard -->
< section x-show = "page === 'dashboard'" x-cloak >
< section x-show = "page === 'dashboard'" x-cloak >
@ -325,13 +325,48 @@ tailwind.config = {
< / div >
< / div >
< / div >
< / div >
< div >
< div >
< div class = "text-[ 9 px] font-bold tracking-wider uppercase text-dm dark:text-dm-dark mb-1" x-text = "t('language')" > Taal< / div >
< div class = "text-[ 10 px] font-bold tracking-wider uppercase text-dm dark:text-dm-dark mb-1.5 " x-text = "t('language')" > Taal< / div >
< select x-model = "settings.LANGUAGE" @ change = "setLang(settings.LANGUAGE)" class = " bg-s2 dark:bg-s2-dark border border-bd dark:border-bd-dark rounded-md px-2 py-1 text-xs ">
< select x-model = "settings.LANGUAGE" @ change = "setLang(settings.LANGUAGE)" class = " form-input !w-auto min-w-[160px] ">
< template x-for = "l in languages" :key = "l.code" > < option :value = "l.code" x-text = "(l.flag||'')+' '+l.name" > < / option > < / template >
< template x-for = "l in languages" :key = "l.code" > < option :value = "l.code" x-text = "(l.flag||'')+' '+l.name" > < / option > < / template >
< / select >
< / select >
< / div >
< / div >
< / div >
< / div >
<!-- Taalmodule (add - on) -->
< details class = "card mb-3" id = "lang-addon" >
< summary class = "px-4 py-3.5 cursor-pointer font-bold text-wh dark:text-wh-dark text-sm flex items-center gap-2" >
< span class = "mdi mdi-translate text-ac dark:text-ac-dark text-lg" > < / span >
< span x-text = "t('language_addon')" > Taalmodule< / span >
< span class = "badge badge-info ml-1" x-text = "languages.length" > < / span >
< span class = "ml-auto flex gap-2" @ click . stop >
< button class = "btn-sm" @ click = "openLangEditor(null)" > < span class = "mdi mdi-plus" > < / span > < span x-text = "t('add_language')" > Taal toevoegen< / span > < / button >
< / span >
< / summary >
< div class = "px-4 pb-4" >
< p class = "text-xs text-t2 dark:text-t2-dark mb-3" x-text = "t('language_addon_hint')" > Voeg extra talen toe als add-on. Ingebouwde talen kunnen niet bewerkt of verwijderd worden.< / p >
< div class = "space-y-2" >
< template x-for = "l in languages" :key = "l.code" >
< div class = "flex items-center gap-3 p-3 rounded-xl bg-s2 dark:bg-s2-dark" >
< span class = "text-2xl" x-text = "l.flag || '🌐'" > < / span >
< div class = "flex-1 min-w-0" >
< div class = "font-semibold text-wh dark:text-wh-dark text-sm truncate" > < span x-text = "l.name" > < / span > < span class = "font-mono text-[11px] text-t2 dark:text-t2-dark" x-text = "'('+l.code+')'" > < / span > < / div >
< div class = "text-[11px] text-dm dark:text-dm-dark" > < span x-text = "l.count" > < / span > < span x-text = "t('strings_count')" > strings< / span > < template x-if = "l.builtin" > < span class = "ml-2 badge badge-dim" x-text = "t('builtin')" > ingebouwd< / span > < / template > < / div >
< / div >
< template x-if = "!l.builtin" >
< button class = "btn-sm" @ click = "openLangEditor(l.code)" > < span class = "mdi mdi-pencil-outline" > < / span > < span x-text = "t('edit')" > Bewerken< / span > < / button >
< / template >
< template x-if = "!l.builtin" >
< button class = "btn-sm btn-danger" @ click = "deleteLang(l.code)" > < span class = "mdi mdi-delete-outline" > < / span > < / button >
< / template >
< template x-if = "l.builtin" >
< button class = "btn-sm" @ click = "openLangEditor(l.code)" > < span class = "mdi mdi-content-copy" > < / span > < span x-text = "t('duplicate')" > Dupliceren< / span > < / button >
< / template >
< / div >
< / template >
< / div >
< / div >
< / details >
< details class = "card mb-3" open >
< details class = "card mb-3" open >
< summary class = "px-4 py-3 cursor-pointer font-semibold text-wh dark:text-wh-dark text-xs flex items-center gap-2" > < span class = "mdi mdi-folder-cog-outline text-ac dark:text-ac-dark" > < / span > < span x-text = "t('paths')" > Paden< / span > < / summary >
< summary class = "px-4 py-3 cursor-pointer font-semibold text-wh dark:text-wh-dark text-xs flex items-center gap-2" > < span class = "mdi mdi-folder-cog-outline text-ac dark:text-ac-dark" > < / span > < span x-text = "t('paths')" > Paden< / span > < / summary >
< div class = "px-4 pb-4 grid sm:grid-cols-3 gap-3" >
< div class = "px-4 pb-4 grid sm:grid-cols-3 gap-3" >
@ -370,8 +405,8 @@ tailwind.config = {
< / main >
< / main >
<!-- Terminal -->
<!-- Terminal -->
< div class = "fixed bottom-0 right-0 left-0 md:left-56 bg-bg-dark/95 border-t border-bd dark:border-bd-dark transition-transform z-30"
< div class = "fixed bottom-0 right-0 left-0 bg-bg-dark/95 border-t border-bd dark:border-bd-dark transition-all duration-200 z-30"
:class="termOpen ? 'translate-y-0' : 'translate-y-[180px]'">
:class="[(sidebarOpen & & !isMobile) ? 'md:left-72' : 'left-0', termOpen ? 'translate-y-0' : 'translate-y-[180px]'] ">
< div @ click = "termOpen = !termOpen" class = "h-9 px-3 flex items-center gap-2 bg-s1 dark:bg-s1-dark border-b border-bd dark:border-bd-dark cursor-pointer" >
< div @ click = "termOpen = !termOpen" class = "h-9 px-3 flex items-center gap-2 bg-s1 dark:bg-s1-dark border-b border-bd dark:border-bd-dark cursor-pointer" >
< div class = "flex gap-1.5" > < span class = "w-2 h-2 rounded-full bg-[#ff5f57]" > < / span > < span class = "w-2 h-2 rounded-full bg-[#febc2e]" > < / span > < span class = "w-2 h-2 rounded-full bg-[#28c840]" > < / span > < / div >
< div class = "flex gap-1.5" > < span class = "w-2 h-2 rounded-full bg-[#ff5f57]" > < / span > < span class = "w-2 h-2 rounded-full bg-[#febc2e]" > < / span > < span class = "w-2 h-2 rounded-full bg-[#28c840]" > < / span > < / div >
< span class = "text-[11px] font-mono text-t2 dark:text-t2-dark flex-1" > < span class = "mdi mdi-console" > < / span > Log< / span >
< span class = "text-[11px] font-mono text-t2 dark:text-t2-dark flex-1" > < span class = "mdi mdi-console" > < / span > Log< / span >
@ -565,6 +600,39 @@ tailwind.config = {
< button class = "btn" @ click = "modal=null" x-text = "t('close')" > Sluiten< / button >
< button class = "btn" @ click = "modal=null" x-text = "t('close')" > Sluiten< / button >
< / div >
< / div >
< / div >
< / div >
<!-- Language editor -->
< div x-show = "modal==='lang'" class = "bg-s1 dark:bg-s1-dark border border-bd2 dark:border-bd2-dark rounded-2xl p-5 w-full max-w-2xl max-h-[88vh] flex flex-col" >
< h3 class = "text-lg font-extrabold text-wh dark:text-wh-dark mb-4 flex items-center gap-2" >
< span class = "mdi mdi-translate text-ac dark:text-ac-dark" > < / span >
< span x-text = "langEdit.isNew ? t('add_language') : t('edit_language')" > Taal< / span >
< / h3 >
< div class = "grid sm:grid-cols-3 gap-3 mb-3" >
< div > < label class = "form-label" x-text = "t('lang_code')" > Code< / label > < input class = "form-input" x-model = "langEdit.code" :readonly = "!langEdit.isNew" placeholder = "de, fr, pt-br" > < / div >
< div > < label class = "form-label" x-text = "t('lang_name')" > Naam< / label > < input class = "form-input" x-model = "langEdit.name" placeholder = "Deutsch" > < / div >
< div > < label class = "form-label" x-text = "t('lang_flag')" > Vlag< / label > < input class = "form-input" x-model = "langEdit.flag" placeholder = "🇩🇪" maxlength = "8" > < / div >
< / div >
< div class = "relative mb-3" >
< span class = "mdi mdi-magnify absolute left-3 top-1/2 -translate-y-1/2 text-dm dark:text-dm-dark" > < / span >
< input x-model = "langEdit.query" :placeholder = "t('search_keys')" class = "form-input pl-9" >
< / div >
< div class = "flex-1 overflow-y-auto -mx-1 px-1 space-y-2" style = "max-height:48vh" >
< template x-for = "k in langKeysFiltered" :key = "k" >
< div class = "grid sm:grid-cols-2 gap-2 items-center" >
< div class = "text-xs" >
< div class = "font-mono text-[11px] text-ac dark:text-ac-dark" x-text = "k" > < / div >
< div class = "text-t2 dark:text-t2-dark text-[11px] truncate" :title = "langEdit.ref[k]" x-text = "langEdit.ref[k]" > < / div >
< / div >
< input class = "form-input" x-model = "langEdit.values[k]" :placeholder = "langEdit.ref[k]" >
< / div >
< / template >
< p x-show = "langKeysFiltered.length===0" class = "text-center text-dm dark:text-dm-dark text-xs py-6" x-text = "t('no_results')" > Geen resultaten< / p >
< / div >
< div class = "flex justify-end gap-2 mt-4 pt-3 border-t border-bd dark:border-bd-dark" >
< button class = "btn" @ click = "modal=null" x-text = "t('cancel')" > Annuleren< / button >
< button class = "btn btn-primary" @ click = "saveLang()" > < span class = "mdi mdi-content-save" > < / span > < span x-text = "t('save')" > Opslaan< / span > < / button >
< / div >
< / div >
< / div >
< / div >
<!-- First - run wizard -->
<!-- First - run wizard -->
@ -667,19 +735,19 @@ tailwind.config = {
<!-- Tailwind component classes -->
<!-- Tailwind component classes -->
< style type = "text/tailwindcss" >
< style type = "text/tailwindcss" >
@layer components {
@layer components {
.btn { @apply inline-flex items-center gap-1 px-3 py-1.5 rounded-md border border-bd dark:border-bd-dark bg-s1 dark:bg-s1-dark text-tx dark:text-tx-dark text-xs font-medium hover:border-ac hover:text-ac dark:hover:text-ac-dark transition cursor-pointer; }
.btn { @apply inline-flex items-center justify-center gap-1.5 px-4 py-2.5 min-h-[44px] rounded-xl border border-bd dark:border-bd-dark bg-s1 dark:bg-s1-dark text-tx dark:text-tx-dark text-sm font-semibold hover:border-ac hover:text-ac dark:hover:text-ac-dark active:scale-[.97] transition cursor-pointer; }
.btn-primary { @apply !bg-ac !text-black !border-ac hover:!brightness-110 font-semi bold; }
.btn-primary { @apply !bg-ac !text-black !border-ac hover:!brightness-110 shadow-sm shadow-ac/20 font-bold; }
.btn-danger { @apply !text-er !border-er/30 hover:!bg-er/10; }
.btn-danger { @apply !text-er !border-er/30 hover:!bg-er/10; }
.btn-icon { @apply w-8 h-8 grid place-items-center rounded-md border border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:text-ac hover:border-ac transition cursor-pointer; }
.btn-icon { @apply w-11 h-11 grid place-items-center rounded-xl border border-bd dark:border-bd-dark text-t2 dark:text-t2-dark hover:text-ac hover:border-ac active:scale-95 transition cursor-pointer; }
.btn-sm { @apply inline-flex items-center gap-1 px-2 py-1 rounded border border-bd dark:border-bd-dark text-tx dark:text-tx-dark text-[10px] hover:border-ac hover:text-ac dark:hover:text-ac-dark cursor-pointer transition; }
.btn-sm { @apply inline-flex items-center justify-center gap-1.5 px-3 py-2 min-h-[38px] rounded-lg border border-bd dark:border-bd-dark text-tx dark:text-tx-dark text-xs font-semibold hover:border-ac hover:text-ac dark:hover:text-ac-dark active:scale-95 cursor-pointer transition; }
.card { @apply bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-xl overflow-hidden; }
.card { @apply bg-s1 dark:bg-s1-dark border border-bd dark:border-bd-dark rounded-2 xl overflow-hidden shadow-sm shadow-black/[0.03] dark:shadow-black/20 ; }
.stat-card { @apply card p-3 flex items-center gap-3 ; }
.stat-card { @apply card p-4 flex items-center gap-4 ; }
.stat-icon { @apply w-9 h-9 rounded-lg grid place-items-center text-base ; }
.stat-icon { @apply w-12 h-12 rounded-xl grid place-items-center text-xl ; }
.stat-value { @apply text-xl font-extrabold text-wh dark:text-wh-dark leading-none; }
.stat-value { @apply text-2 xl font-extrabold text-wh dark:text-wh-dark leading-none; }
.stat-label { @apply text-[9px] text-t2 dark:text-t2-dark mt-0.5 uppercase tracking-wider ; }
.stat-label { @apply text-[10px] text-t2 dark:text-t2-dark mt-1 uppercase tracking-wider font-semibold ; }
.form-label { @apply block text-[9 px] font-bold tracking-wider uppercase text-dm dark:text-dm-dark mb-1; }
.form-label { @apply block text-[10 px] font-bold tracking-wider uppercase text-dm dark:text-dm-dark mb-1.5 ; }
.form-input { @apply w-full bg-s2 dark:bg-s2-dark border border-bd dark:border-bd-dark rounded-md px-3 py-1.5 text-xs font-mono text-tx dark:text-tx-dark outline-none focus:border-ac focus:bg-s1 dark:focus:bg-s1-dark transition; }
.form-input { @apply w-full bg-s2 dark:bg-s2-dark border border-bd dark:border-bd-dark rounded-lg px-3 py-2.5 min-h-[44px] text-sm font-mono text-tx dark:text-tx-dark outline-none focus:border-ac focus:ring-2 focus:ring-ac/20 focus:bg-s1 dark:focus:bg-s1-dark transition; }
.badge { @apply text-[8px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded-full; }
.badge { @apply text-[9px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full; }
.badge-ok { @apply bg-ok/10 text-ok; }
.badge-ok { @apply bg-ok/10 text-ok; }
.badge-warn { @apply bg-wr/10 text-wr; }
.badge-warn { @apply bg-wr/10 text-wr; }
.badge-info { @apply bg-bl/10 text-bl; }
.badge-info { @apply bg-bl/10 text-bl; }
@ -693,7 +761,8 @@ function app() {
return {
return {
version: '0.3.0',
version: '0.3.0',
page: location.hash.slice(1) || 'dashboard',
page: location.hash.slice(1) || 'dashboard',
sidebarOpen: window.innerWidth >= 768,
isMobile: window.innerWidth < 768 ,
sidebarOpen: window.innerWidth >= 768 & & localStorage.getItem('sidebar') !== 'closed',
termOpen: false,
termOpen: false,
theme: localStorage.getItem('theme') || 'dark',
theme: localStorage.getItem('theme') || 'dark',
saved: false,
saved: false,
@ -724,6 +793,7 @@ function app() {
},
},
editor: { stack:'', type:'env', title:'', content:'' },
editor: { stack:'', type:'env', title:'', content:'' },
logs: { title:'', content:'' },
logs: { title:'', content:'' },
langEdit: { isNew:true, code:'', name:'', flag:'', keys:[], ref:{}, values:{}, query:'' },
wizard: { open:false, step:0, steps: [], LIBRARY_DIR:'', DATA_DIR:'', BACKUP_DIR:'',
wizard: { open:false, step:0, steps: [], LIBRARY_DIR:'', DATA_DIR:'', BACKUP_DIR:'',
git: { choice:'skip', url:'', token:'', subdir:'', port_web:3000, port_ssh:2222 } },
git: { choice:'skip', url:'', token:'', subdir:'', port_web:3000, port_ssh:2222 } },
@ -738,6 +808,10 @@ function app() {
async init() {
async init() {
window.addEventListener('hashchange', () => this.page = location.hash.slice(1) || 'dashboard');
window.addEventListener('hashchange', () => this.page = location.hash.slice(1) || 'dashboard');
window.addEventListener('resize', () => {
const m = window.innerWidth < 768 ;
if (m !== this.isMobile) { this.isMobile = m; if (m) this.sidebarOpen = false; else this.sidebarOpen = localStorage.getItem('sidebar') !== 'closed'; }
});
await this.loadLangs();
await this.loadLangs();
await this.loadLang(localStorage.getItem('lang') || 'nl');
await this.loadLang(localStorage.getItem('lang') || 'nl');
await this.checkWizard();
await this.checkWizard();
@ -759,6 +833,57 @@ function app() {
async loadLang(code) { try { const d = await fetch('/api/i18n/'+code).then(r=>r.json()); this.T = d.strings || {}; } catch(e) {} },
async loadLang(code) { try { const d = await fetch('/api/i18n/'+code).then(r=>r.json()); this.T = d.strings || {}; } catch(e) {} },
setLang(c) { localStorage.setItem('lang', c); this.loadLang(c); },
setLang(c) { localStorage.setItem('lang', c); this.loadLang(c); },
async openLangEditor(code) {
const ref = await this.rpc('/api/i18n/keys');
const keys = ref.keys || [];
const refMap = ref.en || {};
let values = {}, meta = { code:'', name:'', flag:'' }, isNew = true;
if (code) {
const raw = await this.rpc('/api/i18n/'+code+'/raw');
const existing = this.languages.find(l => l.code === code) || {};
if (existing.builtin) {
// Dupliceren: leeg code, gebruik bestaande strings als startpunt
values = { ...(raw.strings || {}) };
meta = { code:'', name: (existing.name||'')+' (kopie)', flag: existing.flag||'' };
isNew = true;
} else {
values = { ...(raw.strings || {}) };
meta = { code, name: existing.name||'', flag: existing.flag||'' };
isNew = false;
}
}
this.langEdit = { isNew, ...meta, keys, ref: refMap, values, query:'' };
this.modal = 'lang';
},
get langKeysFiltered() {
const q = (this.langEdit.query||'').toLowerCase();
if (!q) return this.langEdit.keys;
return this.langEdit.keys.filter(k =>
k.toLowerCase().includes(q) || String(this.langEdit.ref[k]||'').toLowerCase().includes(q));
},
async saveLang() {
const e = this.langEdit;
if (!e.code.trim()) { this.toast(this.t('lang_code')+' ?', 'wr'); return; }
const strings = {};
for (const [k,v] of Object.entries(e.values)) { if (v !== undefined & & v !== null & & String(v).trim() !== '') strings[k] = v; }
const d = await this.rpc('/api/i18n', { code: e.code.trim().toLowerCase(), name: e.name.trim(), flag: e.flag.trim(), strings });
if (d.ok) {
this.toast(d.msg || this.t('saved'), 'ok');
this.modal = null;
await this.loadLangs();
if (d.code & & d.code === (localStorage.getItem('lang') || 'nl')) await this.loadLang(d.code);
} else { this.toast(d.msg || 'Fout', 'er'); }
},
async deleteLang(code) {
if (!confirm(this.t('delete_language_confirm') || ('Taal '+code+' verwijderen?'))) return;
const d = await fetch('/api/i18n/'+code, { method:'DELETE' }).then(r=>r.json()).catch(()=>({}));
if (d.ok) {
this.toast(d.msg || this.t('deleted'), 'ok');
if (localStorage.getItem('lang') === code) this.setLang('nl');
await this.loadLangs();
} else { this.toast(d.msg || 'Fout', 'er'); }
},
toggleTheme() { this.theme = this.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('theme', this.theme); this.autoSave(); },
toggleTheme() { this.theme = this.theme === 'dark' ? 'light' : 'dark'; localStorage.setItem('theme', this.theme); this.autoSave(); },
toast(msg, kind='in') {
toast(msg, kind='in') {
@ -776,7 +901,12 @@ function app() {
goto(id) {
goto(id) {
this.page = id;
this.page = id;
location.hash = '#' + id;
location.hash = '#' + id;
if (window.innerWidth < 768 ) this . sidebarOpen = false;
if (this.isMobile) this.sidebarOpen = false;
},
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
if (!this.isMobile) localStorage.setItem('sidebar', this.sidebarOpen ? 'open' : 'closed');
},
},
pollJob(jid, onDone) {
pollJob(jid, onDone) {