feat: finalisation éditeur Forge + mode hors-ligne PWA + consolidation roadmap
CI / lint (push) Failing after 7s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Failing after -7s

- Forge: Tab indent, Ctrl+K, auto-focus, AI status, messages erreur FR
- Forge: slash commands clean, cache-bust iframe, preview serveur
- Forge: iframe 82vh, hauteur auto, table inline, sélecteur langage
- PWA hors-ligne: IndexedDB (offline-db.js + offline.js)
- Mermaid: thème dark complet, highlight.js preview
- Backend: màj CSP, rename chain, bookmarks/history/shares
- Roadmap: consolidation dans docs/ROADMAP.md (75 items)
- Infra: docker-compose.podman.yml, suppression test-vault
- .gitignore: exclusion odysseus/
This commit is contained in:
2026-06-14 20:27:37 -04:00
parent 0cdebc5b40
commit feeb46b873
31 changed files with 1769 additions and 981 deletions
+82
View File
@@ -609,6 +609,30 @@ function initConfigModal() {
const saveHiddenBtn = document.getElementById("cfg-save-hidden-files");
if (saveHiddenBtn) saveHiddenBtn.addEventListener("click", saveHiddenFilesSettings);
// AI Keys
const saveAIKeysBtn = document.getElementById("cfg-save-ai-keys");
if (saveAIKeysBtn) saveAIKeysBtn.addEventListener("click", saveAIKeys);
const testAIKeysBtn = document.getElementById("cfg-test-ai-keys");
if (testAIKeysBtn) testAIKeysBtn.addEventListener("click", testAIKeys);
// Load existing keys on modal open
document.getElementById("config-open-btn").addEventListener("click", loadAIKeys);
// Config nav search
var cfgSearch = document.getElementById("config-nav-search");
if (cfgSearch) {
cfgSearch.addEventListener("input", function() {
var q = cfgSearch.value.toLowerCase();
document.querySelectorAll("#config-scroll .config-section").forEach(function(s) {
var text = s.textContent.toLowerCase();
s.style.display = (q && !text.includes(q)) ? "none" : "";
});
document.querySelectorAll("#config-nav .help-nav-link").forEach(function(a) {
var li = a.closest("li");
if (li) li.style.display = (q && !a.textContent.toLowerCase().includes(q)) ? "none" : "";
});
});
}
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeConfigModal();
@@ -1290,6 +1314,64 @@ function updateRegexPreview() {
}
// ── AI Keys management ──
async function loadAIKeys() {
try {
const data = await api("/api/config/ai-keys");
["DEEPSEEK_API_KEY","OPENROUTER_API_KEY","GEMINI_API_KEY"].forEach(k => {
const id = "cfg-" + k.toLowerCase().replace(/_api_key/g, "") + "-key";
const input = document.getElementById(id);
if (input && data[k]) input.placeholder = data[k];
});
} catch(e) {}
}
async function saveAIKeys() {
const keys = {};
const map = { "cfg-deepseek-key": "DEEPSEEK_API_KEY", "cfg-openrouter-key": "OPENROUTER_API_KEY", "cfg-gemini-key": "GEMINI_API_KEY" };
for (const [id, name] of Object.entries(map)) {
const input = document.getElementById(id);
if (input && input.value.trim()) keys[name] = input.value.trim();
}
if (!Object.keys(keys).length) { showToast("Aucune cle a sauvegarder", "warning"); return; }
try {
await api("/api/config/ai-keys", { method: "POST", body: JSON.stringify(keys) });
showToast("Cles API sauvegardees", "success");
// Clear inputs
Object.keys(map).forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; });
loadAIKeys();
} catch(e) { showToast("Erreur: " + e.message, "error"); }
}
async function testAIKeys() {
const status = document.getElementById("cfg-ai-status");
if (status) status.textContent = "Test en cours...";
try {
// Test connectivity
const results = await api("/api/config/ai-keys/test", { method: "POST" });
var msg = Object.entries(results).map(([k,v]) => k + ": " + v).join(" | ");
if (status) status.textContent = msg;
// Fetch models for configured providers
for (const p of ["deepseek","openrouter","gemini"]) {
if (results[p] === "ok") {
try {
const m = await api("/api/config/ai-models?provider=" + p);
var sel = document.getElementById("cfg-" + p + "-model");
if (sel && m.models && m.models.length) {
sel.innerHTML = '<option value="">-- Modele (' + m.models.length + ') --</option>';
m.models.forEach(function(md) {
sel.innerHTML += '<option value="' + md + '">' + md + '</option>';
});
}
} catch(e) { console.warn("Failed to fetch models for", p, e); }
}
}
} catch(e) {
if (status) status.textContent = "Erreur: " + e.message;
}
}
export {
initSidebarTabs,
initConfigModal,