2026-06-09 00:41:27 +00:00
{% extends "base.html" %}
{% block content %}
< div x-data = "settingsPage()" x-init = "load()" >
< header class = "page-header settings-header" >
< div >
< h1 > Settings</ h1 >
< p class = "subtitle page-tip" > Configure integrations — Herman reads active settings from PostgreSQL.</ p >
</ div >
</ header >
< nav class = "settings-tabs" >
< a href = "/settings?tab=email" class = "settings-tab {% if active_tab == 'email' %}active{% endif %}" > Email</ a >
< a href = "/settings?tab=social" class = "settings-tab {% if active_tab == 'social' %}active{% endif %}" > Social API's</ a >
< a href = "/settings?tab=permissions" class = "settings-tab {% if active_tab == 'permissions' %}active{% endif %}" > Rechten Herman</ a >
2026-08-16 11:01:58 +00:00
{% if can_manage_users %}
< a href = "/settings?tab=users" class = "settings-tab {% if active_tab == 'users' %}active{% endif %}" > Gebruikers</ a >
{% endif %}
2026-08-16 15:49:16 +00:00
< a href = "/settings?tab=themes" class = "settings-tab {% if active_tab == 'themes' %}active{% endif %}" > Themes</ a >
2026-06-09 00:41:27 +00:00
< a href = "/settings?tab=general" class = "settings-tab {% if active_tab == 'general' %}active{% endif %}" > General</ a >
</ nav >
{% if active_tab == 'email' %}
< section class = "panel settings-panel" >
< div class = "settings-panel-head" >
< div >
< h2 > Email accounts</ h2 >
< p class = "page-tip" > Kies welk mailbox Herman gebruikt om te versturen. IMAP velden zijn voor latere inbox-sync.</ p >
</ div >
< button type = "button" class = "btn btn-primary" @ click = "openForm()" > + Nieuw account</ button >
</ div >
< div class = "settings-active-banner" x-show = "activeAccount" x-cloak >
< span class = "badge badge-positive" > Actief</ span >
< span x-text = "activeAccount ? activeAccount.label + ' · ' + activeAccount.email_address : ''" ></ span >
</ div >
< div class = "email-account-list" >
< template x-for = "acc in accounts" :key = "acc.id" >
< article class = "email-account-card" :class = "{ 'is-active': acc.is_active }" >
< div class = "email-account-card-head" >
< div >
< strong x-text = "acc.label" ></ strong >
< span class = "muted" x-text = "acc.email_address" ></ span >
</ div >
< span class = "badge" :class = "acc.is_active ? 'badge-positive' : 'badge-neutral'" x-text = "acc.is_active ? 'Actief' : acc.provider" ></ span >
</ div >
< div class = "email-account-meta" >
< span x-text = "'SMTP: ' + (acc.smtp_host || '—') + ':' + (acc.smtp_port || '')" ></ span >
< span x-text = "acc.smtp_password_set ? '🔒 wachtwoord opgeslagen' : '⚠️ geen SMTP wachtwoord'" ></ span >
< span x-show = "acc.last_test_status" x-text = "'Test: ' + acc.last_test_status" ></ span >
</ div >
< div class = "btn-group" >
< button type = "button" class = "btn btn-secondary btn-sm" @ click = "edit(acc)" > Bewerken</ button >
< button type = "button" class = "btn btn-primary btn-sm" @ click = "activate(acc.id)" x-show = "!acc.is_active" > Maak actief</ button >
< button type = "button" class = "btn btn-secondary btn-sm" @ click = "testAccount(acc.id)" > Test SMTP</ button >
< button type = "button" class = "btn btn-reject btn-sm" @ click = "remove(acc.id)" > Verwijder</ button >
</ div >
</ article >
</ template >
< p class = "empty-state" x-show = "!accounts.length && !loading" > Nog geen email account — voeg er een toe om Herman mail te laten versturen.</ p >
</ div >
</ section >
< div class = "modal" :class = "{ open: formOpen }" @ click . self = "formOpen=false" >
< div class = "modal-card settings-modal" >
< h3 x-text = "form.id ? 'Email account bewerken' : 'Nieuw email account'" ></ h3 >
< label class = "form-label" > Label
< input class = "form-input" x-model = "form.label" placeholder = "bv. Foodlinkk sales" />
</ label >
< label class = "form-label" > Email adres
< input class = "form-input" type = "email" x-model = "form.email_address" placeholder = "info@foodlinkk.nl" />
</ label >
< label class = "form-label" > Provider (vult SMTP/IMAP automatisch in)
< select class = "form-input" x-model = "form.provider" @ change = "applyProvider()" >
< option value = "custom" > Custom / eigen server</ option >
< option value = "gmail" > Gmail</ option >
< option value = "outlook" > Outlook / Microsoft 365</ option >
< option value = "zoho" > Zoho Mail</ option >
</ select >
</ label >
< div class = "settings-form-grid" >
< label class = "form-label" > SMTP host< input class = "form-input" x-model = "form.smtp_host" /></ label >
< label class = "form-label" > SMTP port< input class = "form-input" type = "number" x-model . number = "form.smtp_port" /></ label >
< label class = "form-label" > SMTP user< input class = "form-input" x-model = "form.smtp_user" /></ label >
< label class = "form-label" > SMTP wachtwoord
< input class = "form-input" type = "password" x-model = "form.smtp_password" :placeholder = "form.smtp_password_set ? '•••••••• (laat leeg om te behouden)' : 'App-wachtwoord'" />
</ label >
</ div >
< details class = "settings-details" >
< summary > IMAP (inbox sync — later)</ summary >
< div class = "settings-form-grid" >
< label class = "form-label" > IMAP host< input class = "form-input" x-model = "form.imap_host" /></ label >
< label class = "form-label" > IMAP port< input class = "form-input" type = "number" x-model . number = "form.imap_port" /></ label >
< label class = "form-label" > IMAP user< input class = "form-input" x-model = "form.imap_user" /></ label >
< label class = "form-label" > IMAP wachtwoord
< input class = "form-input" type = "password" x-model = "form.imap_password" :placeholder = "form.imap_password_set ? '••••••••' : 'App-wachtwoord'" />
</ label >
</ div >
< label class = "form-check" >
< input type = "checkbox" x-model = "form.sync_enabled" /> Inbox sync inschakelen (wanneer beschikbaar)
</ label >
</ details >
< label class = "form-check" >
< input type = "checkbox" x-model = "form.is_active" /> Direct als actief account instellen
</ label >
< div class = "btn-group" >
< button type = "button" class = "btn btn-primary" @ click = "save()" :disabled = "saving" > Opslaan</ button >
< button type = "button" class = "btn btn-secondary" @ click = "testForm()" :disabled = "saving" > Test SMTP</ button >
< button type = "button" class = "btn btn-secondary" @ click = "formOpen=false" > Annuleren</ button >
</ div >
</ div >
</ div >
{% elif active_tab == 'permissions' %}
< section class = "panel settings-panel hm-neo" x-data = "permissionsPage()" x-init = "loadPerms()" >
< div class = "settings-panel-head" >
< div >
< h2 > Herman — Module rechten</ h2 >
< p class = "page-tip" > Bepaal welke bedrijfsprocessen Herman mag benaderen. Grant all = volledige toegang.</ p >
</ div >
< button type = "button" class = "btn btn-pulse-green" @ click = "grantAll()" > 👑 Alles aan Herman</ button >
</ div >
< p >< span class = "live-badge" > Live</ span > < span x-text = "grantedCount + ' / ' + total + ' modules actief'" ></ span ></ p >
< div class = "perm-grid" >
< template x-for = "p in perms" :key = "p.module_key" >
< div class = "perm-card" :class = "p.granted ? 'granted' : ''" >
< div >
< strong x-text = "p.module_label" ></ strong >
< p style = "font-size:0.8rem;margin:0.25rem 0;color:#94a3b8" x-text = "p.description" ></ p >
< small x-text = "p.category" ></ small >
</ div >
< label class = "form-check" >
< input type = "checkbox" :checked = "p.granted" @ change = "toggle(p.module_key, $event.target.checked)" > Toegang
</ label >
</ div >
</ template >
</ div >
</ section >
2026-08-16 11:01:58 +00:00
{% elif active_tab == 'users' and can_manage_users %}
< section class = "panel settings-panel hm-neo" x-data = "usersAdminPage()" x-init = "load()" >
< div class = "settings-panel-head" >
< div >
< h2 > Gebruikers & rechten</ h2 >
< p class = "page-tip" > Maak accounts aan en bepaal welke cockpit-pagina's zichtbaar zijn. Admins hebben altijd volledige toegang.</ p >
</ div >
< button type = "button" class = "btn btn-primary" @ click = "openCreate()" > + Nieuwe gebruiker</ button >
</ div >
< div class = "email-account-list" >
< template x-for = "u in users" :key = "u.id" >
< article class = "email-account-card" :class = "{ 'is-active': u.is_active }" >
< div class = "email-account-card-head" >
< div style = "display:flex;align-items:center;gap:0.75rem;min-width:0" >
< img class = "user-card-avatar" x-show = "u.avatar_url" :src = "u.avatar_url" alt = "" />
< span class = "user-card-avatar fallback" x-show = "!u.avatar_url" x-text = "initials(u)" ></ span >
< div style = "min-width:0" >
< strong x-text = "u.display_name" ></ strong >
< span class = "muted" x-text = "'@' + u.username" ></ span >
</ div >
</ div >
< span class = "badge" :class = "u.role === 'admin' ? 'badge-positive' : 'badge-neutral'" x-text = "u.role" ></ span >
</ div >
< div class = "email-account-meta" >
< span x-text = "u.is_active ? 'Actief' : 'Uitgeschakeld'" ></ span >
< span x-text = "u.role === 'admin' ? 'Alle pagina\'s' : ((u.permissions.pages||[]).length + ' pagina\'s')" ></ span >
< span x-show = "u.last_login_at" x-text = "'Laatste login: ' + (u.last_login_at || '')" ></ span >
</ div >
< div class = "btn-group" >
< button type = "button" class = "btn btn-secondary btn-sm" @ click = "edit(u)" > Bewerken</ button >
< button type = "button" class = "btn btn-reject btn-sm" @ click = "remove(u)" x-show = "u.id !== meId" > Verwijder</ button >
</ div >
</ article >
</ template >
< p class = "empty-state" x-show = "!users.length && !loading" > Nog geen gebruikers.</ p >
</ div >
< div class = "modal" :class = "{ open: formOpen }" @ click . self = "formOpen=false" >
< div class = "modal-card settings-modal" style = "max-width:560px" >
< h3 x-text = "form.id ? 'Gebruiker bewerken' : 'Nieuwe gebruiker'" ></ h3 >
< div class = "user-form-avatar-row" >
< img class = "user-form-avatar-preview" x-show = "form.avatar_url || form.avatarPreview"
:src = "form.avatarPreview || form.avatar_url" alt = "" />
< span class = "user-form-avatar-preview fallback" x-show = "!form.avatar_url && !form.avatarPreview"
x-text = "formInitials()" ></ span >
< div >
< input type = "file" accept = "image/png,image/jpeg,image/webp,image/gif"
@ change = "onAvatarPick($event)" />
< p class = "page-tip" style = "margin:0.35rem 0 0" > JPG/PNG/WEBP, max 3 MB. Bij nieuwe user: eerst opslaan, daarna foto.</ p >
< button type = "button" class = "btn btn-sm btn-secondary" style = "margin-top:0.35rem"
x-show = "form.id && form.avatar_url" @ click = "removeAvatar()" > Foto verwijderen</ button >
</ div >
</ div >
< label class = "form-label" > Gebruikersnaam
< input class = "form-input" x-model = "form.username" :disabled = "!!form.id" placeholder = "bijv. sandra" autocomplete = "off" />
</ label >
< label class = "form-label" > Weergavenaam
< input class = "form-input" x-model = "form.display_name" placeholder = "Sandra" />
</ label >
< label class = "form-label" > Wachtwoord < span class = "muted" x-show = "form.id" > (leeg = ongewijzigd)</ span >
< input class = "form-input" type = "password" x-model = "form.password" autocomplete = "new-password" />
</ label >
< label class = "form-label" > Rol
< select class = "form-input" x-model = "form.role" >
< option value = "user" > User</ option >
< option value = "admin" > Admin</ option >
</ select >
</ label >
< label class = "form-check" x-show = "form.id" >
< input type = "checkbox" x-model = "form.is_active" /> Account actief
</ label >
< label class = "form-check" >
< input type = "checkbox" x-model = "form.manage_users" :disabled = "form.role === 'admin'" /> Mag gebruikers beheren
</ label >
< div x-show = "form.role !== 'admin'" style = "margin-top:0.75rem" >
< strong style = "font-size:0.85rem" > Pagina-rechten</ strong >
< div class = "perm-grid" style = "margin-top:0.5rem;grid-template-columns:1fr 1fr" >
< template x-for = "p in pages" :key = "p.key" >
< label class = "form-check" >
< input type = "checkbox" :value = "p.key" :checked = "form.pages.includes(p.key)"
@ change = "togglePage(p.key, $event.target.checked)" />
< span x-text = "p.label" ></ span >
</ label >
</ template >
</ div >
< div class = "btn-group" style = "margin-top:0.5rem" >
< button type = "button" class = "btn btn-sm btn-secondary" @ click = "selectAllPages()" > Alles</ button >
< button type = "button" class = "btn btn-sm btn-secondary" @ click = "form.pages=[]" > Geen</ button >
</ div >
</ div >
< p class = "page-tip" x-show = "form.role === 'admin'" > Admins hebben automatisch toegang tot alle pagina's en gebruikersbeheer.</ p >
< div class = "btn-group" style = "margin-top:1rem" >
< button type = "button" class = "btn btn-primary" @ click = "save()" :disabled = "saving" x-text = "saving ? 'Bezig…' : 'Opslaan'" ></ button >
< button type = "button" class = "btn btn-secondary" @ click = "formOpen=false" > Annuleer</ button >
</ div >
</ div >
</ div >
</ section >
2026-06-09 00:41:27 +00:00
{% elif active_tab == 'social' %}
< section class = "panel settings-panel" x-data = "socialSettingsPage()" x-init = "load()" >
< div class = "settings-panel-head" >
< div >
< h2 > Social API's</ h2 >
< p class = "page-tip" > Configureer later je API-sleutels per platform. Zonder keys worden posts in Automatisering overgeslagen (skip-modus).</ p >
</ div >
< a class = "btn btn-sm btn-pulse" href = "/marketing?tab=publish" > → Marketing Automatisering</ a >
</ div >
< div class = "social-platform-grid" >
< template x-for = "p in platforms" :key = "p.key" >
< div class = "perm-card" :class = "p.configured ? 'granted' : ''" >
< div class = "social-platform-head" >
< strong x-text = "p.label" ></ strong >
< span class = "badge" :class = "p.is_active && p.configured ? 'badge-positive' : 'badge-muted'"
x-text = "p.configured ? (p.is_active ? 'Actief' : 'Inactief') : 'Niet geconfigureerd'" ></ span >
</ div >
< p class = "page-tip" x-show = "p.last_test_status" x-text = "'Laatste test: ' + (p.last_test_status||'') + ' — ' + (p.last_test_message||'')" ></ p >
< template x-for = "field in p.fields" :key = "field.name" >
< label class = "form-label" x-text = "field.label" ></ label >
< input class = "form-input" :type = "field.secret ? 'password' : 'text'"
:placeholder = "field.placeholder"
x-model = "p.form[field.name]" />
< small x-show = "field.set && !p.form[field.name]" class = "muted" > Opgeslagen waarde blijft behouden</ small >
</ template >
< label class = "form-check" >
< input type = "checkbox" x-model = "p.form.is_active" /> Platform actief voor publicatie
</ label >
< div class = "btn-group" >
< button type = "button" class = "btn btn-primary btn-sm" @ click = "save(p)" :disabled = "p.saving" > Opslaan</ button >
< button type = "button" class = "btn btn-secondary btn-sm" @ click = "test(p)" :disabled = "p.saving || !p.configured" > Test</ button >
</ div >
</ div >
</ template >
</ div >
</ section >
2026-08-16 15:49:16 +00:00
{% elif active_tab == 'themes' %}
< section class = "panel settings-panel" x-data = "flkThemesPage()" x-init = "init()" >
< div class = "settings-panel-head" >
< div >
< h2 > UI Themes</ h2 >
< p class = "page-tip" > Kies een kleurrijk cyaan/blauw thema voor de hele cockpit. Direct toegepast op alle pagina's.</ p >
</ div >
</ div >
< div class = "flk-theme-grid" >
< template x-for = "t in themes" :key = "t.id" >
< button type = "button" class = "flk-theme-card" :class = "{ 'is-active': current === t.id }" @ click = "select(t.id)" >
< div class = "flk-theme-swatch" :style = "swatch(t.id)" ></ div >
< strong x-text = "t.name" ></ strong >
< small x-text = "t.desc" ></ small >
</ button >
</ template >
</ div >
</ section >
2026-06-09 00:41:27 +00:00
{% elif active_tab == 'general' %}
< section class = "panel settings-panel" >
< h2 > General</ h2 >
< p class = "page-tip" > Dashboard: < a href = "http://10.4.7.18:8600" > http://10.4.7.18:8600</ a > · Telegram: @klaploper_bot · Ochtend briefing: 07:00</ p >
< p class = "page-tip" > Meer instellingen (Telegram tijd, timezone) komen hier later.</ p >
</ section >
{% endif %}
</ div >
{% endblock %}
{% block scripts %}
2026-08-16 15:49:16 +00:00
< script >
function flkThemesPage () {
return {
themes : ( window . FlkThemes && window . FlkThemes . list ) || [],
current : 'ocean' ,
init () {
this . current = ( window . FlkThemes && window . FlkThemes . current ()) || 'ocean' ;
if ( window . FlkThemes ) window . FlkThemes . apply ( this . current );
},
select ( id ) {
this . current = window . FlkThemes . apply ( id );
if ( window . Cockpit ) Cockpit . toast ( 'Theme: ' + id , 'ok' );
},
swatch ( id ) {
var map = {
ocean : 'linear-gradient(135deg,#07131f,#123049 40%,#22d3ee)' ,
aurora : 'linear-gradient(135deg,#0a1020,#1a2550 45%,#a78bfa 75%,#22d3ee)' ,
neon : 'linear-gradient(135deg,#050b14,#10243f 40%,#06b6d4 70%,#e879f9)' ,
glacier : 'linear-gradient(135deg,#0b1522,#1a2f4a 45%,#38bdf8 80%,#7dd3fc)' ,
cobalt : 'linear-gradient(135deg,#08101c,#172642 40%,#3b82f6 70%,#22d3ee)'
};
return 'background:' + ( map [ id ] || map . ocean );
}
};
}
</ script >
2026-06-09 00:41:27 +00:00
< script >
function socialSettingsPage () {
const META = {
twitter : { label : 'X (Twitter)' , fields : [
{ name : 'api_key' , label : 'API Key' , secret : true },
{ name : 'api_secret' , label : 'API Secret' , secret : true },
{ name : 'access_token' , label : 'Access Token' , secret : true },
{ name : 'access_secret' , label : 'Access Secret' , secret : true },
]},
linkedin : { label : 'LinkedIn' , fields : [
{ name : 'access_token' , label : 'Access Token' , secret : true },
{ name : 'person_urn' , label : 'Person URN' , secret : false },
]},
instagram : { label : 'Instagram' , fields : [
{ name : 'access_token' , label : 'Access Token' , secret : true },
{ name : 'page_id' , label : 'Page / Business ID' , secret : false },
]},
facebook : { label : 'Facebook' , fields : [
{ name : 'access_token' , label : 'Access Token' , secret : true },
{ name : 'page_id' , label : 'Page ID' , secret : false },
]},
tiktok : { label : 'TikTok' , fields : [
{ name : 'access_token' , label : 'Access Token' , secret : true },
{ name : 'open_id' , label : 'Open ID' , secret : false },
]},
pinterest : { label : 'Pinterest' , fields : [
{ name : 'access_token' , label : 'Access Token' , secret : true },
{ name : 'board_id' , label : 'Board ID' , secret : false },
]},
};
return {
platforms : [],
async load () {
try {
const r = await Cockpit . api ( '/api/settings/social' );
const items = r . items || [];
const byKey = {};
items . forEach ( i => { byKey [ i . platform ] = i ; });
this . platforms = ( r . platforms || Object . keys ( META )). map ( key => {
const row = byKey [ key ] || {};
const cfg = row . config || {};
const meta = META [ key ] || { label : key , fields : [] };
const form = { is_active : !! row . is_active };
meta . fields . forEach ( f => {
form [ f . name ] = '' ;
f . set = !! cfg [ f . name + '_set' ];
f . placeholder = f . set ? '••••••••' : '' ;
});
const configured = meta . fields . every ( f => f . set || form [ f . name ]);
return {
key , label : meta . label , fields : meta . fields , form ,
is_active : !! row . is_active ,
configured : meta . fields . every ( f => cfg [ f . name + '_set' ]),
last_test_status : row . last_test_status ,
last_test_message : row . last_test_message ,
saving : false ,
};
});
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
},
async save ( p ) {
p . saving = true ;
try {
const body = { is_active : p . form . is_active };
p . fields . forEach ( f => {
if ( p . form [ f . name ]) body [ f . name ] = p . form [ f . name ];
});
await Cockpit . api ( '/api/settings/social/' + p . key , { method : 'PUT' , body : JSON . stringify ( body ) });
Cockpit . toast ( p . label + ' opgeslagen' , 'success' );
await this . load ();
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
p . saving = false ;
},
async test ( p ) {
p . saving = true ;
try {
const r = await Cockpit . api ( '/api/settings/social/' + p . key + '/test' , { method : 'POST' });
Cockpit . toast ( r . message || ( r . ok ? 'OK' : 'Mislukt' ), r . ok ? 'success' : 'error' );
await this . load ();
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
p . saving = false ;
},
};
}
2026-08-16 11:01:58 +00:00
function usersAdminPage () {
return {
loading : false ,
saving : false ,
users : [],
pages : [],
meId : null ,
formOpen : false ,
form : { id : null , username : '' , display_name : '' , password : '' , role : 'user' , is_active : true , manage_users : false , pages : [] },
async load () {
this . loading = true ;
try {
const me = await Cockpit . api ( '/api/auth/me' );
this . meId = me . user && me . user . id ;
const r = await Cockpit . api ( '/api/settings/users' );
this . users = r . users || [];
this . pages = r . pages || [];
} catch ( e ) {
Cockpit . toast (( e && e . message ) || 'Laden mislukt' , 'error' );
} finally {
this . loading = false ;
}
},
openCreate () {
this . form = { id : null , username : '' , display_name : '' , password : '' , role : 'user' , is_active : true , manage_users : false , pages : [ 'dashboard' ], avatar_url : null , avatarPreview : null , avatarFile : null };
this . formOpen = true ;
},
edit ( u ) {
this . form = {
id : u . id ,
username : u . username ,
display_name : u . display_name || '' ,
password : '' ,
role : u . role || 'user' ,
is_active : !! u . is_active ,
manage_users : !! ( u . permissions && u . permissions . manage_users ),
pages : ( u . permissions && u . permissions . pages ) ? [... u . permissions . pages ] : [],
avatar_url : u . avatar_url || null ,
avatarPreview : null ,
avatarFile : null ,
};
this . formOpen = true ;
},
initials ( u ) {
const n = (( u && ( u . display_name || u . username )) || '?' ). trim ();
const parts = n . split ( /\s+/ ). filter ( Boolean );
if ( parts . length >= 2 ) return ( parts [ 0 ][ 0 ] + parts [ 1 ][ 0 ]). toUpperCase ();
return n . slice ( 0 , 2 ). toUpperCase ();
},
formInitials () {
return this . initials ({ display_name : this . form . display_name , username : this . form . username });
},
onAvatarPick ( ev ) {
const file = ev . target . files && ev . target . files [ 0 ];
if ( ! file ) return ;
this . form . avatarFile = file ;
this . form . avatarPreview = URL . createObjectURL ( file );
},
async uploadAvatarFor ( userId , file ) {
const fd = new FormData ();
fd . append ( 'file' , file );
const res = await fetch ( '/api/settings/users/' + userId + '/avatar' , { method : 'POST' , body : fd , credentials : 'same-origin' });
const data = await res . json (). catch (() => ({}));
if ( ! res . ok ) throw new Error ( data . detail || 'Foto upload mislukt' );
return data . user ;
},
async removeAvatar () {
if ( ! this . form . id ) return ;
if ( ! confirm ( 'Profielfoto verwijderen?' )) return ;
try {
await Cockpit . api ( '/api/settings/users/' + this . form . id + '/avatar' , { method : 'DELETE' });
this . form . avatar_url = null ;
this . form . avatarPreview = null ;
this . form . avatarFile = null ;
Cockpit . toast ( 'Foto verwijderd' , 'success' );
await this . load ();
} catch ( e ) {
Cockpit . toast (( e && e . message ) || 'Mislukt' , 'error' );
}
},
togglePage ( key , on ) {
if ( on && ! this . form . pages . includes ( key )) this . form . pages . push ( key );
if ( ! on ) this . form . pages = this . form . pages . filter ( k => k !== key );
},
selectAllPages () { this . form . pages = this . pages . map ( p => p . key ); },
async save () {
this . saving = true ;
try {
const permissions = {
pages : this . form . role === 'admin' ? this . pages . map ( p => p . key ) : this . form . pages ,
manage_users : this . form . role === 'admin' ? true : !! this . form . manage_users ,
};
let saved = null ;
if ( this . form . id ) {
const body = {
display_name : this . form . display_name ,
role : this . form . role ,
is_active : this . form . is_active ,
permissions ,
};
if ( this . form . password ) body . password = this . form . password ;
saved = await Cockpit . api ( '/api/settings/users/' + this . form . id , { method : 'PUT' , body : JSON . stringify ( body ) });
saved = saved . user || saved ;
} else {
if ( ! this . form . username || ! this . form . password ) {
Cockpit . toast ( 'Gebruikersnaam en wachtwoord verplicht' , 'error' );
return ;
}
saved = await Cockpit . api ( '/api/settings/users' , {
method : 'POST' ,
body : JSON . stringify ({
username : this . form . username ,
password : this . form . password ,
display_name : this . form . display_name || this . form . username ,
role : this . form . role ,
permissions ,
}),
});
saved = saved . user || saved ;
}
if ( this . form . avatarFile && saved && saved . id ) {
await this . uploadAvatarFor ( saved . id , this . form . avatarFile );
}
this . formOpen = false ;
Cockpit . toast ( 'Gebruiker opgeslagen' , 'success' );
await this . load ();
} catch ( e ) {
Cockpit . toast (( e && e . message ) || 'Opslaan mislukt' , 'error' );
} finally {
this . saving = false ;
}
},
async remove ( u ) {
if ( ! confirm ( 'Verwijder gebruiker ' + u . username + '?' )) return ;
try {
await Cockpit . api ( '/api/settings/users/' + u . id , { method : 'DELETE' });
Cockpit . toast ( 'Verwijderd' , 'success' );
await this . load ();
} catch ( e ) {
Cockpit . toast (( e && e . message ) || 'Verwijderen mislukt' , 'error' );
}
},
};
}
2026-06-09 00:41:27 +00:00
function permissionsPage () {
return {
perms : [], grantedCount : 0 , total : 0 ,
async loadPerms () {
const r = await Cockpit . api ( '/api/settings/permissions' );
this . perms = r . items || [];
this . grantedCount = r . granted_count || 0 ;
this . total = r . total || 0 ;
},
async toggle ( key , granted ) {
await Cockpit . api ( '/api/settings/permissions/' + key , { method : 'PUT' , body : JSON . stringify ({ granted }) });
await this . loadPerms ();
},
async grantAll () {
const r = await Cockpit . api ( '/api/settings/permissions/grant-all' , { method : 'POST' });
Cockpit . toast ( r . message || 'Alle rechten toegekend' , 'success' );
await this . loadPerms ();
}
};
}
</ script >
{% if active_tab == 'email' %}
< script >
const PROVIDERS = {
gmail : { smtp_host : 'smtp.gmail.com' , smtp_port : 587 , imap_host : 'imap.gmail.com' , imap_port : 993 },
outlook : { smtp_host : 'smtp.office365.com' , smtp_port : 587 , imap_host : 'outlook.office365.com' , imap_port : 993 },
zoho : { smtp_host : 'smtp.zoho.eu' , smtp_port : 587 , imap_host : 'imap.zoho.eu' , imap_port : 993 },
custom : { smtp_host : '' , smtp_port : 587 , imap_host : '' , imap_port : 993 },
};
function settingsPage () {
return {
accounts : [],
activeAccount : null ,
loading : true ,
formOpen : false ,
saving : false ,
form : defaultForm (),
async load () {
this . loading = true ;
try {
const r = await Cockpit . api ( '/api/settings/email' );
this . accounts = r . accounts || [];
this . activeAccount = this . accounts . find ( a => a . is_active ) || null ;
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
this . loading = false ;
},
defaultForm () { return defaultForm (); },
openForm () {
this . form = defaultForm ();
this . formOpen = true ;
},
edit ( acc ) {
this . form = Object . assign ( defaultForm (), acc , { smtp_password : '' , imap_password : '' });
this . formOpen = true ;
},
applyProvider () {
const p = PROVIDERS [ this . form . provider ] || PROVIDERS . custom ;
this . form . smtp_host = p . smtp_host ;
this . form . smtp_port = p . smtp_port ;
this . form . imap_host = p . imap_host ;
this . form . imap_port = p . imap_port ;
if ( this . form . email_address && ! this . form . smtp_user ) {
this . form . smtp_user = this . form . email_address ;
this . form . imap_user = this . form . email_address ;
}
},
bodyFromForm () {
const b = {
label : this . form . label ,
email_address : this . form . email_address ,
provider : this . form . provider ,
is_active : this . form . is_active ,
smtp_host : this . form . smtp_host ,
smtp_port : this . form . smtp_port ,
smtp_user : this . form . smtp_user ,
imap_host : this . form . imap_host ,
imap_port : this . form . imap_port ,
imap_user : this . form . imap_user ,
sync_enabled : this . form . sync_enabled ,
};
if ( this . form . smtp_password ) b . smtp_password = this . form . smtp_password ;
if ( this . form . imap_password ) b . imap_password = this . form . imap_password ;
return b ;
},
async save () {
this . saving = true ;
try {
const body = this . bodyFromForm ();
if ( this . form . id ) {
await Cockpit . api ( '/api/settings/email/' + this . form . id , { method : 'PUT' , body : JSON . stringify ( body ) });
} else {
await Cockpit . api ( '/api/settings/email' , { method : 'POST' , body : JSON . stringify ( body ) });
}
Cockpit . toast ( 'Opgeslagen' , 'success' );
this . formOpen = false ;
await this . load ();
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
this . saving = false ;
},
async activate ( id ) {
try {
await Cockpit . api ( '/api/settings/email/' + id + '/activate' , { method : 'POST' });
Cockpit . toast ( 'Actief account gewijzigd' , 'success' );
await this . load ();
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
},
async testAccount ( id ) {
try {
Cockpit . toast ( 'SMTP test…' , 'info' );
const r = await Cockpit . api ( '/api/settings/email/' + id + '/test' , { method : 'POST' });
Cockpit . toast ( r . message || 'SMTP OK' , r . ok ? 'success' : 'error' );
await this . load ();
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
},
async testForm () {
this . saving = true ;
try {
const r = await Cockpit . api ( '/api/settings/email/test' , {
method : 'POST' ,
body : JSON . stringify ( this . bodyFromForm ()),
});
Cockpit . toast ( r . message || 'SMTP OK' , r . ok ? 'success' : 'error' );
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
this . saving = false ;
},
async remove ( id ) {
if ( ! confirm ( 'Email account verwijderen?' )) return ;
try {
await Cockpit . api ( '/api/settings/email/' + id , { method : 'DELETE' });
Cockpit . toast ( 'Verwijderd' , 'success' );
await this . load ();
} catch ( e ) { Cockpit . toast ( e . message , 'error' ); }
},
};
}
function defaultForm () {
return {
id : null , label : '' , email_address : '' , provider : 'gmail' , is_active : true ,
smtp_host : 'smtp.gmail.com' , smtp_port : 587 , smtp_user : '' , smtp_password : '' , smtp_password_set : false ,
imap_host : 'imap.gmail.com' , imap_port : 993 , imap_user : '' , imap_password : '' , imap_password_set : false ,
sync_enabled : false ,
};
}
</ script >
{% endif %}
{% endblock %}