feat: A20 phase 3 LOT 3b — gitea + agent + éditeur verts en CSP (v7.41.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s

Changed :
- gitea_workspace : x-data="giteaWorkspace" → appel giteaWorkspace(),
  new Date(…) → fmtGwDate(pp), x-html icône arbre → bindGwIcon (x-init +
  Alpine.effect).
- agent_panel : x-html markdown → bindMarkdown($el, m) (effet reactif).
- page_editor : les 12 sites window.E du topbar right_actions →
  délégués appState (edCall('…') x6, edTimeAgo, edCommentCount, edShared,
  bindStar — les 2 branches du ternaire favorited étaient identiques) ;
  + 3 sites dans _page_editor_content (edCall commentOnSelection,
  openBacklink, fmtImportSize, bindIconHtml). Garde Jinja : quotes \' dans
  le set délimité par ' (quote nue = 500).
- Gate éditeur (csp_preview) : création collection → /pages/{id},
  délégués + editorState liés, filet 0-erreur.

Fixed :
- x-html iconHtml() du contenu éditeur = directive INTERDITE sous build
  CSP (attrapé par le filet) → x-init + Alpine.effect.

⚠️ BUG pre-existant identifie (pas introduit ici) : les right_actions du
topbar sont servis ÉCHAPPÉS sur TOUTES les pages (entities "/< —
boutons Share/Star/Settings en texte brut). _header:141 a bien |safe,
ENV standard, rendu local = PARSED ; cause serveur à cerner → suivi
ROADMAP dédié. Le gate éditeur n'asserte donc pas la présence boutons.

suite **1093/1093** · ruff OK · E2E **7/7** (5 csp_preview + 2 smoke)
· docs a jour
This commit is contained in:
2026-10-02 15:15:26 -04:00
parent d7d9966edf
commit 6914780f24
17 changed files with 213 additions and 16 deletions
+1 -1
View File
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="7.40.0",
version="7.41.0",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
+4 -4
View File
@@ -546,7 +546,7 @@
<div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 50%,rgba(0,0,0,.6) 100%);z-index:1;"></div>
</div>
<div class="page-title-block">
<span class="page-icon-emoji" @click="openIconPicker()" title="Change icon" x-html="iconHtml()"></span>
<span class="page-icon-emoji" @click="openIconPicker()" title="Change icon" x-init="bindIconHtml($el)"></span>
<div
class="page-title-input"
contenteditable="true"
@@ -583,7 +583,7 @@
style="display:none;position:fixed;z-index:1100;padding:7px 12px;font-size:13px;font-weight:600;
color:#fff;background:var(--accent,#2383E2);border:none;border-radius:8px;cursor:pointer;
box-shadow:0 4px 16px rgba(0,0,0,.35);" title="Comment on selection"
@click="window.E && window.E.commentOnSelection()">
@click="edCall('commentOnSelection')">
💬 Comment
</button>
@@ -820,7 +820,7 @@
<div style="padding:24px;text-align:center;color:var(--text-dim);">No pages link here</div>
</template>
<template x-for="b in backlinksList" :key="b.id">
<a :href="'/pages/' + b.id" style="display:block;padding:12px 16px;border-bottom:1px solid var(--border);text-decoration:none;color:inherit;" @click.prevent="backlinksOpen=false;window.location.href='/pages/'+b.id">
<a :href="'/pages/' + b.id" style="display:block;padding:12px 16px;border-bottom:1px solid var(--border);text-decoration:none;color:inherit;" @click.prevent="openBacklink(b)">
<div style="font-weight:500;color:var(--text-primary);" x-text="b.title"></div>
<div style="font-size:12px;color:var(--text-dim);margin-top:2px;" x-text="b.workspace || ''"></div>
</a>
@@ -856,7 +856,7 @@
<template x-if="importFile">
<div style="margin-top:8px;display:flex;align-items:center;gap:12px;color:var(--text-dim);font-size:13px;">
<span x-text="importFile.name"></span>
<span x-text="Math.round(importFile.size/1024)+' KB'"></span>
<span x-text="fmtImportSize(importFile)"></span>
</div>
</template>
<div style="display:flex;gap:8px;margin-top:10px;">
+1 -1
View File
@@ -305,7 +305,7 @@
</div>
</div>
<div class="fd-agent-msg-content" x-show="m.generating" x-cloak>Génération…</div>
<div class="fd-agent-msg-content fd-agent-msg-markdown" x-show="!m.generating && m.role==='assistant' && m.content" x-html="renderMarkdown(m.content)"></div>
<div class="fd-agent-msg-content fd-agent-msg-markdown" x-show="!m.generating && m.role==='assistant' && m.content" x-init="bindMarkdown($el, m)"></div>
<div class="fd-agent-msg-content" x-show="!m.generating && m.role!=='assistant' && m.content" x-text="m.content"></div>
<template x-if="m.proposal">
<div class="fd-proposal-bar">
+13
View File
@@ -1574,6 +1574,19 @@
bindProjectIcon(el, p) {
Alpine.effect(() => { el.innerHTML = getSvgIcon(p.icon || 'folder', 20); });
},
// ── A20 ph3 : délégués du topbar éditeur (window.E hors portée CSP,
// scope du topbar = appState) — voir right_actions de page_editor ──
edCall(name) {
if (window.E && typeof window.E[name] === 'function') window.E[name]();
},
edTimeAgo() { return (window.E && window.E.timeAgo) || ''; },
edCommentCount() {
return (window.E && window.E.commentCount > 0) ? window.E.commentCount : '';
},
edShared() { return !!(window.E && window.E.pageIsShared); },
// les 2 branches du ternaire favorited étaient identiques → rendu 1×
bindStar(el) { Alpine.effect(() => { el.innerHTML = getSvgIcon('star', 14); }); },
bindAgentIcon(el, a) {
Alpine.effect(() => { el.innerHTML = a.icon || '🤖'; });
},
+3 -3
View File
@@ -36,7 +36,7 @@
</style>
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/gitea_workspace.js?v={{ asset_version }}"></script>
<div class="gw-main" x-data="giteaWorkspace">
<div class="gw-main" x-data="giteaWorkspace()">
<!-- File view -->
<div x-show="showFile && !showEditor" x-transition>
<div class="gw-file-toolbar">
@@ -120,7 +120,7 @@
@click="item.type==='folder' ? drillDown(item.path) : openFile(item.path, item.sha)"
@contextmenu.prevent="openGwContext($event, item)"
style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;">
<span x-html="item.type==='folder' ? getSvgIcon('folder',16) : getSvgIcon('file',16)" style="font-size:16px;"></span>
<span x-init="bindGwIcon($el, item)" style="font-size:16px;"></span>
<span x-text="item.name" style="flex:1;font-size:14px;" :style="{ fontWeight: item.type==='folder' ? '500' : '400' }"></span>
<span x-text="item.type==='folder' ? '' : formatSize(item.size)" style="font-size:12px;color:var(--text-tertiary);"></span>
</div>
@@ -165,7 +165,7 @@
@click="openPrivate(pp.id)">
<div>
<div style="font-weight:500;" x-text="pp.title"></div>
<div style="font-size:12px;color:var(--text-dim);" x-text="pp.updated_at ? new Date(pp.updated_at+'Z').toLocaleString() : ''"></div>
<div style="font-size:12px;color:var(--text-dim);" x-text="fmtGwDate(pp)"></div>
</div>
<button class="btn btn-danger" style="padding:4px 8px;font-size:11px;" @click.stop="deletePrivate(pp.id)">{{ fd_icon('trash',14) }}</button>
</div>
+1 -1
View File
@@ -3,7 +3,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %}
{% set page_icon = "file" if page.content_format != 'file' else "paperclip" %}
{% set page_title = page.title %}
{% set nav_page_id = page.id %}
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn" @click="window.E && window.E.toggleComments()" title="Comments"><span class="fd-comment-btn-ico">💬</span><span class="fd-comment-count" x-text="window.E && window.E.commentCount>0 ? window.E.commentCount : \'\'"></span></button><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()"><span x-show="!window.E || !window.E.pageIsShared">' ~ fd_icon("lock",14) ~ ' Share ▾</span><span x-show="window.E && window.E.pageIsShared" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="edCall(\'toggleActivityOpen\')">Edited <span x-text="edTimeAgo()"></span> ▾</span><button class="topbar-btn" @click="edCall(\'toggleComments\')" title="Comments"><span class="fd-comment-btn-ico">💬</span><span class="fd-comment-count" x-text="edCommentCount()"></span></button><button class="topbar-btn share-btn" @click="edCall(\'toggleShareOpen\')"><span x-show="!edShared()">' ~ fd_icon("lock",14) ~ ' Share ▾</span><span x-show="edShared()" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="edCall(\'copyPageLink\')" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="edCall(\'toggleFavorite\')" x-init="bindStar($el)"></button><button class="topbar-btn relative" @click="edCall(\'toggleMoreOpen\')">⋯</button>' %}
{% include '_header.html' %}
{% endblock %} {% block content %}
{% include "_page_editor_content.html" %}
+1 -1
View File
@@ -2,7 +2,7 @@
page_title %}{{ page.title }}{% endblock %} {% block topbar %}
{% set page_icon = "file" %}
{% set page_title = page.title %}
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="window.E && window.E.toggleActivityOpen()">Edited <span x-text="window.E && window.E.timeAgo || \'\'"></span> ▾</span><button class="topbar-btn share-btn" @click="window.E && window.E.toggleShareOpen()"><span x-show="!window.E || !window.E.pageIsShared">' ~ fd_icon("lock",14) ~ ' Share ▾</span><span x-show="window.E && window.E.pageIsShared" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="window.E && window.E.copyPageLink()" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="window.E && window.E.toggleFavorite()" x-html="(window.E && window.E.favorited) ? getSvgIcon(\'star\',14) : getSvgIcon(\'star\',14)"></button><button class="topbar-btn relative" @click="window.E && window.E.toggleMoreOpen()">⋯</button>' %}
{% set right_actions = '<span class="topbar-edited" style="cursor:pointer;" @click="edCall(\'toggleActivityOpen\')">Edited <span x-text="edTimeAgo()"></span> ▾</span><button class="topbar-btn share-btn" @click="edCall(\'toggleShareOpen\')"><span x-show="!edShared()">' ~ fd_icon("lock",14) ~ ' Share ▾</span><span x-show="edShared()" title="This page is shared">👥 Shared ▾</span></button><button class="topbar-btn" @click="edCall(\'copyPageLink\')" title="Copy link">' ~ fd_icon("link",14) ~ '</button><button class="topbar-btn star-btn" @click="edCall(\'toggleFavorite\')" x-init="bindStar($el)"></button><button class="topbar-btn relative" @click="edCall(\'toggleMoreOpen\')">⋯</button>' %}
{% include '_header.html' %}
{% endblock %} {% block content %}
{% include "_database_table.html" %}