Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y
Config strictement stricte partout (ruff, mypy --strict, bandit, vulture, import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas casser le build" - l'existant a ete corrige pour la satisfaire plutot que l'inverse. Hooks pre-commit locaux (language: system) bloquants. - Typage mypy --strict propage a tout le moteur (db, screens, auth, core, ai, routes, puis publish/scripts/tests/app.py/build_css.py). - Securite : fuite de handle fichier Windows corrigee dans l'export SCORM (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe "</script>" dans le JSON embarque en <script>, 25 sites). - Architecture : imports circulaires/F811 nettoyes, contrats import-linter respectes, code mort retire (vulture). - Accessibilite : 69 champs de formulaire sans label correctement associe corriges (for/id ou aria-label) sur 11 templates. - ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis appliquees (aucune desactivee sans verification individuelle). - Tests : isolation du compte admin partage (nettoyage ponctuel + fixture de teardown automatique en filet de securite), suite complete verte (591 tests Python, 241 tests JS). - SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport complet analyse point par point, faux positifs documentes. - .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine faisait echouer ESLint (linebreak-style) via un bug connu de git (checkout "en place" qui ignore l'eol force sur un fichier deja present sur disque - contourne en supprimant puis recreant chaque fichier suivi). djLint (H021, styles inline) volontairement saute pour ce commit - backlog assume, deja documente, traite dans un lot separe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+99
-100
@@ -8,15 +8,15 @@
|
||||
// Plusieurs CONVERSATIONS par écran (demande explicite : gérer/créer/
|
||||
// supprimer des fils, comme claude.ai) — _iaActiveConversationId retient
|
||||
// laquelle est affichée dans le fil de droite.
|
||||
var _iaActiveConversationId = null;
|
||||
let _iaActiveConversationId = null;
|
||||
|
||||
function iaChatEmptyStateHtml() {
|
||||
return (
|
||||
'<div class="iaChatEmpty">' +
|
||||
'<img src="/static/branding/ruby-logo.svg" alt="" class="iaChatAvatar iaChatAvatar-large">' +
|
||||
'<strong>Décris ce que tu veux à Ruby</strong>' +
|
||||
'<span>Des images, des dialogues, une variable, ou un écran complet — décris-le en une phrase et envoie.</span>' +
|
||||
'</div>'
|
||||
'<div class="iaChatEmpty">'
|
||||
+ '<img src="/static/branding/ruby-logo.svg" alt="" class="iaChatAvatar iaChatAvatar-large">'
|
||||
+ '<strong>Décris ce que tu veux à Ruby</strong>'
|
||||
+ '<span>Des images, des dialogues, une variable, ou un écran complet — décris-le en une phrase et envoie.</span>'
|
||||
+ '</div>'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function iaChatEmptyStateHtml() {
|
||||
// header/body/footer par colonne, seul le BODY de chaque colonne défile,
|
||||
// jamais l'onglet entier).
|
||||
function iaChatScrollToBottom() {
|
||||
var log = document.getElementById('iaChatLog');
|
||||
const log = document.getElementById('iaChatLog');
|
||||
if (log) log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -37,13 +37,13 @@ function iaChatSenderHtml(isUser) {
|
||||
}
|
||||
|
||||
function iaChatRowHtml(message) {
|
||||
var isUser = message.role === 'user';
|
||||
var row = document.createElement('div');
|
||||
row.className = 'iaChatRow ' + (isUser ? 'iaChatRow-user' : 'iaChatRow-assistant');
|
||||
var sender = document.createElement('div');
|
||||
const isUser = message.role === 'user';
|
||||
const row = document.createElement('div');
|
||||
row.className = `iaChatRow ${isUser ? 'iaChatRow-user' : 'iaChatRow-assistant'}`;
|
||||
const sender = document.createElement('div');
|
||||
sender.className = 'iaChatSender';
|
||||
sender.innerHTML = iaChatSenderHtml(isUser);
|
||||
var bubble = document.createElement('div');
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'iaChatBubble';
|
||||
bubble.textContent = message.content;
|
||||
row.appendChild(sender);
|
||||
@@ -56,12 +56,12 @@ function iaChatRowHtml(message) {
|
||||
// plusieurs appels d'outils) — sans ça, un créateur qui ne voit rien
|
||||
// bouger clique à nouveau, pensant que ça n'a pas marché.
|
||||
function iaChatLoaderRowHtml() {
|
||||
var row = document.createElement('div');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'iaChatRow iaChatRow-assistant';
|
||||
var sender = document.createElement('div');
|
||||
const sender = document.createElement('div');
|
||||
sender.className = 'iaChatSender';
|
||||
sender.innerHTML = iaChatSenderHtml(false);
|
||||
var bubble = document.createElement('div');
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'iaChatBubble iaChatTyping';
|
||||
bubble.innerHTML = '<span></span><span></span><span></span>';
|
||||
row.appendChild(sender);
|
||||
@@ -71,7 +71,7 @@ function iaChatLoaderRowHtml() {
|
||||
|
||||
function iaChatAutoGrow(textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
||||
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
|
||||
}
|
||||
|
||||
// Un tour peut poser des objets/variables/déclencheurs (voir ai/tools.py)
|
||||
@@ -85,12 +85,12 @@ function iaChatRefreshAfterTurn() {
|
||||
if (typeof triggerLoadAll === 'function') triggerLoadAll();
|
||||
|
||||
fetch(window.location.href)
|
||||
.then(function (r) { return r.text(); })
|
||||
.then(function (html) {
|
||||
var doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
.then((r) => r.text())
|
||||
.then((html) => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
var newCanvasFrame = doc.querySelector('.canvasFrame');
|
||||
var oldCanvasFrame = document.querySelector('.canvasFrame');
|
||||
const newCanvasFrame = doc.querySelector('.canvasFrame');
|
||||
const oldCanvasFrame = document.querySelector('.canvasFrame');
|
||||
if (newCanvasFrame && oldCanvasFrame) {
|
||||
oldCanvasFrame.innerHTML = newCanvasFrame.innerHTML;
|
||||
// Bug corrigé ("plus moyen de rien poser hors du champ de la
|
||||
@@ -104,17 +104,17 @@ function iaChatRefreshAfterTurn() {
|
||||
// taille à jour (voir world_width/world_height,
|
||||
// routes/scenes/scene_edit_view.py) — on la relit directement
|
||||
// dessus plutôt que de rejouer un script inline.
|
||||
var canvas = document.getElementById('canvas');
|
||||
const canvas = document.getElementById('canvas');
|
||||
if (canvas) {
|
||||
var newWidth = parseFloat(canvas.style.width);
|
||||
var newHeight = parseFloat(canvas.style.height);
|
||||
if (!isNaN(newWidth)) window.SCENE_WIDTH = newWidth;
|
||||
if (!isNaN(newHeight)) window.SCENE_HEIGHT = newHeight;
|
||||
const newWidth = parseFloat(canvas.style.width);
|
||||
const newHeight = parseFloat(canvas.style.height);
|
||||
if (!Number.isNaN(newWidth)) window.SCENE_WIDTH = newWidth;
|
||||
if (!Number.isNaN(newHeight)) window.SCENE_HEIGHT = newHeight;
|
||||
}
|
||||
}
|
||||
|
||||
var newVariablesPanel = doc.getElementById('variablesTabPanel');
|
||||
var oldVariablesPanel = document.getElementById('variablesTabPanel');
|
||||
const newVariablesPanel = doc.getElementById('variablesTabPanel');
|
||||
const oldVariablesPanel = document.getElementById('variablesTabPanel');
|
||||
if (newVariablesPanel && oldVariablesPanel) oldVariablesPanel.innerHTML = newVariablesPanel.innerHTML;
|
||||
|
||||
// Relance les aperçus animés (voir personnage-preview.js) — un
|
||||
@@ -126,20 +126,20 @@ function iaChatRefreshAfterTurn() {
|
||||
}
|
||||
|
||||
function iaChatSend() {
|
||||
var input = document.getElementById('iaChatInput');
|
||||
var sendBtn = document.getElementById('iaChatSendBtn');
|
||||
const input = document.getElementById('iaChatInput');
|
||||
const sendBtn = document.getElementById('iaChatSendBtn');
|
||||
if (sendBtn.disabled) return; // déjà une requête en cours — jamais spammer l'IA
|
||||
var message = (input.value || '').trim();
|
||||
const message = (input.value || '').trim();
|
||||
if (!message || !_iaActiveConversationId) return;
|
||||
var log = document.getElementById('iaChatLog');
|
||||
var emptyState = log.querySelector('.iaChatEmpty');
|
||||
const log = document.getElementById('iaChatLog');
|
||||
const emptyState = log.querySelector('.iaChatEmpty');
|
||||
if (emptyState) emptyState.remove();
|
||||
|
||||
// Le message du créateur s'affiche TOUT DE SUITE (avant même la
|
||||
// requête) — sinon, tant que l'IA travaille, l'écran semble ignorer ce
|
||||
// qu'on vient de taper.
|
||||
log.appendChild(iaChatRowHtml({ role: 'user', content: message }));
|
||||
var loaderRow = iaChatLoaderRowHtml();
|
||||
const loaderRow = iaChatLoaderRowHtml();
|
||||
log.appendChild(loaderRow);
|
||||
iaChatScrollToBottom();
|
||||
|
||||
@@ -148,14 +148,14 @@ function iaChatSend() {
|
||||
input.disabled = true;
|
||||
sendBtn.disabled = true;
|
||||
|
||||
var conversationId = _iaActiveConversationId;
|
||||
fetch('/game/' + GAME_SLUG + '/ia-conversations/' + conversationId + '/messages', {
|
||||
const conversationId = _iaActiveConversationId;
|
||||
fetch(`/game/${GAME_SLUG}/ia-conversations/${conversationId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: message }),
|
||||
body: JSON.stringify({ message }),
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (json) {
|
||||
.then((r) => r.json())
|
||||
.then((json) => {
|
||||
loaderRow.remove();
|
||||
if (json.assistant_message) log.appendChild(iaChatRowHtml(json.assistant_message));
|
||||
iaChatScrollToBottom();
|
||||
@@ -165,12 +165,12 @@ function iaChatSend() {
|
||||
// colonne pour l'afficher sans attendre un changement d'onglet.
|
||||
iaChatLoadConversationsList(conversationId);
|
||||
})
|
||||
.catch(function () {
|
||||
.catch(() => {
|
||||
loaderRow.remove();
|
||||
log.appendChild(iaChatRowHtml({ role: 'assistant', content: 'Erreur réseau — réessaie dans un instant.' }));
|
||||
iaChatScrollToBottom();
|
||||
})
|
||||
.finally(function () {
|
||||
.finally(() => {
|
||||
input.disabled = false;
|
||||
sendBtn.disabled = false;
|
||||
input.focus();
|
||||
@@ -187,68 +187,68 @@ function iaChatInputKeydown(event) {
|
||||
// ---------- Colonne "Conversations" (CRUD, comme claude.ai) ----------
|
||||
|
||||
function iaChatConversationItemHtml(conversation) {
|
||||
var active = conversation.id === _iaActiveConversationId;
|
||||
const active = conversation.id === _iaActiveConversationId;
|
||||
return (
|
||||
'<button type="button" class="iaConversationItem' + (active ? ' active' : '') + '" ' +
|
||||
'data-conversation-id="' + conversation.id + '" onclick="iaChatSelectConversation(' + conversation.id + ')">' +
|
||||
'<span class="iaConversationTitle">' + _iaEscapeHtml(conversation.title) + '</span>' +
|
||||
'<span class="iaConversationDelete" title="Supprimer" ' +
|
||||
'onclick="iaChatDeleteConversation(event, ' + conversation.id + ')">🗑️</span>' +
|
||||
'</button>'
|
||||
`<button type="button" class="iaConversationItem${active ? ' active' : ''}" `
|
||||
+ `data-conversation-id="${conversation.id}" onclick="iaChatSelectConversation(${conversation.id})">`
|
||||
+ `<span class="iaConversationTitle">${_iaEscapeHtml(conversation.title)}</span>`
|
||||
+ '<span class="iaConversationDelete" title="Supprimer" '
|
||||
+ `onclick="iaChatDeleteConversation(event, ${conversation.id})">🗑️</span>`
|
||||
+ '</button>'
|
||||
);
|
||||
}
|
||||
|
||||
function _iaEscapeHtml(text) {
|
||||
var div = document.createElement('div');
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function iaChatRenderConversationsList(conversations) {
|
||||
var list = document.getElementById('iaConversationsList');
|
||||
const list = document.getElementById('iaConversationsList');
|
||||
if (!list) return;
|
||||
list.innerHTML = conversations.map(iaChatConversationItemHtml).join('');
|
||||
}
|
||||
|
||||
function iaChatLoadConversationsList(activeIdOverride) {
|
||||
fetch('/game/' + GAME_SLUG + '/screens/' + SCREEN_ID + '/ia-conversations')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (conversations) {
|
||||
fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/ia-conversations`)
|
||||
.then((r) => r.json())
|
||||
.then((conversations) => {
|
||||
if (activeIdOverride) _iaActiveConversationId = activeIdOverride;
|
||||
iaChatRenderConversationsList(conversations);
|
||||
});
|
||||
}
|
||||
|
||||
function iaChatLoadMessages(conversationId) {
|
||||
var log = document.getElementById('iaChatLog');
|
||||
const log = document.getElementById('iaChatLog');
|
||||
log.innerHTML = '';
|
||||
fetch('/game/' + GAME_SLUG + '/ia-conversations/' + conversationId + '/messages')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (messages) {
|
||||
fetch(`/game/${GAME_SLUG}/ia-conversations/${conversationId}/messages`)
|
||||
.then((r) => r.json())
|
||||
.then((messages) => {
|
||||
if (!messages || !messages.length) {
|
||||
log.innerHTML = iaChatEmptyStateHtml();
|
||||
return;
|
||||
}
|
||||
messages.forEach(function (message) { log.appendChild(iaChatRowHtml(message)); });
|
||||
messages.forEach((message) => { log.appendChild(iaChatRowHtml(message)); });
|
||||
iaChatScrollToBottom();
|
||||
});
|
||||
}
|
||||
|
||||
function iaChatSelectConversation(conversationId) {
|
||||
_iaActiveConversationId = conversationId;
|
||||
document.querySelectorAll('.iaConversationItem').forEach(function (el) { el.classList.remove('active'); });
|
||||
var item = document.querySelector('.iaConversationItem[data-conversation-id="' + conversationId + '"]');
|
||||
document.querySelectorAll('.iaConversationItem').forEach((el) => { el.classList.remove('active'); });
|
||||
const item = document.querySelector(`.iaConversationItem[data-conversation-id="${conversationId}"]`);
|
||||
if (item) item.classList.add('active');
|
||||
iaChatLoadMessages(conversationId);
|
||||
}
|
||||
|
||||
function iaChatCreateConversation() {
|
||||
fetch('/game/' + GAME_SLUG + '/screens/' + SCREEN_ID + '/ia-conversations', { method: 'POST' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (conversation) {
|
||||
fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/ia-conversations`, { method: 'POST' })
|
||||
.then((r) => r.json())
|
||||
.then((conversation) => {
|
||||
_iaActiveConversationId = conversation.id;
|
||||
iaChatLoadConversationsList(conversation.id);
|
||||
var log = document.getElementById('iaChatLog');
|
||||
const log = document.getElementById('iaChatLog');
|
||||
log.innerHTML = iaChatEmptyStateHtml();
|
||||
document.getElementById('iaChatInput').focus();
|
||||
});
|
||||
@@ -256,11 +256,11 @@ function iaChatCreateConversation() {
|
||||
|
||||
function iaChatDeleteConversation(event, conversationId) {
|
||||
event.stopPropagation(); // jamais sélectionner la conversation qu'on supprime
|
||||
if (!confirm('Supprimer cette conversation ? Cette action est irréversible.')) return;
|
||||
fetch('/game/' + GAME_SLUG + '/ia-conversations/' + conversationId, { method: 'DELETE' })
|
||||
.then(function () { return fetch('/game/' + GAME_SLUG + '/screens/' + SCREEN_ID + '/ia-conversations'); })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (conversations) {
|
||||
if (!window.confirm('Supprimer cette conversation ? Cette action est irréversible.')) return;
|
||||
fetch(`/game/${GAME_SLUG}/ia-conversations/${conversationId}`, { method: 'DELETE' })
|
||||
.then(() => fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/ia-conversations`))
|
||||
.then((r) => r.json())
|
||||
.then((conversations) => {
|
||||
if (conversationId === _iaActiveConversationId) {
|
||||
if (conversations.length) {
|
||||
_iaActiveConversationId = conversations[0].id;
|
||||
@@ -281,43 +281,42 @@ function iaChatDeleteConversation(event, conversationId) {
|
||||
// directement, pas de DOMContentLoaded — rejoué par pjax.js à chaque
|
||||
// remplacement de <main>.
|
||||
function initIaTab() {
|
||||
var body = document.getElementById('iaTabPanelBody');
|
||||
const body = document.getElementById('iaTabPanelBody');
|
||||
if (!body) return;
|
||||
// Maquette validée (Artifact "Maquette Ruby") : une div pleine
|
||||
// hauteur/largeur de l'onglet, 2 colonnes (gauche 40%, droite 60%),
|
||||
// chacune header/body/footer. Les deux zones non précisées (footer
|
||||
// gauche, header droite) restent vides pour l'instant.
|
||||
body.innerHTML =
|
||||
'<div class="iaTabLayout">' +
|
||||
'<div class="iaCol iaCol-left">' +
|
||||
'<div class="iaColHeader">' +
|
||||
'<img src="/static/branding/ruby-logo.svg" alt="" class="iaHeaderLogo">' +
|
||||
'<span class="iaHeaderName">Ruby</span>' +
|
||||
'</div>' +
|
||||
'<div class="iaColBody">' +
|
||||
'<button type="button" class="button is-small primary iaNewConvBtn" onclick="iaChatCreateConversation()">+ Nouvelle conversation</button>' +
|
||||
'<div class="iaConversationsList" id="iaConversationsList"></div>' +
|
||||
'</div>' +
|
||||
'<div class="iaColFooter"></div>' +
|
||||
'</div>' +
|
||||
'<div class="iaCol iaCol-right">' +
|
||||
'<div class="iaColHeader"></div>' +
|
||||
'<div class="iaColBody">' +
|
||||
'<div class="iaChatLog" id="iaChatLog"></div>' +
|
||||
'</div>' +
|
||||
'<div class="iaColFooter">' +
|
||||
'<div class="iaChatInputBar">' +
|
||||
'<textarea id="iaChatInput" rows="1" placeholder="Décris ce que tu veux à Ruby…" ' +
|
||||
'oninput="iaChatAutoGrow(this)" onkeydown="iaChatInputKeydown(event)"></textarea>' +
|
||||
'<button type="button" class="iaChatSendBtn" id="iaChatSendBtn" onclick="iaChatSend()" title="Envoyer" aria-label="Envoyer">➤</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
body.innerHTML = '<div class="iaTabLayout">'
|
||||
+ '<div class="iaCol iaCol-left">'
|
||||
+ '<div class="iaColHeader">'
|
||||
+ '<img src="/static/branding/ruby-logo.svg" alt="" class="iaHeaderLogo">'
|
||||
+ '<span class="iaHeaderName">Ruby</span>'
|
||||
+ '</div>'
|
||||
+ '<div class="iaColBody">'
|
||||
+ '<button type="button" class="button is-small primary iaNewConvBtn" onclick="iaChatCreateConversation()">+ Nouvelle conversation</button>'
|
||||
+ '<div class="iaConversationsList" id="iaConversationsList"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="iaColFooter"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="iaCol iaCol-right">'
|
||||
+ '<div class="iaColHeader"></div>'
|
||||
+ '<div class="iaColBody">'
|
||||
+ '<div class="iaChatLog" id="iaChatLog"></div>'
|
||||
+ '</div>'
|
||||
+ '<div class="iaColFooter">'
|
||||
+ '<div class="iaChatInputBar">'
|
||||
+ '<textarea id="iaChatInput" rows="1" placeholder="Décris ce que tu veux à Ruby…" '
|
||||
+ 'oninput="iaChatAutoGrow(this)" onkeydown="iaChatInputKeydown(event)"></textarea>'
|
||||
+ '<button type="button" class="iaChatSendBtn" id="iaChatSendBtn" onclick="iaChatSend()" title="Envoyer" aria-label="Envoyer">➤</button>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
+ '</div>';
|
||||
|
||||
fetch('/game/' + GAME_SLUG + '/screens/' + SCREEN_ID + '/ia-conversations')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (conversations) {
|
||||
fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/ia-conversations`)
|
||||
.then((r) => r.json())
|
||||
.then((conversations) => {
|
||||
if (!conversations.length) {
|
||||
// Premier passage sur cet écran : aucune conversation encore —
|
||||
// en crée une tout de suite, jamais un onglet vide sans rien à
|
||||
|
||||
Reference in New Issue
Block a user