Files
flowdeck/app/templates/settings.html
T
bruno efa3d57e93
FlowDeck CI / lint (push) Successful in 2m15s
FlowDeck CI / test (push) Failing after 23m33s
FlowDeck CI / docker (push) Skipped
feat(mobile): side-nav settings + scroll drawer + aide refondue settings-style (v7.59.0)
- Sidebar mobile: display:block du breakpoint ≤768px cassait flex:1 de
  .sidebar-scroll → molette/touch incapables de scroller le drawer. Flex
  restauré. Gate: wheel scroll scrollTop > 0.
- Settings mobile: nav horizontale remplacée par une side-navigation gauche
  drawer (hamburger .settings-menu-btn + backdrop, fermeture auto au choix).
  Squelette panneau extrait vers static/css/settings.css (partagé).
- /help refaite au complet: template help.html (13 sections couvrant toute
  l'app — éditeur, databases/vues, tâches, workspaces, collab, agent IA,
  API v2, PWA, SSO, raccourcis, tips) avec le même panneau 2 colonnes +
  drawer mobile; route réduite à un render; help.js (Alpine.data helpInit).

E2E mobile_regression 6/6 + smoke 2/2 · pytest app+pwa 230/230
2026-10-07 15:36:05 -04:00

1317 lines
84 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block page_title %}Settings{% endblock %}
{% block title_prefix %}Settings{% endblock %}
{% block page_icon %}{{ fd_icon("settings",18) }}{% endblock %}
{% block topbar %}
{% set page_icon = "settings" %}
{% set page_title = "Settings" %}
{% include '_header.html' %}
{% endblock %}
{% block content %}
<style>
.setting-group{margin-bottom:28px;}
.setting-group h3{font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim);margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid var(--border);}
.setting-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--border);}
.setting-row:last-child{border-bottom:none;}
.setting-label{font-size:14px;color:var(--text);}
.setting-desc{font-size:12px;color:var(--text-dim);margin-top:2px;}
.setting-control{flex-shrink:0;margin-left:16px;}
/* Avatar */
.avatar-section{display:flex;align-items:center;gap:16px;padding:12px 0;}
.avatar-preview{width:64px;height:64px;border-radius:50%;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;font-size:28px;font-weight:700;color:var(--text);overflow:hidden;flex-shrink:0;}
.avatar-preview img{width:100%;height:100%;object-fit:cover;}
.avatar-upload-label{cursor:pointer;font-size:13px;color:var(--accent);padding:6px 12px;border:1px solid var(--accent);border-radius:6px;transition:all 150ms;}
.avatar-upload-label:hover{background:rgba(35,131,226,.1);}
.avatar-upload-input{display:none;}
/* Avatar color swatches */
.avatar-color-swatch{width:40px;height:40px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:700;color:var(--text-primary);cursor:pointer;border:2px solid transparent;transition:all 120ms;}
.avatar-color-swatch:hover{transform:scale(1.1);}
.avatar-color-swatch.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(35,131,226,.4);}
/* Tags */
.tag-mgmt-item{display:flex;align-items:center;gap:10px;padding:8px 10px;background:var(--bg-tertiary);border-radius:8px;margin-bottom:4px;}
.tag-mgmt-item .tag-color-dot{width:14px;height:14px;border-radius:50%;flex-shrink:0;}
.tag-name{flex:1;font-size:13px;color:var(--text);}
.tag-count{font-size:12px;color:var(--text-dim);}
.new-tag-form{display:flex;align-items:center;gap:10px;margin-top:8px;padding:8px 10px;background:var(--bg-tertiary);border:1px dashed var(--border);border-radius:8px;}
.new-tag-form input{flex:1;background:none;border:none;color:var(--text);font-size:13px;outline:none;}
.color-swatch{width:22px;height:22px;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:all 120ms;}
.color-swatch:hover{transform:scale(1.15);}
.color-swatch.selected{border-color:var(--text);box-shadow:0 0 0 2px var(--accent);}
/* Admin tables */
.table-wrap{overflow-x:auto;border-radius:8px;border:1px solid var(--border);}
.admin-table{width:100%;border-collapse:collapse;font-size:13px;}
.admin-table th{background:var(--bg-tertiary);color:var(--text-dim);font-weight:500;padding:8px 12px;text-align:left;font-size:11px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;}
.admin-table td{padding:8px 12px;border-top:1px solid var(--border);color:var(--text);}
.admin-table tr:hover td{background:var(--bg-secondary);}
.stat-card{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:14px;text-align:center;}
.stat-value{font-size:22px;font-weight:700;color:var(--accent);}
.stat-label{font-size:11px;color:var(--text-dim);margin-top:2px;text-transform:uppercase;letter-spacing:.5px;}
.role-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:500;}
.role-admin{background:rgba(35,131,226,.2);color:var(--accent);}
.role-user{background:rgba(120,119,116,.2);color:var(--text-dim);}
.status-dot{display:inline-block;width:6px;height:6px;border-radius:50%;margin-right:5px;vertical-align:middle;}
.status-dot.active{background:var(--toast-success-bg);}
.status-dot.inactive{background:var(--danger);}
/* Close button — shared styles in /static/css/settings.css */
/* Agent & IA — status summary + provider cards */
.llm-summary{display:flex;gap:14px;align-items:flex-start;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:12px;padding:14px 16px;margin-bottom:22px;flex-wrap:wrap;}
.llm-summary-icon{font-size:26px;line-height:1;flex-shrink:0;}
.llm-summary-body{flex:1;min-width:220px;}
.llm-summary-title{font-size:14px;font-weight:600;color:var(--text);}
.llm-summary-desc{font-size:12.5px;color:var(--text-dim);margin-top:2px;}
.llm-summary-side{display:flex;align-items:center;gap:8px;flex-shrink:0;}
.llm-badge{display:inline-flex;align-items:center;gap:5px;font-size:11px;font-weight:600;padding:3px 10px;border-radius:99px;white-space:nowrap;}
.llm-badge.ok{background:rgba(46,160,67,.15);color:#2ea043;}
.llm-badge.warn{background:rgba(217,115,13,.16);color:#d9730d;}
.llm-badge.err{background:rgba(224,62,62,.14);color:#e03e3e;}
.llm-badge.off{background:rgba(120,119,116,.15);color:var(--text-dim);}
.prov-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:12px;}
.prov-card{border:1px solid var(--border);border-radius:12px;padding:12px 14px;background:var(--bg-secondary);display:flex;flex-direction:column;gap:4px;}
.prov-card-head{display:flex;align-items:center;gap:8px;margin-bottom:2px;flex-wrap:wrap;}
.prov-card-name{font-size:13.5px;font-weight:600;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;}
.prov-card-sub{font-size:11.5px;color:var(--text-dim);margin-bottom:8px;line-height:1.4;}
.prov-field-row{display:flex;flex-direction:column;gap:4px;margin-top:2px;}
.prov-field-label{font-size:12px;color:var(--text-secondary);display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
.prov-field-label .muted{font-size:10.5px;color:var(--text-dim);font-weight:400;}
.prov-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;}
.prov-card input.settings-input,.prov-card select.settings-input{width:100%;box-sizing:border-box;}
.prov-card .pw-wrapper{width:100%;}
.prov-card .pw-wrapper .settings-input{padding-right:36px;}
.prov-msg{font-size:12px;margin-top:8px;min-height:0;word-break:break-word;}
.llm-hint{font-size:12px;color:var(--text-secondary);background:rgba(35,131,226,.07);border:1px solid rgba(35,131,226,.18);border-radius:8px;padding:8px 12px;margin-top:14px;line-height:1.5;}
/* Agent & IA — provider master/detail (scales to dozens of providers) */
.llm-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap;}
.llm-search{position:relative;flex:1;min-width:180px;}
.llm-search svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--text-dim);pointer-events:none;}
.llm-search input{padding-left:32px;box-sizing:border-box;}
.llm-count{font-size:11px;color:var(--text-dim);white-space:nowrap;text-transform:uppercase;letter-spacing:.4px;}
.llm-layout{display:grid;grid-template-columns:264px 1fr;gap:14px;align-items:start;}
.llm-list{border:1px solid var(--border);border-radius:12px;background:var(--bg-secondary);max-height:480px;overflow-y:auto;padding:6px;display:flex;flex-direction:column;gap:2px;}
.llm-list-item{display:flex;align-items:center;gap:9px;padding:8px 10px;border-radius:8px;cursor:pointer;transition:background 100ms;border:1px solid transparent;}
.llm-list-item:hover{background:var(--bg-hover);}
.llm-list-item.active{background:rgba(35,131,226,.14);border-color:rgba(35,131,226,.3);}
.llm-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;background:var(--text-dim);}
.llm-dot.ok{background:#2ea043;}
.llm-dot.warn{background:#d9730d;}
.llm-dot.err{background:#e03e3e;}
.llm-list-name{flex:1;min-width:0;font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.llm-list-status{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.4px;white-space:nowrap;}
.llm-list-empty{padding:12px;font-size:12px;color:var(--text-dim);text-align:center;}
.llm-detail{border:1px solid var(--border);border-radius:12px;background:var(--bg-secondary);padding:16px;min-height:260px;}
.llm-detail-head{display:flex;align-items:center;gap:10px;margin-bottom:4px;flex-wrap:wrap;}
.llm-detail-name{font-size:15px;font-weight:600;color:var(--text);}
.llm-detail-base{font-size:11px;color:var(--text-dim);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all;margin-bottom:12px;}
.llm-detail .prov-field-row{margin-top:12px;}
.llm-detail-empty{display:flex;align-items:center;justify-content:center;height:260px;font-size:13px;color:var(--text-dim);text-align:center;padding:0 20px;}
@media (max-width:760px){.llm-layout{grid-template-columns:1fr;}.llm-list{max-height:220px;}}
/* ── Mobile side-nav drawer: shared settings.css handles overlay/nav/panel ── */
</style>
<div class="settings-overlay" x-data="settingsInit()">
<div class="settings-panel" style="position:relative;" @keydown.escape="historyBack()">
<button class="settings-menu-btn" @click="navOpen = !navOpen" title="Sections" aria-label="Open sections menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<button class="settings-close" @click="closeSettings()" title="Close">×</button>
<!-- Left navigation (mobile: side-navigation drawer via hamburger + backdrop) -->
<div class="settings-nav-backdrop" x-show="navOpen" x-cloak @click="navOpen = false"></div>
<div class="settings-nav" :class="{ 'nav-open': navOpen }" @click.capture="closeNavOnMobile()">
<div class="settings-nav-header">Account</div>
<div class="settings-nav-item" :class="{ active: activeSection==='account' }" @click="activeSection='account'"><span class="nav-icon-inline">{{ fd_icon("user",14) }}</span> My account</div>
<div class="settings-nav-item" :class="{ active: activeSection==='notifications' }" @click="activeSection='notifications'"><span class="nav-icon-inline">{{ fd_icon("bell",14) }}</span> Notifications</div>
<div class="settings-nav-item" :class="{ active: activeSection==='api-tokens' }" @click="navTo('api-tokens')"><span class="nav-icon-inline">{{ fd_icon("key",14) }}</span> API tokens</div>
<div class="settings-nav-item" :class="{ active: activeSection==='sessions' }" @click="navTo('sessions')"><span class="nav-icon-inline">{{ fd_icon("users",14) }}</span> Sessions</div>
{% if plugin_enabled('web-clipper') %}<div class="settings-nav-item" :class="{ active: activeSection==='extensions' }" @click="navTo('extensions')"><span class="nav-icon-inline">{{ fd_icon("zap",14) }}</span> Extensions</div>{% endif %}
<div class="settings-nav-header">Workspace</div>
<div class="settings-nav-item" :class="{ active: activeSection==='workspace' }" @click="activeSection='workspace'">{{ fd_icon("file",14) }} General</div>
<div class="settings-nav-item" :class="{ active: activeSection==='tags' }" @click="activeSection='tags'">{{ fd_icon("tag",14) }} Tags</div>
<div class="settings-nav-header">Features</div>
<div class="settings-nav-item" :class="{ active: activeSection==='features' }" @click="activeSection='features'">{{ fd_icon("link",14) }} Integrations</div>
{% if plugin_enabled('automations') %}<div class="settings-nav-item" :class="{ active: activeSection==='automations' }" @click="navTo('automations')">{{ fd_icon("zap",14) }} Automations</div>{% endif %}
<div class="settings-nav-item" :class="{ active: activeSection==='llm' }" @click="activeSection='llm'">{{ fd_icon("bot",14) }} Agent &amp; IA</div>
<!-- Admin nav: only visible to admins -->
<template x-if="userIsAdmin">
<div>
<div class="settings-nav-header">Admin</div>
<div class="settings-nav-item" :class="{ active: activeSection==='admin-users' }" @click="navTo('admin-users')">{{ fd_icon("users",14) }} Users &amp; Roles</div>
<div class="settings-nav-item" :class="{ active: activeSection==='admin-audit' }" @click="navTo('admin-audit')">{{ fd_icon("file-text",14) }} Audit Log</div>
<div class="settings-nav-item" :class="{ active: activeSection==='admin-backups' }" @click="navTo('admin-backups')">{{ fd_icon("download",14) }} Backups</div>
<div class="settings-nav-item" :class="{ active: activeSection==='admin-sso' }" @click="navTo('admin-sso')">{{ fd_icon("lock",14) }} SSO / Enterprise</div>
</div>
</template>
</div>
<!-- Right content -->
<div class="settings-content">
<!-- Account -->
<div x-show="activeSection==='account'">
<h2>My account</h2>
<p class="section-desc">Manage your profile and preferences.</p>
<div class="setting-group">
<h3>Profile</h3>
<div class="avatar-section">
<div class="avatar-preview" :style="{background: avatarColor || '#3A3A3A'}">
<img x-show="avatarUrl" :src="avatarUrl" alt="Avatar">
<span x-show="!avatarUrl">{{ user.get('full_name', user.get('login',''))[:1].upper() }}</span>
</div>
<div>
<label class="avatar-upload-label" style="margin-bottom:4px;">
Upload photo
<input type="file" class="avatar-upload-input" accept="image/*" @change="uploadAvatar($event)">
</label>
<div style="font-size:11px;color:var(--text-dim);margin-top:8px;">JPG, PNG or GIF — max 2MB</div>
</div>
</div>
<div style="margin-top:8px;">
<div style="font-size:12px;color:var(--text-dim);margin-bottom:6px;">Or pick a color:</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<template x-for="c in avatarColors" :key="c">
<div class="avatar-color-swatch" :style="{background: c}"
:class="{ selected: avatarColor === c }"
@click="selectAvatarColor(c)">
<span>{{ user.get('full_name', user.get('login',''))[:1].upper() }}</span>
</div>
</template>
</div>
</div>
</div>
<!-- Editable fields -->
<div class="setting-group">
<h3>Account details</h3>
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
<div class="setting-label">Username</div>
<input type="text" class="settings-input" x-model="accountForm.login" style="max-width:300px;">
</div>
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
<div class="setting-label">Full name</div>
<input type="text" class="settings-input" x-model="accountForm.full_name" style="max-width:300px;">
</div>
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
<div class="setting-label">Email</div>
<input type="email" class="settings-input" x-model="accountForm.email" style="max-width:300px;">
</div>
<div style="margin-top:12px;">
<button class="btn btn-primary" @click="saveAccount()" :disabled="accountSaving" style="width:auto;padding:8px 20px;font-size:13px;">
<span x-show="!accountSaving">{{ fd_icon("save",14) }} Save changes</span>
<span x-show="accountSaving">Saving…</span>
</button>
<span x-show="accountMsg" x-text="accountMsg" style="margin-left:12px;font-size:13px;color:var(--toast-success-bg);"></span>
</div>
</div>
<!-- Password change -->
<div class="setting-group">
<h3>Change password</h3>
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:4px;">
<div class="setting-label">New password</div>
<div class="pw-wrapper" style="max-width:300px;">
<input :type="pwVisible ? 'text' : 'password'" class="settings-input" x-model="accountForm.password" placeholder="Leave blank to keep current" style="padding-right:36px;">
<button type="button" class="pw-toggle" @click="pwVisible=!pwVisible" style="top:50%;">
<span x-show="pwVisible" x-cloak>{{ fd_icon('eye-off',14) }}</span>
<span x-show="!pwVisible" x-cloak>{{ fd_icon('eye',14) }}</span>
</button>
</div>
</div>
<div style="margin-top:12px;">
<button class="btn btn-primary" @click="saveAccount()" :disabled="accountSaving || !accountForm.password" style="width:auto;padding:8px 20px;font-size:13px;">
<span x-show="!accountSaving">{{ fd_icon("lock",14) }} Update password</span>
<span x-show="accountSaving">Saving…</span>
</button>
</div>
</div>
<!-- Preferences -->
<div class="setting-group">
<h3>Preferences</h3>
<div class="setting-row">
<div>
<div class="setting-label">Theme</div>
<div class="setting-desc">Choose between light and dark appearance</div>
</div>
<div class="setting-control">
<select x-model="theme" @change="applyTheme()" style="background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;padding:6px 10px;outline:none;">
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</div>
</div>
<div class="setting-row">
<div>
<div class="setting-label">Default view</div>
<div class="setting-desc">Workspace view mode when opening a workspace</div>
</div>
<div class="setting-control">
<select x-model="defaultView" @change="saveDefaultView()" style="background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:13px;padding:6px 10px;outline:none;">
<option value="tree">{{ fd_icon("folder",14) }} Tree</option>
<option value="list">{{ fd_icon("list",14) }} List</option>
<option value="details">{{ fd_icon("bar-chart",14) }} Details</option>
<option value="title">{{ fd_icon("grid",14) }} Title</option>
<option value="content">{{ fd_icon("align-left",14) }} Content</option>
</select>
</div>
</div>
</div>
</div>
<!-- Notifications -->
<div x-show="activeSection==='notifications'">
<h2>Notifications</h2>
<p class="section-desc">Choose when and how you want to be notified.</p>
<div class="setting-group">
<h3>Email notifications</h3>
<div class="setting-row">
<div>
<div class="setting-label">Comments</div>
<div class="setting-desc">When someone comments on your pages</div>
</div>
<div class="setting-control">
<label class="toggle-switch sm">
<input type="checkbox" x-model="notifPrefs.comments" @change="saveNotifPrefs()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-row">
<div>
<div class="setting-label">Mentions</div>
<div class="setting-desc">When someone @mentions you</div>
</div>
<div class="setting-control">
<label class="toggle-switch sm">
<input type="checkbox" x-model="notifPrefs.mentions" @change="saveNotifPrefs()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-row">
<div>
<div class="setting-label">Reminders</div>
<div class="setting-desc">Upcoming events &amp; date reminders from your databases</div>
</div>
<div class="setting-control">
<label class="toggle-switch sm">
<input type="checkbox" x-model="notifPrefs.reminders" @change="saveNotifPrefs()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="setting-row">
<div>
<div class="setting-label">Assignments</div>
<div class="setting-desc">When a database row is assigned to you</div>
</div>
<div class="setting-control">
<label class="toggle-switch sm">
<input type="checkbox" x-model="notifPrefs.assignments" @change="saveNotifPrefs()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="setting-group">
<h3>Timezone</h3>
<div class="setting-row">
<div>
<div class="setting-label">Your timezone</div>
<div class="setting-desc">Used by calendar views (today, week start) and reminder firing. Empty = UTC.</div>
</div>
<div class="setting-control">
<select class="settings-input" style="max-width:260px" x-model="userTimezone" @change="saveTimezone()">
<option value="">UTC</option>
<template x-for="z in userZones" :key="z">
<option :value="z" x-text="z"></option>
</template>
</select>
</div>
</div>
</div>
</div>
<!-- API tokens -->
<div x-show="activeSection==='api-tokens'">
<h2>API tokens</h2>
<p class="section-desc">Generate personal access tokens for the public API (<code>/api/v1</code>) — e.g. for scripts and external tools.</p>
<div class="setting-group">
<h3>New token</h3>
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:6px;">
<div class="setting-label">Name</div>
<div style="display:flex;gap:8px;">
<input type="text" class="settings-input" x-model="tokenName" placeholder="Ex : CI script, monitoring…" style="flex:1;" @keydown.enter="createApiToken()">
<button class="btn btn-primary" @click="createApiToken()" :disabled="!tokenName.trim()" style="white-space:nowrap;">Generate</button>
</div>
</div>
<template x-if="newToken">
<div class="setting-row" style="flex-direction:column;align-items:stretch;gap:6px;background:rgba(46,160,67,.08);border-radius:8px;">
<div class="setting-label" style="color:var(--toast-success-bg);">Token créé — copiez-le maintenant (affiché une seule fois)</div>
<div style="display:flex;gap:8px;align-items:center;">
<code class="settings-input" x-text="newToken.token" style="flex:1;font-family:var(--font-mono);font-size:12px;user-select:all;"></code>
<button class="btn-sm" @click="copyNewToken()">Copy</button>
</div>
</div>
</template>
</div>
<div class="setting-group">
<h3>Tokens existants <span style="text-transform:none;font-size:11px;" x-text="'(' + apiTokens.length + ')'"></span></h3>
<div x-show="!apiTokens.length" class="setting-desc" style="padding:8px 0;">Aucun token pour l'instant.</div>
<template x-for="t in apiTokens" :key="t.id">
<div class="setting-row">
<div style="flex:1;min-width:0;">
<div class="setting-label">
<span x-text="t.name"></span>
<code style="font-size:11px;color:var(--text-dim);margin-left:8px;" x-text="'fd_' + (t.token_prefix||'').slice(0,12)"></code>
</div>
<div class="setting-desc" style="font-size:11px;">
<span x-text="'Créé le ' + (t.created_at||'').slice(0,16)"></span>
<template x-if="t.last_used_at"><span x-text="' · utilisé le ' + t.last_used_at.slice(0,16)"></span></template>
</div>
</div>
<div class="setting-control" style="display:flex;gap:6px;align-items:center;">
<span class="llm-badge" :class="t.revoked ? 'off' : 'ok'" x-text="t.revoked ? 'Révoqué' : 'Actif'"></span>
<button class="btn-sm" :style="{color:'var(--danger)'}" @click="revokeApiToken(t)" x-show="!t.revoked">Révoquer</button>
</div>
</div>
</template>
</div>
</div>
<!-- Sessions -->
<div x-show="activeSection==='sessions'">
<h2>Active sessions</h2>
<p class="section-desc">Appareils actuellement connectés à votre compte. Révoquez une session pour la déconnecter immédiatement.</p>
<div class="setting-group">
<div x-show="!sessions.length" class="setting-desc" style="padding:8px 0;">Aucune session enregistrée.</div>
<template x-for="s in sessions" :key="s.id">
<div class="setting-row">
<div style="flex:1;min-width:0;">
<div class="setting-label">
<span x-text="s.is_current ? 'Cet appareil' : 'Autre appareil'"></span>
<span class="llm-badge" :class="s.revoked ? 'off' : 'ok'" style="margin-left:8px;" x-text="s.revoked ? 'Révoquée' : (s.is_current ? 'Active' : 'Active')"></span>
</div>
<div class="setting-desc" style="font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:520px;">
<span x-text="s.user_agent || 'Agent inconnu'"></span>
</div>
<div class="setting-desc" style="font-size:11px;">
<span x-text="'IP: ' + (s.ip_address || '—')"></span>
<span x-text="' · ' + (s.last_seen_at||'').slice(0,16)"></span>
</div>
</div>
<div class="setting-control">
<button class="btn-sm" :style="{color:'var(--danger)'}" @click="revokeSession(s)" x-show="!s.revoked" x-text="s.is_current ? 'Se déconnecter' : 'Révoquer'"></button>
</div>
</div>
</template>
</div>
</div>
<!-- Extensions (Web Clipper) -->
<div x-show="{{ 'true' if plugin_enabled('web-clipper') else 'false' }} &amp;&amp; activeSection==='extensions'">
<h2>Extensions</h2>
<p class="section-desc">FlowDeck Web Clipper — capture web content directly into FlowDeck. Manage connected browsers.</p>
<div class="setting-group">
<h3>Web Clipper</h3>
<p style="font-size:12px;color:var(--text-dim);line-height:1.5;margin-bottom:12px;">
Install the <a href="/extensions" style="color:var(--accent);">browser extension</a> (Chrome/Firefox/Edge). After install, use <b>Verify &amp; Connect</b> once or paste an <b>API token</b> in the extension popup.
</p>
<div style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap;">
<a href="/extensions" class="btn btn-secondary" style="text-decoration:none;font-size:13px;">Get extension</a>
<a href="/static/extension/flowdeck-clipper.zip" download class="btn btn-secondary" style="text-decoration:none;font-size:13px;">Download .zip</a>
</div>
<div x-show="!clipperDevices.length" class="setting-desc" style="padding:8px 0;">No extension connected yet — clip your first page to appear here.</div>
<template x-for="d in clipperDevices" :key="d.id">
<div class="setting-row">
<div style="flex:1;min-width:0;">
<div class="setting-label">
<span x-text="(d.device_name || d.extension_name || 'Clipper') + ' — ' + d.device_id.slice(0,24)"></span>
<span class="llm-badge" :class="d.revoked ? 'off' : 'ok'" style="margin-left:8px;" x-text="d.revoked ? 'Révoqué' : 'Actif'"></span>
<span class="llm-badge warn" style="margin-left:6px;" x-text="d.extension_name"></span>
</div>
<div class="setting-desc" style="font-size:11px;">
<span x-text="'Connecté: ' + (d.created_at||'').slice(0,16)"></span>
<span x-show="d.last_used_at" x-text="' · Dernier: ' + d.last_used_at.slice(0,16)"></span>
<span x-text="' · ' + d.clips_count + ' clipp(s)'"></span>
<span x-show="d.last_clip_at" x-text="' · Dernier clip: ' + d.last_clip_at.slice(0,16)"></span>
</div>
</div>
<div class="setting-control">
<button class="btn-sm" :style="{color:'var(--danger)'}" @click="revokeClipperDevice(d)" x-show="!d.revoked">Révoquer</button>
</div>
</div>
</template>
</div>
<div class="setting-group">
<h3>Tester le clipper (sans extension)</h3>
<p style="font-size:12px;color:var(--text-dim);">Envoyez un clip тест direct via l'API :</p>
<div style="display:flex;gap:8px;margin-top:8px;">
<input class="settings-input" x-model="clipTestUrl" placeholder="https://example.com/article" style="flex:1;">
<button class="btn btn-primary" @click="testClip()" :disabled="clipTesting" x-text="clipTesting ? 'Envoi…' : 'Clip URL'"></button>
</div>
<div x-show="clipTestMsg" style="margin-top:8px;font-size:12px;" :style="{color: clipTestOk ? 'var(--toast-success-bg)' : 'var(--danger)'}" x-text="clipTestMsg"></div>
</div>
</div>
<!-- Workspace -->
<div x-show="activeSection==='workspace'">
<h2>Workspace</h2>
<p class="section-desc">General workspace settings.</p>
<div class="setting-group">
<h3>General</h3>
<div class="setting-row">
<div>
<div class="setting-label">Workspace name</div>
<div class="setting-desc">{{ workspace_name }}</div>
</div>
</div>
</div>
</div>
<!-- Tags -->
<div x-show="activeSection==='tags'">
<h2>Tags</h2>
<p class="section-desc">Manage all tags across your workspace.</p>
<div class="setting-group">
<h3>All tags</h3>
<template x-for="tag in allTags" :key="tag.id">
<div class="tag-mgmt-item">
<div class="tag-color-dot" :style="{background: tag.color}"></div>
<span class="tag-name"
x-text="tag.name"
@dblclick="renameTag(tag)"
title="Double-click to rename"
style="cursor:pointer;"></span>
<span class="tag-count" x-text="tag.count + ' items'"></span>
<input type="color" :value="tag.color" @input="updateTagColor(tag.id, $event.target.value)" style="width:24px;height:24px;border:none;cursor:pointer;background:none;padding:0;" title="Change color">
<button class="tag-chip rm-btn" @click="deletingTag=tag" title="Delete">×</button>
</div>
</template>
<div class="new-tag-form">
<template x-for="c in presetColors" :key="c">
<div class="color-swatch" :class="{ selected: newTagColor === c }" :style="{background: c}" @click="newTagColor=c"></div>
</template>
<input type="text" placeholder="New tag..." x-model="newTagName" @keydown.enter="createTag()">
<button class="btn btn-primary" @click="createTag()" :disabled="!newTagName.trim()" style="padding:4px 12px;font-size:12px;">Add</button>
</div>
</div>
<!-- Delete confirmation modal -->
<div class="modal-overlay" x-show="deletingTag" @click.self="deletingTag=null" @keydown.escape="deletingTag=null" style="z-index:1000;">
<div class="modal-box" style="max-width:360px;">
<h3 style="margin:0 0 8px;">Delete tag</h3>
<p style="color:var(--text-dim);margin:0 0 16px;">
Are you sure you want to delete the tag "<strong x-text="deletingTag ? deletingTag.name : ''"></strong>"?
<span x-show="deletingTag && deletingTag.count > 0" style="color:var(--danger);display:block;margin-top:4px;">
{{ fd_icon('alert-triangle',14) }} This tag is used on <span x-text="deletingTag ? deletingTag.count : 0"></span> item(s).
</span>
</p>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button class="btn" @click="deletingTag=null">Cancel</button>
<button class="btn btn-danger" @click="confirmDeleteTag()">Delete</button>
</div>
</div>
</div>
<!-- Rename modal -->
<div class="modal-overlay" x-show="renamingTag" @click.self="renamingTag=null" @keydown.escape="renamingTag=null" style="z-index:1000;">
<div class="modal-box" style="max-width:360px;">
<h3 style="margin:0 0 12px;">Rename tag</h3>
<div style="display:flex;gap:8px;align-items:center;">
<div class="tag-color-dot" :style="{background: renamingTag ? renamingTag.color : ''}" style="width:16px;height:16px;"></div>
<input type="text" class="settings-input" x-model="renameValue"
@keydown.enter="confirmRenameTag()" @keydown.escape="renamingTag=null"
style="flex:1;" autofocus>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;">
<button class="btn" @click="renamingTag=null">Cancel</button>
<button class="btn btn-primary" @click="confirmRenameTag()" :disabled="!renameValue.trim()">Rename</button>
</div>
</div>
</div>
</div>
<!-- Features -->
<div x-show="activeSection==='features'">
<h2>Features</h2>
<p class="section-desc">Integrations and feature toggles.</p>
<div class="setting-group">
<h3>Integrations</h3>
<!-- Gitea -->
<div class="setting-row">
<div>
<div class="setting-label">Gitea <span x-show="authMethod==='gitea'" style="font-size:10px;background:var(--accent);color:#fff;padding:1px 6px;border-radius:3px;margin-left:6px;">Primary</span></div>
<div class="setting-desc">
<span x-show="giteaLinked">Connected to git.dracodev.net</span>
<span x-show="!giteaLinked">Connect your Gitea account to access projects</span>
</div>
</div>
<div class="setting-control">
<span x-show="giteaLinked" style="font-size:12px;color:var(--toast-success-bg);">✅ Active</span>
<button x-show="giteaLinked && authMethod!=='gitea'" class="btn-sm" style="color:var(--danger);" @click="disconnectGitea()">Disconnect</button>
<a x-show="!giteaLinked" class="btn-sm" href="/auth/login?provider=gitea&mode=link" style="text-decoration:none;color:var(--accent);">{{ fd_icon("link",14) }} Connect</a>
</div>
</div>
<!-- GitHub -->
<div class="setting-row">
<div>
<div class="setting-label">GitHub <span x-show="authMethod==='github'" style="font-size:10px;background:var(--accent);color:#fff;padding:1px 6px;border-radius:3px;margin-left:6px;">Primary</span></div>
<div class="setting-desc">
<span x-show="githubLinked">Connected to github.com</span>
<span x-show="!githubLinked">Connect your GitHub account to access repositories</span>
</div>
</div>
<div class="setting-control">
<span x-show="githubLinked" style="font-size:12px;color:var(--toast-success-bg);">✅ Active</span>
<button x-show="githubLinked && authMethod!=='github'" class="btn-sm" style="color:var(--danger);" @click="disconnectGithub()">Disconnect</button>
<a x-show="!githubLinked" class="btn-sm" href="/auth/login?provider=github&mode=link" style="text-decoration:none;color:var(--accent);">{{ fd_icon("link",14) }} Connect</a>
</div>
</div>
</div>
</div>
<!-- Automations -->
<div x-show="{{ 'true' if plugin_enabled('automations') else 'false' }} &amp;&amp; activeSection==='automations'">
<h2>Automations</h2>
<p class="section-desc">Règles if-this-then-that : déclencheur + condition + action. Utilisables aussi via le bloc bouton dans l'éditeur de page.</p>
<!-- New / Edit form -->
<div class="setting-group">
<h3 x-text="editAutomationId ? 'Modifier' : 'Nouvelle automation'"></h3>
<div class="setting-row">
<div>
<div class="setting-label">Nom</div>
<input class="settings-input" style="width:100%;" type="text" x-model="af.name" placeholder="Ex : Notifier le statut Done">
</div>
</div>
<div class="setting-row" style="flex-wrap:wrap;gap:10px;">
<div>
<div class="setting-label">Déclencheur</div>
<select class="settings-input" style="min-width:180px;" x-model="af.trigger_type" @change="af.event='page.created'">
<option value="event">Événement (base de données)</option>
<option value="cron">Planifié (cron)</option>
<option value="button">Bouton cliquable</option>
</select>
</div>
<div x-show="af.trigger_type==='event'">
<div class="setting-label">Événement</div>
<select class="settings-input" style="min-width:200px;" x-model="af.event">
<option value="page.created">Page créée</option>
<option value="page.updated">Page modifiée</option>
<option value="page.deleted">Page supprimée</option>
<option value="collection.created">Base créée</option>
<option value="collection.updated">Base modifiée</option>
<option value="collection.deleted">Base supprimée</option>
</select>
</div>
<div x-show="af.trigger_type==='cron'">
<div>
<div class="setting-label">Expression cron</div>
<input class="settings-input" style="min-width:180px;" type="text" x-model="af.cron_expression" placeholder="*/15 * * * * | @hourly | @daily">
</div>
</div>
<div x-show="af.trigger_type==='event'">
<div>
<div class="setting-label">Base (optionnel)</div>
<select class="settings-input" style="min-width:180px;" x-model="af.collection_id">
<option value="">Toutes les bases</option>
<template x-for="c in globalCollections" :key="c.id">
<option x-bind:value="c.id" x-text="c.icon + ' ' + c.name"></option>
</template>
</select>
</div>
</div>
</div>
<div class="setting-row" x-show="!editSteps.length" style="flex-direction:column;align-items:stretch;gap:6px;">
<div class="setting-label">Conditions (JSON — toutes AND)</div>
<textarea class="settings-input" style="width:100%;min-height:60px;font-family:monospace;font-size:12px;" x-model="af.condition_json" placeholder='[{"property":"Status","op":"eq","value":"Done"}]'></textarea>
<div class="setting-desc">Ops : eq, neq, contains, not_contains, is_empty, is_not_empty, changed</div>
</div>
<div class="setting-row" x-show="!editSteps.length" style="flex-direction:column;align-items:stretch;gap:6px;">
<div class="setting-label">Actions (JSON)</div>
<textarea class="settings-input" style="width:100%;min-height:80px;font-family:monospace;font-size:12px;" x-model="af.actions_json" placeholder='[{"type":"webhook","url":"https://..."}, {"type":"set_property","property":"Status","value":"Done"}, {"type":"create_page","collection_id":1,"title":"New task"}, {"type":"notify","message":"Automation fired"}]'></textarea>
<div class="setting-desc">Types : webhook, set_property, create_page, notify</div>
</div>
<!-- Pipeline visuel (steps API v7.0) -->
<div class="setting-row" x-show="editAutomationId" style="flex-direction:column;align-items:stretch;gap:6px;">
<div class="setting-label">Pipeline visuel (steps)</div>
<div class="setting-desc" x-show="!editSteps.length">Aucun step : automation en mode legacy (JSON ci-dessus). Ajoutez une étape (ou convertissez le JSON) pour basculer en pipeline.</div>
<template x-for="(s, i) in editSteps" :key="s.id">
<div style="border:1px solid var(--border);border-radius:6px;padding:8px 10px;margin-bottom:6px;">
<div style="display:flex;gap:8px;align-items:center;">
<span class="setting-label" style="font-weight:500;" x-text="(i+1) + '. ' + stepSummary(s)"></span>
<span style="flex:1"></span>
<button class="btn-sm" title="Monter" @click="moveStep(i, -1)">↑</button>
<button class="btn-sm" title="Descendre" @click="moveStep(i, 1)">↓</button>
<button class="btn-sm" @click="stepEdit = stepEdit===i ? -1 : i" x-text="stepEdit===i ? 'Fermer' : 'Modifier'"></button>
<button class="btn-sm" style="color:#e5534b;" title="Supprimer" @click="delStep(i)">✕</button>
</div>
<div x-show="stepEdit===i" style="margin-top:8px;display:flex;flex-direction:column;gap:6px;">
<template x-if="s.kind==='trigger'">
<div>
<div class="setting-label">Événement</div>
<input class="settings-input" style="width:100%;" x-model="s.config.event" list="auto-events">
<datalist id="auto-events">
<option value="page.created"></option><option value="page.updated"></option><option value="page.deleted"></option>
<option value="collection.created"></option><option value="collection.updated"></option><option value="collection.deleted"></option>
<option value="form.submitted"></option><option value="meeting.summarized"></option><option value="site.viewed"></option>
</datalist>
</div>
</template>
<template x-if="s.kind==='condition'">
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<input class="settings-input" placeholder="Propriété" x-model="s.config.property">
<select class="settings-input" x-model="s.config.op">
<option value="eq">=</option><option value="neq">≠</option>
<option value="contains">contient</option><option value="not_contains">ne contient pas</option>
<option value="is_empty">vide</option><option value="is_not_empty">non vide</option>
<option value="changed">changé</option>
</select>
<input class="settings-input" placeholder="Valeur" x-model="s.config.value">
</div>
</template>
<template x-if="s.kind==='delay'">
<div>
<div class="setting-label">Secondes (0-86400 · exécution plafonnée à 300 s)</div>
<input class="settings-input" type="number" min="0" max="86400" x-model.number="s.config.seconds">
</div>
</template>
<template x-if="s.kind==='action'">
<div style="display:flex;flex-direction:column;gap:6px;">
<select class="settings-input" x-model="s.config.type">
<option value="webhook">Webhook</option>
<option value="set_property">Définir une propriété</option>
<option value="create_page">Créer une page</option>
<option value="notify">Notification</option>
<option value="slack">Slack</option>
<option value="email">Email</option>
<option value="forge_issue">Issue Gitea/GitHub</option>
<option value="agent_trigger">Déclencher un agent</option>
</select>
<template x-if="s.config.type==='webhook'">
<input class="settings-input" placeholder="https://…" x-model="s.config.url">
</template>
<template x-if="s.config.type==='set_property'">
<div style="display:flex;gap:8px;">
<input class="settings-input" placeholder="Propriété" x-model="s.config.property">
<input class="settings-input" placeholder="Valeur" x-model="s.config.value">
</div>
</template>
<template x-if="s.config.type==='create_page'">
<div style="display:flex;gap:8px;">
<select class="settings-input" x-model="s.config.collection_id">
<option value="">Base (optionnel)</option>
<template x-for="c in globalCollections" :key="c.id">
<option x-bind:value="c.id" x-text="c.icon + ' ' + c.name"></option>
</template>
</select>
<input class="settings-input" placeholder="Titre" x-model="s.config.title">
</div>
</template>
<template x-if="s.config.type==='notify'">
<input class="settings-input" placeholder="Message" x-model="s.config.message">
</template>
<template x-if="s.config.type==='slack'">
<div style="display:flex;gap:8px;">
<input class="settings-input" placeholder="Webhook URL (chiffrée)" x-model="s.config.webhook_url">
<input class="settings-input" placeholder="Texte" x-model="s.config.text">
</div>
</template>
<template x-if="s.config.type==='email'">
<div style="display:flex;gap:8px;flex-direction:column;">
<input class="settings-input" placeholder="À (to)" x-model="s.config.to">
<input class="settings-input" placeholder="Sujet" x-model="s.config.subject">
<textarea class="settings-input" style="min-height:56px;" placeholder="Corps" x-model="s.config.body"></textarea>
</div>
</template>
<template x-if="s.config.type==='forge_issue'">
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<input class="settings-input" placeholder="owner" x-model="s.config.owner">
<input class="settings-input" placeholder="repo" x-model="s.config.repo">
<input class="settings-input" placeholder="Titre" x-model="s.config.title">
<input class="settings-input" placeholder="Labels (virgules)" x-model="s.config.labels">
</div>
</template>
<template x-if="s.config.type==='agent_trigger'">
<div style="display:flex;gap:8px;">
<input class="settings-input" type="number" placeholder="Agent ID" x-model.number="s.config.agent_id">
<input class="settings-input" placeholder="Message" x-model="s.config.message">
</div>
</template>
</div>
</template>
<div><button class="btn-sm" @click="saveStep(s)">Enregistrer l'étape</button></div>
</div>
</div>
</template>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<select class="settings-input" style="min-width:150px;" x-model="stepNewKind">
<option value="trigger">Déclencheur</option>
<option value="condition">Condition</option>
<option value="delay">Attente</option>
<option value="action">Action</option>
</select>
<button class="btn-sm" @click="addStep()">+ Ajouter l'étape</button>
<button class="btn-sm" style="color:var(--accent,#2383e2);" x-show="!editSteps.length && editAutomationId"
@click="convertToSteps()">✨ Convertir le JSON en pipeline</button>
</div>
</div>
<div style="display:flex;gap:8px;margin-top:8px;">
<button class="btn-sm" @click="saveAutomation()" x-text="editAutomationId ? 'Enregistrer' : 'Créer'"></button>
<button class="btn-sm" style="color:var(--text-secondary);" x-show="editAutomationId" @click="cancelEditAutomation()">Annuler</button>
</div>
</div>
<!-- List -->
<div class="setting-group">
<h3>Mes automatisations <span style="font-size:11px;color:var(--text-tertiary);" x-text="automations.length + ' règle(s)'"></span></h3>
<div x-show="!automations.length" class="setting-desc" style="padding:8px 0;">Aucune automation pour l'instant.</div>
<template x-for="a in automations" :key="a.id">
<div class="setting-row" style="border-bottom:1px solid var(--border);">
<div style="flex:1;min-width:0;">
<div class="setting-label">
<span x-text="'⚡ '+a.name"></span>
<span x-show="!a.enabled" style="font-size:10px;background:#666;color:#fff;padding:1px 6px;border-radius:3px;margin-left:6px;">OFF</span>
<span style="font-size:11px;color:var(--text-tertiary);margin-left:8px;" x-text="'('+a.trigger_type+' · '+(a.event||a.cron_expression||'')+')'"></span>
</div>
<div class="setting-desc" style="font-size:11px;">
<span x-text="a.run_count + ' exécution(s)'"></span>
<span x-show="a.last_run_at" x-text="' · dernière : '+a.last_run_at"></span>
</div>
</div>
<div class="setting-control" style="gap:6px;">
<button class="btn-sm" @click="runAutomation(a.id)">▶ Lancer</button>
<button class="btn-sm" @click="toggleAutomation(a.id, a.enabled)"><span x-text="a.enabled ? 'Désactiver' : 'Activer'"></span></button>
<button class="btn-sm" @click="editAutomation(a)">✎</button>
<button class="btn-sm" style="color:var(--danger);" @click="deleteAutomation(a.id)">🗑</button>
</div>
</div>
</template>
</div>
<!-- Runs history -->
<div class="setting-group" x-show="autoRuns.length">
<h3>Historique d'exécutions</h3>
<template x-for="r in autoRuns" :key="r.id">
<div class="setting-row" style="border-bottom:1px solid var(--border);">
<div style="flex:1;min-width:0;">
<span style="font-size:12px;" x-text="r.created_at"></span>
<span style="font-size:11px;color:var(--text-tertiary);margin-left:8px;" x-text="'['+r.trigger_source+']'"></span>
</div>
<span style="font-size:11px;" x-text="r.detail" :style="r.status==='error' ? 'color:var(--danger);' : 'color:var(--text-secondary);'"></span>
</div>
</template>
</div>
</div>
<!-- Admin: Users & Roles -->
<div x-show="activeSection==='admin-users'">
<h2>Users &amp; Roles</h2>
<p class="section-desc">Manage user accounts, roles, and permissions.</p>
<!-- Quick Stats -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px;">
<div class="stat-card">
<div class="stat-value" x-text="adminStats.total_users">—</div>
<div class="stat-label">Total Users</div>
</div>
<div class="stat-card">
<div class="stat-value" x-text="adminStats.total_workspaces">—</div>
<div class="stat-label">Workspaces</div>
</div>
<div class="stat-card">
<div class="stat-value" x-text="adminStats.total_files">—</div>
<div class="stat-label">Files</div>
</div>
<div class="stat-card">
<div class="stat-value" x-text="adminStats.total_mb + ' MB'">— MB</div>
<div class="stat-label">Storage</div>
</div>
</div>
<!-- Create User Button -->
<div style="margin-bottom:16px;">
<button class="btn btn-primary" @click="showCreateUser=!showCreateUser" style="font-size:13px;">+ New User</button>
</div>
<!-- Create User Form -->
<div x-show="showCreateUser" class="new-tag-form" style="margin-bottom:16px;">
<input type="text" placeholder="Login" x-model="newUser.login" style="width:120px;">
<input type="text" placeholder="Full name" x-model="newUser.name" style="width:140px;">
<input type="email" placeholder="Email" x-model="newUser.email" style="width:160px;">
<input type="password" placeholder="Password" x-model="newUser.password" style="width:130px;">
<label style="font-size:11px;color:var(--text-dim);white-space:nowrap;">
<input type="checkbox" x-model="newUser.is_admin"> Admin
</label>
<button class="btn btn-primary" @click="adminCreateUser()" :disabled="!newUser.login || !newUser.password" style="padding:4px 12px;font-size:12px;">Create</button>
</div>
<!-- Users Table -->
<div class="table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>User</th>
<th>Role</th>
<th>Status</th>
<th>Workspaces</th>
<th>Files</th>
<th>Folders</th>
<th>Storage</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<template x-for="u in adminUsers" :key="u.id">
<tr>
<td>
<div style="font-weight:500;" x-text="u.full_name || u.login"></div>
<div style="font-size:11px;color:var(--text-dim);" x-text="u.login"></div>
</td>
<td>
<span class="role-badge" :class="u.is_admin ? 'role-admin' : 'role-user'" x-text="u.is_admin ? 'Admin' : 'User'"></span>
</td>
<td>
<span class="status-dot" :class="u.is_active ? 'active' : 'inactive'"></span>
<span x-text="u.is_active ? 'Active' : 'Inactive'" style="font-size:12px;color:var(--text-dim);"></span>
</td>
<td x-text="u.ws_count" style="text-align:center;"></td>
<td x-text="u.file_count" style="text-align:center;"></td>
<td x-text="u.folder_count" style="text-align:center;"></td>
<td x-text="u.total_mb + ' MB'" style="text-align:right;"></td>
<td>
<span style="font-size:11px;color:var(--text-dim);" x-text="fmtLastLogin(u)"></span>
</td>
<td>
<div style="display:flex;gap:4px;">
<button class="btn-sm" @click="editUserRow(u)" title="Edit">{{ fd_icon("edit",14) }}</button>
<button class="btn-sm btn-sm-danger" @click="adminDeleteUser(u.id)" title="Delete" x-show="adminUsers.length > 1">{{ fd_icon("trash",14) }}</button>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Edit User Modal -->
<div class="modal-overlay" x-show="editingUser" @click.self="editingUser=null" @keydown.escape="editingUser=null" style="z-index:1001;">
<div class="modal-box" style="max-width:400px;">
<h3 style="margin:0 0 12px;">Edit User: <span x-text="editUserForm.login"></span></h3>
<div style="display:flex;flex-direction:column;gap:10px;">
<input type="text" class="settings-input" placeholder="Full name" x-model="editUserForm.name">
<input type="email" class="settings-input" placeholder="Email" x-model="editUserForm.email">
<input type="password" class="settings-input" placeholder="New password (leave blank to keep)" x-model="editUserForm.password">
<label style="font-size:13px;color:var(--text);display:flex;align-items:center;gap:8px;">
<input type="checkbox" x-model="editUserForm.is_admin"> Admin role
</label>
<label style="font-size:13px;color:var(--text);display:flex;align-items:center;gap:8px;">
<input type="checkbox" x-model="editUserForm.is_active"> Active account
</label>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px;">
<button class="btn" @click="editingUser=null">Cancel</button>
<button class="btn btn-primary" @click="adminUpdateUser()">Save</button>
</div>
</div>
</div>
</div>
<!-- Admin: Audit Log -->
<div x-show="activeSection==='admin-audit'">
<h2>Audit Log</h2>
<p class="section-desc">Actions API, changements de permissions et connexions SSO (unifiés).</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px;align-items:center;">
<template x-for="s in ['all','api','permissions','sso']" :key="s">
<button class="btn" :class="{ 'btn-primary': auditSource===s }" style="font-size:12px;" @click="setAuditSource(s)" x-text="s"></button>
</template>
<input type="text" placeholder="actor (user id)" x-model="auditActor" style="font-size:12px;padding:6px 8px;" @keyup.enter="loadAuditLogs()">
<input type="text" placeholder="action (LIKE)" x-model="auditAction" style="font-size:12px;padding:6px 8px;" @keyup.enter="loadAuditLogs()">
<button class="btn" style="font-size:12px;" @click="loadAuditLogs()">Apply</button>
<span style="flex:1;"></span>
<a class="btn" style="font-size:12px;" :href="auditCsvUrl()" download>Export CSV</a>
</div>
<div class="table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Date</th>
<th>Source</th>
<th>Actor</th>
<th>Action</th>
<th>Resource</th>
<th>IP</th>
<th>Detail</th>
</tr>
</thead>
<tbody>
<template x-for="e in auditLogs" :key="e.at + '-' + e.source + '-' + e.actor">
<tr>
<td><span style="font-size:12px;" x-text="fmtAuditDate(e)"></span></td>
<td><span style="font-size:11px;" :class="'audit-src audit-src-'+e.source" x-text="e.source"></span></td>
<td><code style="font-size:11px;" x-text="e.actor"></code></td>
<td><code style="font-size:11px;" x-text="e.action"></code></td>
<td><span style="font-size:11px;" x-text="e.resource"></span></td>
<td><code style="font-size:11px;" x-text="e.ip || '—'"></code></td>
<td style="font-size:11px;color:var(--text-dim);max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" :title="e.detail" x-text="e.detail || '—'"></td>
</tr>
</template>
<tr x-show="!auditLogs.length && !auditLoading">
<td colspan="7" style="text-align:center;color:var(--text-dim);padding:20px;">No audit events yet</td>
</tr>
</tbody>
</table>
</div>
<div style="display:flex;justify-content:center;margin-top:12px;">
<button class="btn" style="font-size:12px;" x-show="auditHasMore" @click="auditNext()">Load more</button>
</div>
<style>
.audit-src{display:inline-block;padding:1px 7px;border-radius:8px;font-weight:600;}
.audit-src-api{background:rgba(76,154,255,.15);color:#4c9aff;}
.audit-src-permissions{background:rgba(255,176,0,.15);color:#ffb000;}
.audit-src-sso{background:rgba(0,200,100,.15);color:#00cc66;}
</style>
</div>
<!-- Admin: Backups -->
<div x-show="activeSection==='admin-backups'">
<h2>Backups</h2>
<p class="section-desc">Sauvegardes automatiques de la base (quotidiennes). Cliquez pour en générer une immédiatement.</p>
<div class="setting-group">
<div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;">
<button class="btn btn-primary" @click="runBackup()" :disabled="backupRunning" style="font-size:13px;">
<span x-show="!backupRunning">{{ fd_icon("save",14) }} Lancer une sauvegarde</span>
<span x-show="backupRunning">Sauvegarde…</span>
</button>
<span x-show="backupMsg" x-text="backupMsg" style="font-size:13px;" :style="{color: backupOk ? 'var(--toast-success-bg)' : 'var(--danger)'}"></span>
</div>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Fichier</th><th>Taille</th><th>Modifié</th></tr></thead>
<tbody>
<template x-for="b in backups" :key="b.filename">
<tr>
<td><code style="font-size:12px;" x-text="b.filename"></code></td>
<td x-text="b.size + ' B'" style="text-align:right;"></td>
<td x-text="b.modified_at" style="font-size:12px;color:var(--text-dim);"></td>
</tr>
</template>
<tr x-show="!backups.length"><td colspan="3" style="text-align:center;color:var(--text-dim);padding:20px;" x-text="'Aucune sauvegarde — ou dossier '+ '[' + 'backups' + ']' + ' non accessible.'"></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Admin: SSO / Enterprise (v6.7.0) -->
<div x-show="activeSection==='admin-sso'">
<h2>SSO / Enterprise</h2>
<p class="section-desc">Connectez un IdP d'entreprise (SAML 2.0 ou OpenID Connect). Les comptes sont créés au premier login et les groupes de l'IdP deviennent des rôles workspace.</p>
<!-- Status -->
<div class="llm-summary" x-show="ssoCfg.id || ssoSource==='env'">
<div class="llm-summary-icon">🔐</div>
<div class="llm-summary-body">
<div class="llm-summary-title" x-text="ssoStatusTitle()"></div>
<div class="llm-summary-desc" x-text="ssoStatusDesc()"></div>
</div>
<div class="llm-summary-side">
<span class="llm-badge" :class="ssoSource==='env' ? 'warn' : 'ok'" x-text="ssoSource==='env' ? 'via .env' : 'Actif'"></span>
</div>
</div>
<!-- Provider -->
<div class="setting-group">
<h3>Fournisseur</h3>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
<select class="settings-input" x-model="ssoCfg.provider_type" style="max-width:220px;">
<option value="saml">SAML 2.0</option>
<option value="oidc">OpenID Connect</option>
</select>
<input type="text" class="settings-input" placeholder="Nom affiché (ex : Company SSO)" x-model="ssoCfg.name" style="max-width:300px;">
</div>
</div>
<!-- SAML -->
<div class="setting-group" x-show="ssoCfg.provider_type==='saml'">
<h3>Configuration SAML</h3>
<div style="font-size:12px;color:var(--text-dim);margin-bottom:6px;">Entity ID (IdP)</div>
<input type="text" class="settings-input" x-model="ssoCfg.entity_id" placeholder="https://idp.example.com/saml/metadata" style="max-width:560px;">
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Single Sign-On URL</div>
<input type="text" class="settings-input" x-model="ssoCfg.sso_url" placeholder="https://idp.example.com/saml/sso" style="max-width:560px;">
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Single Logout URL (optionnel)</div>
<input type="text" class="settings-input" x-model="ssoCfg.slo_url" placeholder="https://idp.example.com/saml/slo" style="max-width:560px;">
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Certificat de signature de l'IdP (PEM)</div>
<textarea class="settings-input" rows="6" x-model="ssoCfg.x509_certificate" style="max-width:560px;font-family:var(--font-mono);font-size:12px;" placeholder="-----BEGIN CERTIFICATE-----"></textarea>
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Mapping des attributs (JSON)</div>
<textarea class="settings-input" rows="4" x-model="ssoAttrJson" style="max-width:560px;font-family:var(--font-mono);font-size:12px;" placeholder='{"email":"email","full_name":"displayName","groups":"groups"}'></textarea>
<label style="display:flex;gap:8px;align-items:center;font-size:13px;margin-top:12px;">
<input type="checkbox" x-model="ssoCfg.sign_requests"> Signer les AuthnRequests / LogoutRequests
</label>
<div style="margin-top:14px;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;font-size:13px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<span>SP metadata à donner à l'IdP :</span>
<code style="font-size:12px;user-select:all;" x-text="ssoMetadataUrl()"></code>
<button class="btn btn-secondary" style="font-size:12px;padding:4px 10px;" @click="copySsoMetadata()">Copier</button>
</div>
</div>
<!-- OIDC -->
<div class="setting-group" x-show="ssoCfg.provider_type==='oidc'">
<h3>Configuration OpenID Connect</h3>
<div style="font-size:12px;color:var(--text-dim);margin-bottom:6px;">Issuer URL</div>
<input type="text" class="settings-input" x-model="ssoCfg.issuer_url" placeholder="https://idp.example.com/realms/flowdeck" style="max-width:560px;">
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Client ID</div>
<input type="text" class="settings-input" x-model="ssoCfg.client_id" placeholder="flowdeck" style="max-width:560px;">
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Client Secret</div>
<div class="pw-wrapper" style="max-width:560px;">
<input type="password" class="settings-input" x-model="ssoCfg.client_secret"
:placeholder="ssoCfg.client_secret_set ? 'Déjà enregistré — laisser vide pour conserver' : 'Client secret'"
style="padding-right:36px;">
</div>
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Scope</div>
<input type="text" class="settings-input" x-model="ssoCfg.scope" placeholder="openid profile email" style="max-width:560px;">
<p style="font-size:12px;color:var(--text-dim);margin-top:10px;">L'échange du code utilise PKCE (S256) et le nonce est vérifié à chaque login.</p>
</div>
<!-- Provisioning -->
<div class="setting-group">
<h3>Provisioning &amp; accès</h3>
<label style="display:flex;gap:8px;align-items:flex-start;font-size:13px;margin-bottom:8px;">
<input type="checkbox" x-model="ssoCfg.auto_provision" style="margin-top:3px;">
<span><b>Auto-provision</b> — créer le compte au premier login (sinon seuls les comptes existants passent)</span>
</label>
<label style="display:flex;gap:8px;align-items:flex-start;font-size:13px;margin-bottom:8px;">
<input type="checkbox" x-model="ssoCfg.sso_only" style="margin-top:3px;">
<span><b>SSO only</b> — désactiver le login local (les administrateurs gardent le leur)</span>
</label>
<div style="font-size:12px;color:var(--text-dim);margin:12px 0 6px;">Espace par défaut des utilisateurs SSO</div>
<select class="settings-input" x-model="ssoCfg.default_workspace_id" style="max-width:320px;">
<option value="">— Aucun —</option>
<template x-for="w in ssoWorkspaces" :key="w.id">
<option :value="w.id" x-text="w.name"></option>
</template>
</select>
</div>
<!-- Group mapping -->
<div class="setting-group">
<h3>Groupes IdP → rôles workspace</h3>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Groupe SSO</th><th>Rôle</th><th>Espace</th><th></th></tr></thead>
<tbody>
<template x-for="(g, i) in ssoGroups" :key="i">
<tr>
<td><input class="settings-input" x-model="g.sso_group" placeholder="FlowDeck Admins" style="min-width:170px;"></td>
<td>
<select class="settings-input" x-model="g.workspace_role" style="min-width:110px;">
<option value="admin">admin</option>
<option value="editor">editor</option>
<option value="viewer">viewer</option>
</select>
</td>
<td>
<select class="settings-input" x-model="g.workspace_id" style="min-width:150px;">
<option value="">Espace par défaut</option>
<template x-for="w in ssoWorkspaces" :key="w.id">
<option :value="w.id" x-text="w.name"></option>
</template>
</select>
</td>
<td><button class="btn btn-secondary" style="font-size:12px;padding:4px 8px;" @click="removeSsoGroup(i)">✕</button></td>
</tr>
</template>
<tr x-show="!ssoGroups.length"><td colspan="4" style="text-align:center;color:var(--text-dim);padding:14px;">Aucune règle — les groupes de l'IdP ne modifient aucun rôle.</td></tr>
</tbody>
</table>
</div>
<button class="btn btn-secondary" style="font-size:13px;margin-top:10px;" @click="addSsoGroup()">+ Ajouter une règle</button>
</div>
<!-- Actions -->
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:6px;">
<button class="btn btn-primary" style="font-size:13px;" @click="saveSsoConfig()" :disabled="ssoSaving" x-text="ssoSaving ? 'Enregistrement…' : 'Enregistrer la configuration SSO'"></button>
<button class="btn btn-secondary" style="font-size:13px;" x-show="ssoCfg.id" @click="disableSso()">Désactiver le SSO</button>
<button class="btn btn-secondary" style="font-size:13px;" x-show="ssoCfg.id" @click="syncSsoGroups()">Re-sync des groupes</button>
<span x-show="ssoMsg" x-text="ssoMsg" style="font-size:13px;" :style="{color: ssoOk ? 'var(--toast-success-bg, #00CC66)' : 'var(--danger)'}"></span>
</div>
<p x-show="ssoSource==='env'" class="section-desc" style="margin-top:10px;">Configuration actuellement lue depuis les variables <code>SSO_*</code> du .env — un enregistrement ici la remplace.</p>
</div>
<!-- Agent & IA: per-user provider keys + admin global default -->
<div x-show="activeSection==='llm'">
<h2>Agent &amp; IA</h2>
<p class="section-desc">Connectez des fournisseurs d'IA pour piloter l'agent FlowDeck. Un fournisseur doit être enregistré <b>et</b> avoir réussi son test de connexion pour devenir disponible dans le panneau Agent (🤖 en bas à droite). Sans fournisseur actif, l'agent fonctionne en mode hors-ligne (mock).</p>
<!-- Status summary -->
<div class="llm-summary">
<div class="llm-summary-icon">🤖</div>
<div class="llm-summary-body">
<div class="llm-summary-title" x-text="llmSummaryTitle"></div>
<div class="llm-summary-desc" x-text="llmSummaryDesc"></div>
</div>
<div class="llm-summary-side" x-show="llmActiveCount">
<span class="llm-badge ok">✓ <span x-text="llmActiveCount"></span>&nbsp;actif(s)</span>
</div>
</div>
<!-- Admin: workspace default provider -->
<div class="setting-group" x-show="userIsAdmin">
<h3>Fournisseur par défaut de l'espace (tous les utilisateurs)</h3>
<div class="prov-card">
<div class="prov-card-head">
<span class="prov-card-name">Configuration globale</span>
<span class="llm-badge" :class="llmGlobalBadgeClass()" x-text="llmGlobalBadge()"></span>
</div>
<div class="prov-field-row" style="margin-top:2px;">
<div class="prov-field-label">Fournisseur</div>
<select class="settings-input" x-model="llmForm.provider" @change="onLlmProviderChange()" style="max-width:320px;">
<template x-for="p in llmProviders" :key="p.id">
<option :value="p.id" x-text="p.name + (p.requires_key ? ' (clé API requise)' : '')"></option>
</template>
</select>
</div>
<div class="prov-field-row" x-show="llmForm.provider !== 'offline'">
<div class="prov-field-label">Modèle par défaut</div>
<select class="settings-input" x-model="llmForm.model" style="max-width:320px;">
<template x-for="m in llmProviderModels" :key="m">
<option :value="m" x-text="m"></option>
</template>
</select>
</div>
<div class="prov-field-row" x-show="globalNeedsKey()">
<div class="prov-field-label">Clé API <span class="muted" x-text="llmHasKey ? '(déjà configurée — laisser vide pour conserver)' : '(requise pour ce fournisseur)'"></span></div>
<div class="pw-wrapper" style="max-width:320px;">
<input :type="llmKeyVisible ? 'text' : 'password'" class="settings-input" x-model="llmForm.api_key" placeholder="sk-…" style="padding-right:36px;">
<button type="button" class="pw-toggle" @click="llmKeyVisible=!llmKeyVisible" style="top:50%;">
<span x-show="llmKeyVisible" x-cloak>{{ fd_icon('eye-off',14) }}</span>
<span x-show="!llmKeyVisible" x-cloak>{{ fd_icon('eye',14) }}</span>
</button>
</div>
</div>
<div class="prov-field-row">
<div class="prov-field-label">URL API <span class="muted">(optionnelle — Ollama, proxy OpenAI-compatible…)</span></div>
<input type="text" class="settings-input" x-model="llmForm.api_base" placeholder="https://…" style="max-width:320px;">
</div>
<div style="display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;">
<button class="btn btn-primary" @click="saveLlmConfig()" :disabled="llmSaving" style="font-size:13px;">
<span x-show="!llmSaving">{{ fd_icon("save",14) }} Enregistrer</span>
<span x-show="llmSaving">Enregistrement…</span>
</button>
<button class="btn" @click="testLlm()" :disabled="llmTesting" style="font-size:13px;">
<span x-show="!llmTesting">🔌 Tester la connexion</span>
<span x-show="llmTesting">Test…</span>
</button>
<button class="btn" @click="resetGlobalLlm()" :disabled="llmSaving" x-show="llmDefaultProvider !== 'offline'" style="font-size:13px;color:var(--danger);">🗑 Réinitialiser</button>
</div>
<div style="margin-top:10px;font-size:12.5px;">
<span x-show="llmMsg" x-text="llmMsg" :style="{color: llmMsgOk ? 'var(--toast-success-bg)' : 'var(--danger)'}"></span>
<span x-show="llmTestMsg" x-text="llmTestMsg" :style="{color: llmTestOk ? 'var(--toast-success-bg)' : 'var(--danger)'}"></span>
</div>
<p style="font-size:11.5px;color:var(--text-dim);margin:10px 0 0;">💡 Après un changement de fournisseur ou de clé, lancez « Tester la connexion » : si le test réussit, ce fournisseur devient disponible pour tous dans le panneau Agent.</p>
</div>
</div>
<!-- My providers (per-user API keys) -->
<div class="setting-group">
<h3>Mes fournisseurs</h3>
<p style="font-size:12px;color:var(--text-dim);margin:0 0 12px;">Enregistrez vos propres clés : elles sont utilisées quand vous choisissez ce fournisseur dans le panneau Agent, et vous rendent le fournisseur disponible (après test réussi). Sélectionnez un fournisseur dans la liste pour le configurer.</p>
<div class="llm-toolbar">
<div class="llm-search">
{{ fd_icon("search",14) }}
<input type="text" class="settings-input" x-model="llmSearch" placeholder="Rechercher un fournisseur…">
</div>
<span class="llm-count" x-text="filteredProviders().length + ' fournisseurs'"></span>
</div>
<div class="llm-layout">
<div class="llm-list">
<template x-for="p in filteredProviders()" :key="p.id">
<div class="llm-list-item" :class="{active: p.id===selectedProviderId}" @click="selectProvider(p.id)">
<span class="llm-dot" :class="llmBadgeClass(p)"></span>
<span class="llm-list-name" x-text="p.name"></span>
<span class="llm-list-status" x-text="llmShortStatus(p)"></span>
</div>
</template>
<div class="llm-list-empty" x-show="!filteredProviders().length">Aucun fournisseur</div>
</div>
<div class="llm-detail">
<template x-if="selectedProvider()">
<div>
<div class="llm-detail-head">
<span class="llm-detail-name" x-text="selectedProvider().name"></span>
<span class="llm-badge" :class="llmBadgeClass(selectedProvider())" x-text="llmBadge(selectedProvider())"></span>
</div>
<div class="llm-detail-base" x-text="keyForm(selectedProviderId).api_base || selectedProvider().base_url || '—'"></div>
<div class="prov-card-sub" x-show="keyForm(selectedProviderId).last_error" style="color:var(--danger);margin-bottom:0;" x-text="'Dernier test : ' + keyForm(selectedProviderId).last_error"></div>
<div class="prov-field-row">
<div class="prov-field-label">Clé API <span class="muted" x-text="keyForm(selectedProviderId).has_key ? '(enregistrée — laisser vide pour conserver)' : (selectedProvider().requires_key ? '(requise)' : '(optionnelle)')"></span></div>
<div class="pw-wrapper" style="max-width:100%;">
<input :type="keyForm(selectedProviderId).keyVisible ? 'text' : 'password'" class="settings-input"
x-model="keyForm(selectedProviderId).api_key" placeholder="sk-…" style="padding-right:36px;box-sizing:border-box;">
<button type="button" class="pw-toggle" @click="keyForm(selectedProviderId).keyVisible=!keyForm(selectedProviderId).keyVisible" style="top:50%;">
<span x-show="keyForm(selectedProviderId).keyVisible" x-cloak>{{ fd_icon('eye-off',14) }}</span>
<span x-show="!keyForm(selectedProviderId).keyVisible" x-cloak>{{ fd_icon('eye',14) }}</span>
</button>
</div>
</div>
<div class="prov-field-row">
<div class="prov-field-label">URL API <span class="muted">(optionnelle)</span></div>
<input type="text" class="settings-input" x-model="keyForm(selectedProviderId).api_base" placeholder="https://…" style="box-sizing:border-box;">
</div>
<div class="prov-field-row">
<div class="prov-field-label">Modèle par défaut</div>
<select class="settings-input" x-model="keyForm(selectedProviderId).model" style="box-sizing:border-box;">
<template x-for="m in keyModels(selectedProviderId)" :key="m">
<option :value="m" x-text="m"></option>
</template>
</select>
</div>
<div class="prov-actions">
<button class="btn btn-primary" @click="saveUserKey(selectedProviderId)" :disabled="keyForm(selectedProviderId).saving" style="font-size:12px;padding:6px 12px;">
<span x-show="!keyForm(selectedProviderId).saving">{{ fd_icon("save",14) }} Enregistrer &amp; tester</span>
<span x-show="keyForm(selectedProviderId).saving">Enregistrement…</span>
</button>
<button class="btn" @click="testUserKey(selectedProviderId)" :disabled="keyForm(selectedProviderId).testing" style="font-size:12px;padding:6px 12px;">
<span x-show="!keyForm(selectedProviderId).testing">🔌 Tester</span>
<span x-show="keyForm(selectedProviderId).testing">Test…</span>
</button>
<button class="btn" @click="loadProviderModels(selectedProviderId)" :disabled="keyForm(selectedProviderId).modelLoading" style="font-size:12px;padding:6px 12px;">
<span x-show="!keyForm(selectedProviderId).modelLoading">{{ fd_icon("refresh",14) }} Modèles</span>
<span x-show="keyForm(selectedProviderId).modelLoading">Chargement…</span>
</button>
<button class="btn" @click="deleteUserKey(selectedProviderId)" :disabled="keyForm(selectedProviderId).deleting" x-show="keyForm(selectedProviderId).has_key" style="font-size:12px;padding:6px 12px;color:var(--danger);">Supprimer</button>
</div>
<div class="prov-msg">
<span x-show="keyForm(selectedProviderId).msg" x-text="keyForm(selectedProviderId).msg" :style="{color: keyForm(selectedProviderId).ok ? 'var(--toast-success-bg)' : 'var(--danger)'}"></span>
</div>
</div>
</template>
<div class="llm-detail-empty" x-show="!selectedProvider()">Sélectionnez un fournisseur pour le configurer.</div>
</div>
</div>
<div class="llm-hint">
💡 <b>Comment activer un fournisseur ?</b> Sélectionnez-le, enregistrez votre clé puis cliquez « Enregistrer &amp; tester » (ou « Tester ») : dès que la connexion réussit, le fournisseur et ses modèles apparaissent dans le sélecteur du panneau Agent.
Pour Ollama, aucun fournisseur distant : renseignez l'URL locale (ex. <code>http://localhost:11434/v1</code>) puis testez.
En mode hors-ligne (aucun fournisseur actif), l'agent reste utilisable mais génère du contenu local, sans appel réseau.
</div>
</div>
</div>
</div>
</div>
</div>
<script type="application/json" id="st-config" nonce="{{ csp_nonce() }}">{{ {"avatar_url": avatar_url or "", "avatar_color": avatar_color or "#3A3A3A", "full_name": user.full_name or "", "login": user.login or "", "email": user.email or "", "is_admin": True if user.get("is_admin") else False, "auth_method": auth_method} | tojson }}</script>
<script data-cfasync="false" src="/static/js/settings.js?v={{ asset_version }}"></script>
{% endblock %}