feat: éditeur visuel d'automations (pipeline steps) + fix CSP multi-instructions (v7.45.0)
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / test (push) Successful in 15m9s

Added — Settings → Automations, pipeline visuel (API steps v7.0) :
- Cartes ordinees (trigger/condition/delay/action) : resume + edition
  TYPÉE par kind/type (datalist evenements, 7 ops, 8 types d'action avec
  leurs champs reels), ajout/edition/suppression/haut-bas via
  POST/PUT/DELETE /workspace/automations[/steps]/...
- ✨ Convertir le JSON en pipeline (legacy → steps ordonnes) ; textareas
  JSON masques des qu'un step existe.
- Gate E2E « editeur visuel de steps » : creation → edition → ajout →
  carte « Action · webhook » sous CSP reel.

Fixed (trouve par le gate) — 51 expressions Alpine MULTI-INSTRUCTIONS
(`a=1; b()`) = interdites par le parseur CSP (une seule expression par
directive ; ';' = token inattendu) — INVISIBLE pour le scan par tokens :
- Conversion en methodes dans 11 fichiers : nav settings x8 (navTo),
  menu section base x7 (closeAndSetCount/Move/...), parts+editeur x13
  (setSharePerm, more*, markAndSave...), breadcrumb x5 (hover*/goClose),
  library/local x6 (menus popup), board x3 (pickStatus/...), ctx-menu x2
  (addTagAndClear), agent/card/gitea/workspaces x6.
- Scanner dedie scan_semi (inventaire ';' hors chaines) ajoute au lot.

Verifs : 39 templates Jinja parse OK · scan expressions = 0 incompatible
(4 faux positifs en chaines) · **E2E 8/8** · suite **1094/1094** ·
ruff OK · docs a jour
This commit is contained in:
2026-10-02 16:56:29 -04:00
parent b0af1bbfb6
commit 6d7af3fb64
28 changed files with 445 additions and 60 deletions
+129
View File
@@ -75,6 +75,9 @@ function settingsInit() {
automations: [],
autoRuns: [],
editAutomationId: null,
editSteps: [],
stepEdit: -1,
stepNewKind: 'action',
globalCollections: [],
af: { name:'', trigger_type:'event', event:'page.created', cron_expression:'*/15 * * * *', collection_id:'', condition_json:'[]', actions_json:'[]' },
@@ -773,6 +776,128 @@ function settingsInit() {
if (r.ok) { this.githubLinked = false; }
} catch { /* volontaire */ }
},
// ── Éditeur visuel de steps (API v7.0 : /steps CRUD) ──
async loadSteps() {
if (!this.editAutomationId) { this.editSteps = []; return; }
try {
var r = await fetch('/workspace/automations/' + this.editAutomationId + '/steps',
{credentials:'same-origin'});
var d = await r.json();
this.editSteps = Array.isArray(d) ? d : (d.steps || []);
} catch { this.editSteps = []; }
},
stepSummary(s) {
var c = s.config || {};
if (s.kind === 'trigger') return '\ud83d\udce1 D\u00e9clencheur \u00b7 ' + (c.event || '?');
if (s.kind === 'condition') return '\u2696 Condition \u00b7 ' + (c.property || '?') + ' ' + (c.op || 'eq') + ' ' + (c.value || '');
if (s.kind === 'delay') return '\u23f3 Attente \u00b7 ' + (c.seconds || 0) + ' s';
var extra = c.url || c.message || c.text || c.to || (c.owner ? c.owner + '/' + c.repo : '') || c.title || '';
return '\u26a1 Action \u00b7 ' + (c.type || '?') + (extra ? ' \u2192 ' + String(extra).slice(0, 60) : '');
},
defaultStepConfig(kind) {
if (kind === 'trigger') return { event: 'page.created' };
if (kind === 'condition') return { property: '', op: 'eq', value: '' };
if (kind === 'delay') return { seconds: 60 };
return { type: 'webhook', url: '' };
},
async addStep() {
if (!this.editAutomationId) return;
try {
var r = await fetch('/workspace/automations/' + this.editAutomationId + '/steps', {
method: 'POST', credentials:'same-origin',
headers: {'Content-Type':'application/json','X-CSRF-Token': this.getCsrfToken()},
body: JSON.stringify({ kind: this.stepNewKind, config: this.defaultStepConfig(this.stepNewKind) })
});
var d = await r.json();
if (!r.ok) { window.showToast && window.showToast(d.detail || '\u00c9chec', 'error'); return; }
await this.loadSteps();
this.stepEdit = this.editSteps.length - 1;
} catch { /* volontaire */ }
},
async saveStep(s) {
var cfg = s.config || {};
// forge_issue : labels en cha\u00eene s\u00e9par\u00e9e par virgules → liste
if (cfg.type === 'forge_issue' && typeof cfg.labels === 'string') {
cfg.labels = cfg.labels.split(',').map(function(x){ return x.trim(); }).filter(Boolean);
}
try {
var r = await fetch('/workspace/automations/steps/' + s.id, {
method:'PUT', credentials:'same-origin',
headers: {'Content-Type':'application/json','X-CSRF-Token': this.getCsrfToken()},
body: JSON.stringify({ kind: s.kind, config: cfg })
});
var d = await r.json();
if (!r.ok) { window.showToast && window.showToast(d.detail || '\u00c9chec', 'error'); return; }
window.showToast && window.showToast('\u00c9tape enregistr\u00e9e');
await this.loadSteps();
} catch { /* volontaire */ }
},
async delStep(i) {
var s = this.editSteps[i]; if (!s) return;
try {
await fetch('/workspace/automations/steps/' + s.id, {
method:'DELETE', credentials:'same-origin',
headers:{'X-CSRF-Token': this.getCsrfToken()} });
this.stepEdit = -1;
await this.loadSteps();
} catch { /* volontaire */ }
},
async moveStep(i, dir) {
var j = i + dir;
if (j < 0 || j >= this.editSteps.length) return;
var a = this.editSteps[i], b = this.editSteps[j];
var h = {'Content-Type':'application/json','X-CSRF-Token': this.getCsrfToken()};
try {
await fetch('/workspace/automations/steps/' + a.id, {method:'PUT', headers:h,
credentials:'same-origin', body: JSON.stringify({position: b.position})});
await fetch('/workspace/automations/steps/' + b.id, {method:'PUT', headers:h,
credentials:'same-origin', body: JSON.stringify({position: a.position})});
await this.loadSteps();
} catch { /* volontaire */ }
},
async convertToSteps() {
// JSON legacy → pipeline ordonn\u00e9 (trigger, conditions, actions)
if (!this.editAutomationId) return;
var h = {'Content-Type':'application/json','X-CSRF-Token': this.getCsrfToken()};
var base = '/workspace/automations/' + this.editAutomationId + '/steps';
var order = [];
if (this.af.trigger_type !== 'cron') {
var tcfg = {event: this.af.event || 'page.created'};
if (this.af.collection_id) tcfg.collection_id = parseInt(this.af.collection_id, 10);
order.push({kind:'trigger', config: tcfg});
}
try {
JSON.parse(this.af.condition_json || '[]').forEach(function(c){ order.push({kind:'condition', config:c}); });
} catch { /* volontaire */ }
try {
JSON.parse(this.af.actions_json || '[]').forEach(function(a){ order.push({kind:'action', config:a}); });
} catch { /* volontaire */ }
for (var i = 0; i < order.length; i++) {
var r = await fetch(base, {method:'POST', headers:h, credentials:'same-origin',
body: JSON.stringify(order[i])});
if (!r.ok) {
var d = await r.json().catch(function(){ return {}; });
window.showToast && window.showToast(d.detail || 'Conversion \u00e9chou\u00e9e', 'error');
break;
}
}
await this.loadSteps();
},
navTo(sec) {
this.activeSection = sec;
var loaders = {'api-tokens':'loadApiTokens', 'sessions':'loadSessions',
'extensions':'loadClipperDevices', 'automations':'loadAutomations',
'admin-users':'loadAdminUsers', 'admin-audit':'loadAdminAudit',
'admin-backups':'loadBackups', 'admin-sso':'loadSsoConfig'};
if (loaders[sec] && this[loaders[sec]]) this[loaders[sec]]();
},
setAuditSource(s) { this.auditSource = s; this.loadAuditLogs(); },
auditNext() { this.auditOffset += this.auditPageSize; this.loadAuditLogs(false); },
editUserRow(u) {
this.editingUser = u;
this.editUserForm = { login: u.login, name: u.full_name, email: u.email,
is_admin: u.is_admin, is_active: u.is_active };
},
async loadAutomations() {
try { await this.loadCollectionsForForm(); } catch { /* volontaire */ }
try {
@@ -826,9 +951,13 @@ function settingsInit() {
condition_json: a.condition_json || '[]',
actions_json: a.actions_json || '[]'
};
this.stepEdit = -1;
this.loadSteps();
},
cancelEditAutomation() {
this.editAutomationId = null;
this.editSteps = [];
this.stepEdit = -1;
this.af = { name:'', trigger_type:'event', event:'page.created', cron_expression:'*/15 * * * *', collection_id:'', condition_json:'[]', actions_json:'[]' };
},
async toggleAutomation(id, currentlyEnabled) {