diff --git a/app.js b/app.js index 06e4d12..54c063a 100644 --- a/app.js +++ b/app.js @@ -3,6 +3,8 @@ const path = require('path'); const indexRoutes = require('./routes/index'); const apiRoutes = require('./routes/api'); +const templatesRoutes = require('./routes/templates'); +const exportRoutes = require('./routes/export'); const app = express(); const port = process.env.PORT || 3000; @@ -12,8 +14,17 @@ app.use(express.urlencoded({ extended: true })); app.use('/assets', express.static(path.join(__dirname, 'assets'))); +// Créer le dossier data s'il n'existe pas +const fs = require('fs'); +const dataDir = path.join(__dirname, 'data'); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir); +} + app.use('/', indexRoutes); app.use('/api', apiRoutes); +app.use('/api/templates', templatesRoutes); +app.use('/api/export', exportRoutes); app.listen(port, () => { console.log(`Server running on port ${port}`); diff --git a/assets/css/style.css b/assets/css/style.css index e69de29..84d1a4f 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -0,0 +1,998 @@ +/* Variables CSS */ +:root { + --primary-color: #2c3e50; + --secondary-color: #3498db; + --accent-color: #e74c3c; + --success-color: #27ae60; + --warning-color: #f39c12; + --background-color: #ecf0f1; + --surface-color: #ffffff; + --text-color: #2c3e50; + --text-light: #7f8c8d; + --border-color: #bdc3c7; + --shadow: 0 2px 4px rgba(0,0,0,0.1); + --shadow-hover: 0 4px 8px rgba(0,0,0,0.15); + --border-radius: 8px; + --transition: all 0.3s ease; +} + +/* Mode sombre */ +body.dark-theme { + --primary-color: #ecf0f1; + --secondary-color: #3498db; + --background-color: #1a1a1a; + --surface-color: #2c2c2c; + --text-color: #ecf0f1; + --text-light: #95a5a6; + --border-color: #34495e; +} + +/* Reset et base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--background-color); + transition: var(--transition); +} + +/* Layout principal */ +.container { + max-width: 1400px; + margin: 0 auto; + padding: 0 20px; +} + +/* Header professionnel */ +header { + background: var(--surface-color); + border-bottom: 1px solid var(--border-color); + box-shadow: var(--shadow); + position: sticky; + top: 0; + z-index: 100; +} + +header > div { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + max-width: 1400px; + margin: 0 auto; +} + +header h1 { + color: var(--primary-color); + font-size: 2.2rem; + font-weight: 600; + margin: 0; + display: flex; + align-items: center; + gap: 0.75rem; + text-align: center; + letter-spacing: -0.5px; +} + +header h1::before { + content: "📝"; + font-size: 2.4rem; +} + +/* Panneaux latéraux rétractables */ +.side-panel { + position: fixed; + top: 50%; + transform: translateY(-50%); + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + box-shadow: var(--shadow-hover); + z-index: 90; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + backdrop-filter: blur(10px); +} + +/* Panneau gauche - Configuration */ +.left-panel { + left: -320px; + width: 320px; + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.left-panel.open { + transform: translateY(-50%) translateX(320px); +} + +/* Panneau droit - Navigation */ +.right-panel { + right: -320px; + width: 320px; + border-right: none; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.right-panel.open { + transform: translateY(-50%) translateX(-320px); +} + +/* Contenu des panneaux */ +.panel-content { + padding: 2rem; +} + +.panel-header { + margin: 0 0 1.5rem 0; + color: var(--primary-color); + font-size: 1.2rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.75rem; + border-bottom: 2px solid var(--border-color); + padding-bottom: 1rem; +} + +/* Flèches d'ouverture */ +.panel-toggle { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 40px; + height: 80px; + background: var(--surface-color); + border: 1px solid var(--border-color); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + color: var(--text-color); + transition: all 0.3s ease; + box-shadow: var(--shadow); + z-index: 91; +} + +.left-panel .panel-toggle { + right: -40px; + border-left: none; + border-radius: 0 var(--border-radius) var(--border-radius) 0; +} + +.right-panel .panel-toggle { + left: -40px; + border-right: none; + border-radius: var(--border-radius) 0 0 var(--border-radius); +} + +.panel-toggle:hover { + background: var(--secondary-color); + color: white; + transform: translateY(-50%) scale(1.05); + box-shadow: var(--shadow-hover); +} + +.panel-toggle:active { + transform: translateY(-50%) scale(0.95); +} + +/* Rotation de la flèche selon l'état */ +.left-panel .panel-toggle .arrow { + transition: transform 0.3s ease; +} + +.left-panel.open .panel-toggle .arrow { + transform: rotate(180deg); +} + +.right-panel .panel-toggle .arrow { + transition: transform 0.3s ease; +} + +.right-panel.open .panel-toggle .arrow { + transform: rotate(180deg); +} + +/* Styles pour le formulaire de templates */ +.template-form { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.form-group label { + font-size: 0.9rem; + color: var(--text-light); + font-weight: 500; +} + +.form-group select { + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--surface-color); + color: var(--text-color); + font-size: 0.9rem; + cursor: pointer; + transition: var(--transition); +} + +.form-group select:hover:not(:disabled) { + border-color: var(--secondary-color); +} + +.form-group select:focus { + outline: none; + border-color: var(--secondary-color); + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +.form-group select:disabled { + opacity: 0.5; + cursor: not-allowed; + background: var(--background-color); +} + +.template-preview { + padding: 1rem; + background: var(--background-color); + border-radius: var(--border-radius); + border: 1px solid var(--border-color); +} + +.template-preview h4 { + margin: 0 0 0.75rem 0; + color: var(--primary-color); + font-size: 0.95rem; +} + +.preview-content { + font-size: 0.85rem; + color: var(--text-light); + line-height: 1.4; +} + +.form-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.form-actions button { + flex: 1; + min-width: 120px; +} + +/* Styles spécifiques au panneau de navigation */ +.nav-section { + margin-bottom: 2rem; +} + +.nav-section h4 { + margin: 0 0 1rem 0; + font-size: 0.95rem; + color: var(--text-light); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.nav-buttons { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.nav-btn { + width: 100%; + padding: 1rem 1.5rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: transparent; + color: var(--text-color); + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 1rem; + font-size: 0.9rem; + text-align: left; +} + +.nav-btn:hover { + background: var(--background-color); + border-color: var(--secondary-color); + color: var(--secondary-color); + transform: translateX(5px); +} + +.nav-btn .icon { + font-size: 1.2rem; + width: 24px; + text-align: center; +} + +/* File input dans le panneau */ +.panel-file-input { + width: 100%; + padding: 1rem 1.5rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: transparent; + color: var(--text-color); + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 1rem; + font-size: 0.9rem; + text-align: left; + overflow: hidden; +} + +.panel-file-input:hover { + background: var(--background-color); + border-color: var(--warning-color); + color: var(--warning-color); + transform: translateX(5px); +} + +.panel-file-input .icon { + font-size: 1.2rem; + width: 24px; + text-align: center; +} + +/* Animation d'overlay quand les panneaux sont ouverts */ +.panel-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.1); + z-index: 89; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + backdrop-filter: blur(2px); +} + +.panel-overlay.active { + opacity: 1; + visibility: visible; +} + +#skeleton-select { + flex: 1; + padding: 0.75rem 1rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + background: var(--surface-color); + color: var(--text-color); + font-size: 0.9rem; + cursor: pointer; + transition: var(--transition); +} + +#skeleton-select:hover { + border-color: var(--secondary-color); +} + +#skeleton-select:focus { + outline: none; + border-color: var(--secondary-color); + box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); +} + +/* Responsive design */ +@media (max-width: 768px) { + .side-panel { + width: 280px; + top: 60%; + } + + .left-panel { + left: -280px; + } + + .left-panel.open { + transform: translateY(-60%) translateX(280px); + } + + .right-panel { + right: -280px; + } + + .right-panel.open { + transform: translateY(-60%) translateX(-280px); + } + + .panel-content { + padding: 1.5rem; + } + + .panel-toggle { + width: 35px; + height: 70px; + font-size: 1rem; + } + + header h1 { + font-size: 1.8rem; + } + + header h1::before { + font-size: 2rem; + } + + header > div { + padding: 1.5rem; + } + + main { + grid-template-columns: 1fr; + padding: 1rem; + } + + #table-of-contents, + #ai-assistant { + position: static; + max-height: none; + } +} + +@media (max-width: 480px) { + .side-panel { + width: calc(100vw - 40px); + left: -100vw; + right: -100vw; + } + + .left-panel.open { + transform: translateY(-60%) translateX(calc(100vw - 20px)); + } + + .right-panel.open { + transform: translateY(-60%) translateX(calc(-100vw + 20px)); + } +} + +/* Boutons */ +button, .btn { + padding: 0.6rem 1.2rem; + border: none; + border-radius: 25px; + background: var(--secondary-color); + color: white; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +button:hover, .btn:hover { + background: #2980b9; + box-shadow: 0 4px 12px rgba(0,0,0,0.2); + transform: translateY(-2px); +} + +button:active, .btn:active { + transform: translateY(0); +} + +/* Boutons dans le header */ +header button, header .btn { + background: rgba(255,255,255,0.9); + color: var(--primary-color); + border: 1px solid rgba(255,255,255,0.3); + backdrop-filter: blur(10px); +} + +header button:hover, header .btn:hover { + background: white; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + color: var(--secondary-color); +} + +button.secondary { + background: rgba(255,255,255,0.9); + color: var(--primary-color); + border: 1px solid rgba(255,255,255,0.3); +} + +button.secondary:hover { + background: white; + color: var(--secondary-color); +} + +button.danger { + background: var(--accent-color); +} + +button.danger:hover { + background: #c0392b; +} + +button.success { + background: var(--success-color); +} + +button.success:hover { + background: #229954; +} + +/* Input file personnalis� */ +.file-input-label { + padding: 0.6rem 1.2rem; + border-radius: var(--border-radius); + background: var(--warning-color); + color: white; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: var(--transition); + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +.file-input-label:hover { + background: #e67e22; + box-shadow: var(--shadow-hover); + transform: translateY(-1px); +} + +/* Main content */ +main { + display: grid; + grid-template-columns: 300px 1fr 350px; + gap: 2rem; + padding: 2rem; + max-width: 1400px; + margin: 0 auto; + min-height: calc(100vh - 140px); +} + +@media (max-width: 1200px) { + main { + grid-template-columns: 250px 1fr; + gap: 1.5rem; + } + + #ai-assistant { + grid-column: 1 / -1; + margin-top: 2rem; + } +} + +@media (max-width: 768px) { + main { + grid-template-columns: 1fr; + gap: 1rem; + padding: 1rem; + } +} + +/* Sections */ +section { + background: var(--surface-color); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + overflow: hidden; +} + +section h2 { + background: var(--secondary-color); + color: white; + padding: 1rem 1.5rem; + margin: 0; + font-size: 1.2rem; + font-weight: 600; +} + +/* Table des mati�res */ +#table-of-contents { + position: sticky; + top: 100px; + max-height: calc(100vh - 120px); + overflow-y: auto; +} + +#toc-nav { + padding: 1.5rem; +} + +#toc-nav ul { + list-style: none; +} + +#toc-nav li { + margin-bottom: 0.5rem; +} + +#toc-nav a { + color: var(--text-color); + text-decoration: none; + padding: 0.3rem 0.5rem; + border-radius: 4px; + display: block; + transition: var(--transition); + font-size: 0.9rem; +} + +#toc-nav a:hover { + background: var(--background-color); + color: var(--secondary-color); +} + +#toc-nav ul ul { + margin-left: 1rem; + margin-top: 0.5rem; +} + +#toc-nav ul ul a { + font-size: 0.85rem; + color: var(--text-light); +} + +/* Zone d'�criture */ +#design-journal { + min-height: 600px; +} + +#journal-editor { + min-height: 500px; + padding: 2rem; + font-size: 1rem; + line-height: 1.8; + outline: none; + border: none; + background: var(--surface-color); + color: var(--text-color); + resize: vertical; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; +} + +#journal-editor:focus { + background: var(--background-color); +} + +#journal-editor:empty::before { + content: "Commencez à écrire votre journal de conception...\A\A# Titre de votre projet\A\A## Contexte et objectifs\A\ADécrivez ici le contexte de votre projet...\A\A## Architecture\A\A### Composants principaux\A\A### Technologies utilisées\A\A## Décisions de conception\A\A### Décision 1\A\A**Problème :**\A**Options considérées :**\A**Décision :**\A**Justification :**\A\A## Prochaines étapes\A\A- [ ] Tâche 1\A- [ ] Tâche 2"; + color: var(--text-light); + font-style: italic; + white-space: pre-wrap; + pointer-events: none; +} + +#journal-editor p { + margin-bottom: 1rem; +} + +#journal-editor h1, #journal-editor h2, #journal-editor h3 { + color: var(--primary-color); + margin: 1.5rem 0 1rem 0; +} + +#journal-editor h1 { + font-size: 2rem; + border-bottom: 3px solid var(--secondary-color); + padding-bottom: 0.5rem; +} + +#journal-editor h2 { + font-size: 1.6rem; + border-bottom: 2px solid var(--border-color); + padding-bottom: 0.3rem; +} + +#journal-editor h3 { + font-size: 1.3rem; + color: var(--secondary-color); +} + +/* Assistant IA */ +#ai-assistant { + position: sticky; + top: 100px; + max-height: calc(100vh - 120px); + display: flex; + flex-direction: column; +} + +/* Styles pour "Coming Soon" */ +.ai-coming-soon { + padding: 1.5rem; + background: linear-gradient(135deg, var(--background-color), var(--surface-color)); +} + +.coming-soon-badge { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: linear-gradient(135deg, var(--warning-color), #f39c12); + color: white; + padding: 0.5rem 1rem; + border-radius: 50px; + font-size: 0.85rem; + font-weight: 600; + margin-bottom: 1.5rem; + box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3); + animation: glow 2s ease-in-out infinite alternate; +} + +@keyframes glow { + from { box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3); } + to { box-shadow: 0 4px 16px rgba(243, 156, 18, 0.5); } +} + +.badge-icon { + font-size: 1rem; + animation: bounce 1s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-2px); } +} + +.ai-preview h3 { + color: var(--primary-color); + font-size: 1rem; + margin: 0 0 1rem 0; + font-weight: 600; +} + +.ai-features-list { + list-style: none; + padding: 0; + margin: 0 0 1.5rem 0; +} + +.ai-features-list li { + padding: 0.75rem 0; + border-bottom: 1px solid var(--border-color); + font-size: 0.9rem; + line-height: 1.4; + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +.ai-features-list li:last-child { + border-bottom: none; +} + +.ai-tech-stack { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1rem; + margin-top: 1rem; +} + +.ai-tech-stack p { + margin: 0.25rem 0; + font-size: 0.85rem; + color: var(--text-light); +} + +.ai-tech-stack strong { + color: var(--primary-color); +} + +#ai-assistant-feedback { + flex: 1; + padding: 1.5rem; + border-top: 1px solid var(--border-color); + background: var(--background-color); + overflow-y: auto; + min-height: 200px; +} + +.feedback-message { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 1rem; + margin-bottom: 1rem; + font-size: 0.9rem; + line-height: 1.5; +} + +.coming-soon-message { + background: linear-gradient(135deg, var(--surface-color), var(--background-color)); + border: 2px dashed var(--warning-color); + text-align: center; +} + +.feedback-message.loading { + background: var(--background-color); + border-color: var(--secondary-color); + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +/* Footer */ +footer { + background: var(--secondary-color); + color: white; + text-align: center; + padding: 2rem; + margin-top: 3rem; +} + +footer a { + color: white; + text-decoration: underline; +} + +footer a:hover { + color: var(--secondary-color); +} + +/* �tats de chargement */ +.loading { + position: relative; + opacity: 0.7; + pointer-events: none; +} + +.loading::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid var(--border-color); + border-top: 2px solid var(--secondary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Notifications */ +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 1rem 1.5rem; + background: var(--success-color); + color: white; + border-radius: var(--border-radius); + box-shadow: var(--shadow-hover); + z-index: 1000; + transform: translateX(400px); + transition: var(--transition); +} + +.notification.show { + transform: translateX(0); +} + +.notification.error { + background: var(--accent-color); +} + +.notification.warning { + background: var(--warning-color); +} + +/* Responsive design */ +@media (max-width: 768px) { + header > div { + flex-direction: column; + gap: 1rem; + } + + .header-controls { + flex-wrap: wrap; + justify-content: center; + } + + nav { + flex-wrap: wrap; + justify-content: center; + } + + button, .btn { + padding: 0.5rem 1rem; + font-size: 0.85rem; + } +} + +/* Scrollbars personnalis�es */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--background-color); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-light); +} + +/* S�lection de texte */ +::selection { + background: var(--secondary-color); + color: white; +} + +/* Focus outline */ +button:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid var(--secondary-color); + outline-offset: 2px; +} + +/* Utilitaires */ +.hidden { + display: none !important; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.text-center { + text-align: center; +} + +.mb-1 { margin-bottom: 0.5rem; } +.mb-2 { margin-bottom: 1rem; } +.mb-3 { margin-bottom: 1.5rem; } + +.mt-1 { margin-top: 0.5rem; } +.mt-2 { margin-top: 1rem; } +.mt-3 { margin-top: 1.5rem; } \ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..21f47c9 --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,583 @@ +// Application principale +class ConceptionAssistant { + constructor() { + this.currentJournalId = null; + this.editor = null; + this.autoSaveTimer = null; + this.init(); + } + + async init() { + this.setupEditor(); + this.setupEventListeners(); + this.setupAutoSave(); + await this.loadJournalList(); + this.generateTOC(); + } + + setupEditor() { + this.editor = document.getElementById('journal-editor'); + + // Générer TOC à chaque modification + this.editor.addEventListener('input', () => { + this.generateTOC(); + this.resetAutoSave(); + }); + + // Gestion des raccourcis clavier + this.editor.addEventListener('keydown', (e) => { + // Ctrl+S pour sauvegarder + if (e.ctrlKey && e.key === 's') { + e.preventDefault(); + this.saveJournal(); + } + + // Tab pour indentation + if (e.key === 'Tab') { + e.preventDefault(); + document.execCommand('insertText', false, ' '); + } + }); + } + + showEditorPlaceholder() { + // Le placeholder est maintenant géré via CSS ::before + // Cette fonction peut être supprimée ou laissée vide pour compatibilité + } + + setupEventListeners() { + // Boutons de contrôle des journaux + document.getElementById('save-journal')?.addEventListener('click', () => this.saveJournal()); + document.getElementById('load-journal')?.addEventListener('click', () => this.showJournalSelector()); + + // Table des matières + document.getElementById('refresh-toc')?.addEventListener('click', () => this.generateTOC()); + + // Export/Import + document.getElementById('export-md')?.addEventListener('click', () => this.exportMarkdown()); + document.getElementById('export-pdf')?.addEventListener('click', () => this.exportPDF()); + document.getElementById('import-md')?.addEventListener('change', (e) => this.importMarkdown(e)); + + // Thème + document.getElementById('theme-toggle')?.addEventListener('click', () => this.toggleTheme()); + + // Assistant IA (simulé pour le MVP) + document.getElementById('activate-rephrase')?.addEventListener('click', () => this.handleAI('rephrase')); + document.getElementById('check-inconsistencies')?.addEventListener('click', () => this.handleAI('inconsistencies')); + document.getElementById('check-duplications')?.addEventListener('click', () => this.handleAI('duplications')); + document.getElementById('give-advice')?.addEventListener('click', () => this.handleAI('advice')); + document.getElementById('liberty-mode')?.addEventListener('click', () => this.handleAI('liberty')); + } + + setupAutoSave() { + // Sauvegarde automatique toutes les 30 secondes + setInterval(() => { + if (this.currentJournalId && this.editor.innerText.trim()) { + this.saveJournal(true); + } + }, 30000); + } + + resetAutoSave() { + if (this.autoSaveTimer) { + clearTimeout(this.autoSaveTimer); + } + + this.autoSaveTimer = setTimeout(() => { + if (this.currentJournalId && this.editor.innerText.trim()) { + this.saveJournal(true); + } + }, 3000); + } + + async saveJournal(isAutoSave = false) { + const content = this.editor.innerText; + if (!content) return; + + const statusEl = document.getElementById('save-status'); + const saveBtn = document.getElementById('save-journal'); + + if (!isAutoSave) { + saveBtn.classList.add('loading'); + statusEl.textContent = 'Sauvegarde...'; + } + + try { + let response; + + if (this.currentJournalId) { + // Mise à jour + response = await fetch(`/api/journals/${this.currentJournalId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }) + }); + } else { + // Création + response = await fetch('/api/journals', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content }) + }); + } + + const result = await response.json(); + + if (result.success) { + if (!this.currentJournalId) { + this.currentJournalId = result.data.id; + } + + if (!isAutoSave) { + statusEl.textContent = '✅ Sauvegardé'; + this.showNotification('Journal sauvegardé avec succès', 'success'); + setTimeout(() => statusEl.textContent = '', 3000); + } else { + statusEl.textContent = '💾 Auto-sauvegardé'; + setTimeout(() => statusEl.textContent = '', 2000); + } + } else { + throw new Error(result.error || 'Erreur de sauvegarde'); + } + } catch (error) { + console.error('Erreur:', error); + statusEl.textContent = '❌ Erreur'; + if (!isAutoSave) { + this.showNotification('Erreur lors de la sauvegarde: ' + error.message, 'error'); + } + setTimeout(() => statusEl.textContent = '', 3000); + } finally { + if (!isAutoSave) { + saveBtn.classList.remove('loading'); + } + } + } + + async loadJournalList() { + try { + const response = await fetch('/api/journals'); + const result = await response.json(); + + if (result.success && result.data.length > 0) { + // Charger le dernier journal automatiquement + const lastJournal = result.data[result.data.length - 1]; + await this.loadJournal(lastJournal.id); + } + } catch (error) { + console.error('Erreur chargement liste:', error); + } + } + + async loadJournal(id) { + try { + const response = await fetch(`/api/journals/${id}`); + const result = await response.json(); + + if (result.success && result.data.length > 0) { + const journal = result.data[0]; + this.currentJournalId = id; + this.editor.innerText = journal.markdownContent; + this.generateTOC(); + this.showNotification('Journal chargé', 'success'); + } + } catch (error) { + console.error('Erreur chargement journal:', error); + this.showNotification('Erreur lors du chargement', 'error'); + } + } + + async showJournalSelector() { + try { + const response = await fetch('/api/journals'); + const result = await response.json(); + + if (result.success) { + const journalList = result.data.map(journal => { + const preview = journal.markdownContent.substring(0, 100).replace(/\n/g, ' '); + return ``; + }).join(''); + + const feedback = document.getElementById('ai-assistant-feedback'); + feedback.innerHTML = ` +
+

📂 Sélectionner un journal

+
+ + ${journalList} +
+
+ `; + + // Ajouter les event listeners + document.querySelectorAll('.journal-item').forEach(btn => { + btn.addEventListener('click', () => { + this.loadJournal(btn.dataset.id); + this.clearFeedback(); + }); + }); + } + } catch (error) { + console.error('Erreur:', error); + this.showNotification('Erreur lors du chargement de la liste', 'error'); + } + } + + createNewJournal() { + this.currentJournalId = null; + this.editor.innerText = ''; + this.generateTOC(); + this.clearFeedback(); + this.showNotification('Nouveau journal créé', 'success'); + } + + generateTOC() { + const content = this.editor.innerText; + const lines = content.split('\n'); + const toc = []; + let tocHtml = ''; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('#')) { + const level = line.match(/^#+/)[0].length; + const title = line.replace(/^#+\s*/, ''); + const id = 'heading-' + i; + + toc.push({ level, title, id }); + } + } + + if (toc.length > 0) { + tocHtml = ''; + } else { + tocHtml = '

Ajoutez des titres (# ## ###) à votre journal pour générer la table des matières.

'; + } + + document.getElementById('toc-nav').innerHTML = tocHtml; + } + + scrollToHeading(headingId) { + // Simulation du scroll vers les titres + this.showNotification('Navigation vers la section', 'success'); + } + + exportMarkdown() { + const content = this.editor.innerText; + if (!content.trim()) { + this.showNotification('Aucun contenu à exporter', 'warning'); + return; + } + + const blob = new Blob([content], { type: 'text/markdown' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'journal-conception.md'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + this.showNotification('Fichier Markdown exporté', 'success'); + } + + exportPDF() { + // Simulation export PDF (nécessite une vraie implémentation côté serveur) + this.showNotification('Export PDF : fonctionnalité à venir', 'warning'); + } + + importMarkdown(event) { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + this.editor.innerText = e.target.result; + this.generateTOC(); + this.currentJournalId = null; // Nouveau journal + this.showNotification('Fichier Markdown importé', 'success'); + }; + reader.readAsText(file); + } + + toggleTheme() { + document.body.classList.toggle('dark-theme'); + const isDark = document.body.classList.contains('dark-theme'); + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + this.showNotification(`Mode ${isDark ? 'sombre' : 'clair'} activé`, 'success'); + } + + handleAI(action) { + const selection = window.getSelection().toString(); + let message = ''; + + switch (action) { + case 'rephrase': + if (!selection) { + message = '⚠️ Sélectionnez du texte à reformuler'; + } else { + message = `✨ Reformulation suggérée pour:
"${selection.substring(0, 100)}..."

Version reformulée :
"${this.simulateRephrase(selection)}"`; + } + break; + case 'inconsistencies': + message = '🔍 Analyse des incohérences...

✅ Aucune incohérence majeure détectée dans le document.'; + break; + case 'duplications': + message = '📋 Vérification des duplications...

✅ Aucune duplication significative trouvée.'; + break; + case 'advice': + message = '💡 Conseils pour améliorer votre journal :

• Ajoutez plus de détails sur les alternatives considérées
• Documentez les raisons des choix techniques
• Incluez des diagrammes ou schémas
• Ajoutez une section "Lessons learned"'; + break; + case 'liberty': + const count = document.getElementById('liberty-repeat-count').value || 3; + message = `🚀 Mode Liberté activé (${count} itérations)

Génération de contenus automatiques basée sur le contexte existant...

Cette fonctionnalité nécessite une intégration IA complète.`; + break; + } + + this.showAIFeedback(message); + } + + simulateRephrase(text) { + // Simulation simple de reformulation + const replacements = { + 'nous devons': 'il convient de', + 'c\'est important': 'cela revêt une importance', + 'il faut': 'il est nécessaire de', + 'très': 'particulièrement', + 'beaucoup': 'considérablement' + }; + + let result = text; + Object.entries(replacements).forEach(([from, to]) => { + result = result.replace(new RegExp(from, 'gi'), to); + }); + + return result || text; + } + + showAIFeedback(message) { + const feedback = document.getElementById('ai-assistant-feedback'); + feedback.innerHTML = `
${message}
`; + } + + clearFeedback() { + const feedback = document.getElementById('ai-assistant-feedback'); + feedback.innerHTML = ` +
+ 🎯 Assistant prêt
+ Sélectionnez du texte dans l'éditeur et cliquez sur une action pour commencer. +
+ `; + } + + showNotification(message, type = 'success') { + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + notification.textContent = message; + + document.body.appendChild(notification); + + setTimeout(() => notification.classList.add('show'), 100); + setTimeout(() => { + notification.classList.remove('show'); + setTimeout(() => document.body.removeChild(notification), 300); + }, 3000); + } +} + +// Gestion des panneaux latéraux +function togglePanel(side) { + const panel = document.getElementById(`${side}-panel`); + const overlay = document.getElementById('panel-overlay'); + const otherPanel = document.getElementById(side === 'left' ? 'right-panel' : 'left-panel'); + + // Fermer l'autre panneau s'il est ouvert + if (otherPanel && otherPanel.classList.contains('open')) { + otherPanel.classList.remove('open'); + } + + // Toggle le panneau actuel + if (panel.classList.contains('open')) { + panel.classList.remove('open'); + overlay.classList.remove('active'); + } else { + panel.classList.add('open'); + overlay.classList.add('active'); + } +} + +function closeAllPanels() { + const leftPanel = document.getElementById('left-panel'); + const rightPanel = document.getElementById('right-panel'); + const overlay = document.getElementById('panel-overlay'); + + if (leftPanel) leftPanel.classList.remove('open'); + if (rightPanel) rightPanel.classList.remove('open'); + if (overlay) overlay.classList.remove('active'); +} + +// Fermer les panneaux avec Escape +document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + closeAllPanels(); + } +}); + +// Initialisation de l'application +let app; +document.addEventListener('DOMContentLoaded', () => { + app = new ConceptionAssistant(); + + // Charger le thème sauvegardé + const savedTheme = localStorage.getItem('theme'); + if (savedTheme === 'dark') { + document.body.classList.add('dark-theme'); + } + + // Initialiser les panneaux + initializePanels(); +}); + +function initializePanels() { + // Initialiser la gestion des templates + initializeTemplateForm(); +} + +function initializeTemplateForm() { + const domainSelect = document.getElementById('domain-select'); + const levelSelect = document.getElementById('level-select'); + const loadTemplateBtn = document.getElementById('load-template'); + const resetTemplateBtn = document.getElementById('reset-template'); + const templatePreview = document.getElementById('template-preview'); + const previewContent = document.getElementById('preview-content'); + + // Gestion du changement de domaine + if (domainSelect) { + domainSelect.addEventListener('change', async () => { + const domain = domainSelect.value; + + if (!domain) { + levelSelect.disabled = true; + levelSelect.innerHTML = ''; + loadTemplateBtn.disabled = true; + templatePreview.style.display = 'none'; + return; + } + + // Activer le select de niveau + levelSelect.disabled = false; + levelSelect.innerHTML = ` + + + + + `; + + // Cacher l'aperçu + templatePreview.style.display = 'none'; + loadTemplateBtn.disabled = true; + }); + } + + // Gestion du changement de niveau + if (levelSelect) { + levelSelect.addEventListener('change', async () => { + const domain = domainSelect.value; + const level = levelSelect.value; + + if (!domain || !level) { + templatePreview.style.display = 'none'; + loadTemplateBtn.disabled = true; + return; + } + + try { + // Charger l'aperçu du template + const response = await fetch(`/api/templates/${domain}/${level}`); + const result = await response.json(); + + if (result.success) { + // Afficher un aperçu (premières lignes) + const lines = result.data.content.split('\n'); + const preview = lines.slice(0, 8).join('\n'); + + previewContent.textContent = preview + (lines.length > 8 ? '\n...' : ''); + templatePreview.style.display = 'block'; + loadTemplateBtn.disabled = false; + } else { + app.showNotification('Erreur lors du chargement de l\'aperçu', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + app.showNotification('Erreur lors du chargement de l\'aperçu', 'error'); + } + }); + } + + // Gestion du chargement du template + if (loadTemplateBtn) { + loadTemplateBtn.addEventListener('click', async () => { + const domain = domainSelect.value; + const level = levelSelect.value; + + if (!domain || !level) { + app.showNotification('Veuillez sélectionner un domaine et un niveau', 'warning'); + return; + } + + try { + loadTemplateBtn.classList.add('loading'); + + const response = await fetch(`/api/templates/${domain}/${level}`); + const result = await response.json(); + + if (result.success) { + // Charger le template dans l'éditeur + app.editor.innerText = result.data.content; + app.generateTOC(); + app.currentJournalId = null; // Nouveau journal + + app.showNotification(`Template ${domain}/${level} chargé avec succès`, 'success'); + closeAllPanels(); + } else { + app.showNotification('Erreur lors du chargement du template', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + app.showNotification('Erreur lors du chargement du template', 'error'); + } finally { + loadTemplateBtn.classList.remove('loading'); + } + }); + } + + // Gestion du reset + if (resetTemplateBtn) { + resetTemplateBtn.addEventListener('click', () => { + domainSelect.value = ''; + levelSelect.value = ''; + levelSelect.disabled = true; + levelSelect.innerHTML = ''; + templatePreview.style.display = 'none'; + loadTemplateBtn.disabled = true; + }); + } +} \ No newline at end of file diff --git a/config/.env.example b/config/.env.example index ab6c4f1..d87c8e8 100644 --- a/config/.env.example +++ b/config/.env.example @@ -1,2 +1,21 @@ # Server Configuration -PORT=3000 \ No newline at end of file +PORT=3000 + +# Mistral AI Configuration +MISTRAL_API_KEY=your_mistral_api_key_here +MISTRAL_MODEL=mistral-large-latest +MISTRAL_BASE_URL=https://api.mistral.ai/v1 + +# AI Features Configuration +AI_ENABLED=false +AI_MAX_TOKENS=4000 +AI_TEMPERATURE=0.7 +AI_TOP_P=0.95 + +# Rate Limiting for AI +AI_RATE_LIMIT_REQUESTS=10 +AI_RATE_LIMIT_WINDOW=60000 + +# PDF Export Configuration +PDF_MAX_SIZE=10485760 +PDF_TIMEOUT=30000 \ No newline at end of file diff --git a/package.json b/package.json index 4808722..90f08b5 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "test": "node tests/all.test.js" }, "dependencies": { - "uuid": "^13.0.0", - "express": "^4.18.2" + "express": "^4.18.2", + "puppeteer": "^24.22.3", + "uuid": "^13.0.0" } -} \ No newline at end of file +} diff --git a/routes/api.js b/routes/api.js index d80d072..efc2b66 100644 --- a/routes/api.js +++ b/routes/api.js @@ -4,6 +4,9 @@ const fs = require('fs'); const path = require('path'); const { v4: uuidv4 } = require('uuid'); +// Importer le module d'export +const exportRouter = require('./export'); + function modifMd(id, modifications) { if (id === undefined) throw new Error('id obligatoire'); if (!Array.isArray(modifications) || modifications.length === 0) throw new Error('modifications requises'); @@ -101,7 +104,7 @@ function readMd(id = undefined) { } } -function modifMd(id, newMarkdownContent) { +function updateMd(id, newMarkdownContent) { if (id === undefined) throw new Error('id obligatoire'); const dataDir = path.resolve(__dirname, '../data'); const mapPath = path.join(dataDir, 'uuid_map.json'); @@ -112,7 +115,7 @@ function modifMd(id, newMarkdownContent) { if (!uuid) throw new Error(`Aucun fichier trouvé pour l'id ${id}`); const mdPath = path.join(dataDir, `${uuid}.md`); - if (!fs.existsSync(mdPath)) throw new Error('Le fichier markdown n’existe pas'); + if (!fs.existsSync(mdPath)) throw new Error('Le fichier markdown n\'existe pas'); fs.writeFileSync(mdPath, newMarkdownContent, { encoding: 'utf8', flag: 'w' }); return { id, uuid, path: mdPath, newMarkdownContent }; } @@ -166,12 +169,33 @@ router.get('/journals/:id', (req, res) => { // PUT /api/journals/:id - Mettre à jour un journal router.put('/journals/:id', (req, res) => { const { id } = req.params; - const { start, end, content } = req.body; + const { content, modifications } = req.body; - res.json({ - success: true, - data: modifMd(id, {start, end, content}) - }); + try { + let result; + if (content) { + // Mise à jour complète du contenu + result = updateMd(id, content); + } else if (modifications) { + // Modifications partielles (pour compatibilité future) + result = modifMd(id, modifications); + } else { + return res.status(400).json({ + success: false, + error: 'Content ou modifications requis' + }); + } + + res.json({ + success: true, + data: result + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } }); // DELETE /api/journals/:id - Supprimer un journal @@ -184,4 +208,7 @@ router.delete('/journals/:id', (req, res) => { }); }); +// Intégrer les routes d'export +router.use('/export', exportRouter); + module.exports = router; \ No newline at end of file diff --git a/routes/export.js b/routes/export.js new file mode 100644 index 0000000..065060e --- /dev/null +++ b/routes/export.js @@ -0,0 +1,512 @@ +const express = require('express'); +const router = express.Router(); +const puppeteer = require('puppeteer'); +const path = require('path'); + +// POST /api/export/pdf - Générer un PDF depuis le contenu markdown +router.post('/pdf', async (req, res) => { + const { content, title = 'Journal de Conception' } = req.body; + + if (!content || content.trim() === '') { + return res.status(400).json({ + success: false, + error: 'Contenu requis pour générer le PDF' + }); + } + + let browser; + try { + // Convertir le markdown en HTML avec styles + const htmlContent = generateStyledHTML(content, title); + + // Lancer Puppeteer + browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }); + + const page = await browser.newPage(); + + // Définir le contenu HTML + await page.setContent(htmlContent, { + waitUntil: 'domcontentloaded', + timeout: parseInt(process.env.PDF_TIMEOUT) || 30000 + }); + + // Attendre que Mermaid soit chargé et les diagrammes rendus + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Vérifier si des diagrammes Mermaid existent et attendre qu'ils se chargent + try { + await page.waitForFunction(() => { + const mermaidElements = document.querySelectorAll('.mermaid'); + if (mermaidElements.length === 0) return true; + return Array.from(mermaidElements).every(el => + el.querySelector('svg') || el.textContent.trim() === '' || el.classList.contains('error') + ); + }, { timeout: 10000 }); + } catch (e) { + console.log('Timeout waiting for Mermaid, proceeding...'); + } + + // Générer le PDF + const pdfBuffer = await page.pdf({ + format: 'A4', + margin: { + top: '2cm', + right: '2cm', + bottom: '2cm', + left: '2cm' + }, + printBackground: true, + displayHeaderFooter: true, + headerTemplate: ` +
+ ${title} +
+ `, + footerTemplate: ` +
+ Page sur - Généré le ${new Date().toLocaleDateString('fr-FR')} +
+ ` + }); + + await browser.close(); + + // Vérifier la taille du PDF + const maxSize = parseInt(process.env.PDF_MAX_SIZE) || 10485760; // 10MB par défaut + if (pdfBuffer.length > maxSize) { + return res.status(413).json({ + success: false, + error: 'Le PDF généré dépasse la taille maximale autorisée' + }); + } + + // Envoyer le PDF + res.setHeader('Content-Type', 'application/pdf'); + res.setHeader('Content-Disposition', `attachment; filename="${sanitizeFilename(title)}.pdf"`); + res.setHeader('Content-Length', pdfBuffer.length); + + res.send(pdfBuffer); + + } catch (error) { + console.error('Erreur génération PDF:', error); + + if (browser) { + await browser.close(); + } + + res.status(500).json({ + success: false, + error: 'Erreur lors de la génération du PDF' + }); + } +}); + +// Fonction utilitaire pour échapper le HTML +function escapeHtml(text) { + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); +} + +// Fonction pour convertir le markdown en HTML stylé avec support Mermaid +function generateStyledHTML(content, title) { + let html = content; + + // Traitement des diagrammes Mermaid AVANT les blocs de code + html = html.replace(/```mermaid\n([\s\S]*?)```/g, '
$1
'); + + // Traitement des blocs de code avec langage + html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { + const language = lang ? ` class="language-${lang}"` : ''; + return `
${escapeHtml(code)}
`; + }); + + // Code en ligne + html = html.replace(/`([^`]+)`/g, '$1'); + + // Traitement des tableaux amélioré + html = html.replace(/(\|.*\|\s*\n)+/g, (match) => { + const lines = match.trim().split('\n').filter(line => line.trim()); + let tableHtml = '\n'; + let inHeader = true; + + lines.forEach((line, index) => { + // Ignorer les lignes de séparation + if (line.match(/^\s*\|[\s\-:]*\|\s*$/)) { + inHeader = false; + return; + } + + const cells = line.split('|') + .map(cell => cell.trim()) + .filter((cell, idx, arr) => idx > 0 && idx < arr.length - 1); + + if (cells.length === 0) return; + + const tag = inHeader ? 'th' : 'td'; + tableHtml += ' \n'; + cells.forEach(cell => { + tableHtml += ` <${tag}>${cell}\n`; + }); + tableHtml += ' \n'; + + if (inHeader && index === 0) inHeader = false; + }); + + tableHtml += '
\n'; + return tableHtml; + }); + + // Conversion markdown vers HTML + html = html + // Titres (en ordre décroissant) + .replace(/^#{6}\s+(.*$)/gm, '
$1
') + .replace(/^#{5}\s+(.*$)/gm, '
$1
') + .replace(/^#{4}\s+(.*$)/gm, '

$1

') + .replace(/^#{3}\s+(.*$)/gm, '

$1

') + .replace(/^#{2}\s+(.*$)/gm, '

$1

') + .replace(/^#{1}\s+(.*$)/gm, '

$1

') + // Citations + .replace(/^>\s+(.*$)/gm, '
$1
') + // Gras et italique (ordre important) + .replace(/\*\*\*(.*?)\*\*\*/g, '$1') + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + // Barré + .replace(/~~(.*?)~~/g, '$1') + // Listes numérotées + .replace(/^(\s*)(\d+)\. (.*$)/gm, '
  • $3
  • ') + // Listes à puces et tâches + .replace(/^(\s*)- \[x\] (.*$)/gm, '
  • ✅ $2
  • ') + .replace(/^(\s*)- \[ \] (.*$)/gm, '
  • ☐ $2
  • ') + .replace(/^(\s*)[*\-+] (.*$)/gm, '
  • $2
  • ') + // Liens + .replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1') + // Ligne horizontale + .replace(/^---+$/gm, '
    '); + + // Grouper les listes consécutives + html = html.replace(/(]*>.*?<\/li>\s*)+/gs, (match) => { + if (match.includes('class="numbered"')) { + return '
      ' + match.replace(/ class="numbered"/g, '') + '
    '; + } else { + return ''; + } + }); + + // Nettoyer les listes multiples consécutives + html = html.replace(/(<\/[uo]l>\s*<[uo]l>)/g, ''); + + // Grouper les citations consécutives + html = html.replace(/(
    .*?<\/blockquote>\s*)+/gs, (match) => { + const content = match.replace(/<\/?blockquote>/g, ''); + return '
    ' + content + '
    '; + }); + + // Traiter les paragraphes + html = html.split('\n\n').map(paragraph => { + paragraph = paragraph.trim(); + if (!paragraph) return ''; + + // Ne pas entourer les éléments de bloc dans des paragraphes + if (paragraph.match(/^<(h[1-6]|div|table|ul|ol|blockquote|hr|pre)/)) { + return paragraph; + } + + return '

    ' + paragraph.replace(/\n/g, '
    ') + '

    '; + }).join('\n\n'); + + const styledHTML = ` + + + + + + ${title} + + + + + + ${html} + + + `; + + return styledHTML; +} + +// Fonction pour nettoyer le nom de fichier +function sanitizeFilename(filename) { + return filename + .replace(/[^\w\s-]/g, '') // Supprimer caractères spéciaux + .replace(/\s+/g, '-') // Remplacer espaces par tirets + .toLowerCase() // Minuscules + .substring(0, 100); // Limiter la longueur +} + +module.exports = router; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 460e0c2..0bf3466 100644 --- a/routes/index.js +++ b/routes/index.js @@ -9,6 +9,9 @@ router.get('/', (req, res) => { // Route à propos router.get('/about', (req, res) => { + const { getHeader } = require('../views/header'); + const { getFooter } = require('../views/footer'); + res.send(` @@ -19,31 +22,60 @@ router.get('/about', (req, res) => { -

    Journal de Conception

    -

    - Cette application aide les équipes à réaliser un suivi structuré et collaboratif de la conception de leurs projets. Elle permet d’archiver les étapes clés, d’assurer la traçabilité des décisions, et de simplifier la coordination. -

    -

    Historique

    -

    - Ce projet est né du besoin de centraliser et d’organiser les notes de conception lors du développement de projets techniques. -

    -

    Équipe

    - -

    Contact

    -

    - Email : augustin.j.l.roux@gmail.com
    - Dépôt Git : https://gitea.legion-muyue.fr/Muyue/conception-assistant -

    -
    -

    - © 2025 Journal de Conception. -

    -
    + ${getHeader()} + +
    +
    +

    À propos de l'application

    +
    +

    + Cette application aide les équipes à réaliser un suivi structuré et collaboratif de la conception de leurs projets. Elle permet d'archiver les étapes clés, d'assurer la traçabilité des décisions, et de simplifier la coordination. +

    +
    +
    + +
    +

    Historique

    +
    +

    + Ce projet est né du besoin de centraliser et d'organiser les notes de conception lors du développement de projets techniques. Il offre un environnement intuitif pour documenter les décisions architecturales et suivre l'évolution des projets. +

    +
    +
    + +
    +

    Équipe

    +
    +
      +
    • + Augustin ROUX – Développeur et Concepteur principal +
    • +
    +
    +
    + +
    +

    Contact

    +
    +

    + Email : augustin.j.l.roux@gmail.com

    + Dépôt Git : https://gitea.legion-muyue.fr/Muyue/conception-assistant +

    +
    +
    + +
    + + ← Retour à l'application + +
    +
    + + ${getFooter()} + + - `); }); diff --git a/routes/templates.js b/routes/templates.js new file mode 100644 index 0000000..707978c --- /dev/null +++ b/routes/templates.js @@ -0,0 +1,117 @@ +const express = require('express'); +const router = express.Router(); +const fs = require('fs'); +const path = require('path'); + +// GET /api/templates/:domain/:level - Récupérer un template spécifique +router.get('/:domain/:level', (req, res) => { + const { domain, level } = req.params; + + try { + const templatePath = path.join(__dirname, '../templates', domain, `${level}.md`); + + if (!fs.existsSync(templatePath)) { + return res.status(404).json({ + success: false, + error: `Template non trouvé : ${domain}/${level}` + }); + } + + const templateContent = fs.readFileSync(templatePath, 'utf8'); + + res.json({ + success: true, + data: { + domain, + level, + content: templateContent + } + }); + } catch (error) { + console.error('Erreur lors de la lecture du template:', error); + res.status(500).json({ + success: false, + error: 'Erreur serveur lors de la lecture du template' + }); + } +}); + +// GET /api/templates/:domain - Lister les niveaux disponibles pour un domaine +router.get('/:domain', (req, res) => { + const { domain } = req.params; + + try { + const domainPath = path.join(__dirname, '../templates', domain); + + if (!fs.existsSync(domainPath)) { + return res.status(404).json({ + success: false, + error: `Domaine non trouvé : ${domain}` + }); + } + + const files = fs.readdirSync(domainPath); + const levels = files + .filter(file => file.endsWith('.md')) + .map(file => file.replace('.md', '')); + + res.json({ + success: true, + data: { + domain, + levels + } + }); + } catch (error) { + console.error('Erreur lors de la lecture du domaine:', error); + res.status(500).json({ + success: false, + error: 'Erreur serveur lors de la lecture du domaine' + }); + } +}); + +// GET /api/templates - Lister tous les domaines disponibles +router.get('/', (req, res) => { + try { + const templatesPath = path.join(__dirname, '../templates'); + + if (!fs.existsSync(templatesPath)) { + return res.json({ + success: true, + data: [] + }); + } + + const domains = fs.readdirSync(templatesPath, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); + + // Pour chaque domaine, récupérer les niveaux disponibles + const domainsWithLevels = domains.map(domain => { + const domainPath = path.join(templatesPath, domain); + const files = fs.readdirSync(domainPath); + const levels = files + .filter(file => file.endsWith('.md')) + .map(file => file.replace('.md', '')); + + return { + domain, + levels + }; + }); + + res.json({ + success: true, + data: domainsWithLevels + }); + } catch (error) { + console.error('Erreur lors de la lecture des templates:', error); + res.status(500).json({ + success: false, + error: 'Erreur serveur lors de la lecture des templates' + }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/templates/business/simple.md b/templates/business/simple.md new file mode 100644 index 0000000..f645781 --- /dev/null +++ b/templates/business/simple.md @@ -0,0 +1,97 @@ +# Projet Business + +## 1. Présentation du projet + +### Contexte +[Description du contexte économique/business] + +### Opportunité +[Opportunité de marché identifiée] + +### Proposition de valeur +[En quoi votre solution est unique] + +## 2. Analyse de marché + +### Marché cible +- Segment principal : +- Taille du marché : +- Tendances : + +### Concurrence +- **Concurrent 1** : [Forces/Faiblesses] +- **Concurrent 2** : [Forces/Faiblesses] + +### Positionnement +[Comment vous vous différenciez] + +## 3. Modèle économique + +### Sources de revenus +- Revenue stream 1 : +- Revenue stream 2 : + +### Structure de coûts +- Coûts fixes : +- Coûts variables : + +### Pricing +[Stratégie de tarification] + +## 4. Stratégie + +### Objectifs +- Court terme (6 mois) : +- Moyen terme (18 mois) : +- Long terme (3 ans) : + +### Stratégie marketing +- Canaux d'acquisition : +- Message marketing : + +### Stratégie opérationnelle +- Ressources clés : +- Partenaires : + +## 5. Planning + +### Phase 1 : Lancement (3 mois) +- [ ] Développement MVP +- [ ] Tests utilisateurs +- [ ] Go-to-market + +### Phase 2 : Croissance (6 mois) +- [ ] Acquisition clients +- [ ] Amélioration produit +- [ ] Expansion équipe + +### Phase 3 : Scale (12 mois) +- [ ] Expansion géographique +- [ ] Nouveaux produits +- [ ] Levée de fonds + +## 6. Risques et opportunités + +### Risques +- Risque 1 : +- Risque 2 : + +### Opportunités +- Opportunité 1 : +- Opportunité 2 : + +## 7. Métriques clés + +### KPIs business +- Chiffre d'affaires : +- Nombre de clients : +- Coût d'acquisition : + +### KPIs produit +- Utilisateurs actifs : +- Rétention : +- NPS : + +## 8. Notes et actions + +[Espace pour suivi des actions et notes] \ No newline at end of file diff --git a/templates/design/simple.md b/templates/design/simple.md new file mode 100644 index 0000000..87527b6 --- /dev/null +++ b/templates/design/simple.md @@ -0,0 +1,96 @@ +# Projet Design + +## 1. Brief créatif + +### Contexte du projet +[Description du projet et du client] + +### Objectifs +- Objectif principal : +- Cible : +- Message à transmettre : + +### Contraintes +- Budget : +- Délais : +- Contraintes techniques : + +## 2. Recherche et inspiration + +### Analyse de la concurrence +[Étude des solutions existantes] + +### Mood board +[Sources d'inspiration visuelles] + +### Références +- Style graphique : +- Palette couleur : +- Typographie : + +## 3. Concept créatif + +### Idée directrice +[Concept principal du design] + +### Parti pris esthétique +- Style : [Moderne, classique, minimaliste...] +- Ambiance : [Chaleureuse, professionnelle, fun...] +- Tonalité : [Sérieuse, décalée, premium...] + +## 4. Éléments graphiques + +### Palette de couleurs +- Couleur principale : #XXXXXX +- Couleur secondaire : #XXXXXX +- Couleurs d'accent : #XXXXXX, #XXXXXX + +### Typographie +- Titre : [Police] +- Texte : [Police] +- Accent : [Police] + +### Iconographie +- Style d'icônes : +- Illustrations : +- Photos : + +## 5. Déclinaisons + +### Supports à créer +- [ ] Logo et identité +- [ ] Site web +- [ ] Print +- [ ] Réseaux sociaux +- [ ] Packaging + +### Guidelines +[Règles d'utilisation des éléments graphiques] + +## 6. Validation et itérations + +### Feedbacks client +[Retours et ajustements demandés] + +### Tests utilisateurs +[Si applicable - tests d'utilisabilité] + +## 7. Livrables + +### Fichiers sources +- [ ] Fichiers AI/PSD +- [ ] Fichiers vectoriels +- [ ] Polices + +### Exports +- [ ] PNG/JPG haute définition +- [ ] Versions web +- [ ] Versions print + +### Documentation +- [ ] Guide de style +- [ ] Spécifications techniques + +## 8. Notes créatives + +[Espace pour notes et idées créatives] \ No newline at end of file diff --git a/templates/informatique/complet.md b/templates/informatique/complet.md new file mode 100644 index 0000000..6984d65 --- /dev/null +++ b/templates/informatique/complet.md @@ -0,0 +1,1122 @@ +# Projet Informatique - Documentation Complète + +## 1. Contexte et Vision + +### 1.1 Contexte organisationnel +[Description de l'organisation, du département, de l'équipe] + +### 1.2 Problématique métier +[Analyse détaillée des problèmes business à résoudre] + +### 1.3 Vision produit +[Vision long-terme du produit et de son évolution] + +### 1.4 Alignement stratégique +[Comment le projet s'inscrit dans la stratégie de l'entreprise] + +## 2. Analyse des besoins approfondie + +### 2.1 Parties prenantes +| Partie prenante | Rôle | Influence | Besoins | Contact | +|-----------------|------|-----------|---------|---------| +| Product Owner | | Élevée | | | +| Tech Lead | | Élevée | | | +| Utilisateurs finaux | | Moyenne | | | + +### 2.2 User Stories détaillées +```gherkin +Feature: Authentification utilisateur + En tant qu'utilisateur + Je veux pouvoir me connecter de manière sécurisée + Afin d'accéder à mes données personnelles + + Scenario: Connexion réussie + Given je suis sur la page de connexion + When je saisis mes identifiants corrects + Then je suis redirigé vers le dashboard + And je vois mon nom d'utilisateur affiché + + Scenario: Connexion échouée + Given je suis sur la page de connexion + When je saisis des identifiants incorrects + Then je vois un message d'erreur + And je reste sur la page de connexion +``` + +### 2.3 Acceptance Criteria détaillés +[Critères d'acceptation pour chaque user story] + +### 2.4 Contraintes détaillées +#### Contraintes techniques +- **Compatibilité** : IE11+, Chrome 80+, Firefox 75+, Safari 13+ +- **Performance** : Temps de chargement < 2s, Core Web Vitals > 90 +- **Accessibilité** : Conformité WCAG 2.1 AA +- **SEO** : Structure sémantique, balises meta, sitemap + +#### Contraintes légales +- **RGPD** : Consentement, droit à l'oubli, portabilité +- **Sécurité** : ISO 27001, chiffrement bout-en-bout +- **Audit** : Traçabilité des actions, journalisation + +## 3. Architecture complète + +### 3.1 Architecture métier +[Domain-Driven Design, bounded contexts] + +### 3.2 Architecture applicative +```mermaid +graph TB + A[Load Balancer] --> B[API Gateway] + B --> C[Auth Service] + B --> D[User Service] + B --> E[Content Service] + C --> F[(Auth DB)] + D --> G[(User DB)] + E --> H[(Content DB)] + E --> I[File Storage] + + J[Frontend SPA] --> B + K[Mobile App] --> B + L[Admin Panel] --> B +``` + +### 3.3 Architecture technique détaillée +#### Microservices +```yaml +services: + auth-service: + image: auth-service:latest + environment: + - JWT_SECRET=${JWT_SECRET} + - DB_CONNECTION=${AUTH_DB_URL} + dependencies: + - postgres-auth + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + interval: 30s + timeout: 10s + retries: 3 + + user-service: + image: user-service:latest + environment: + - DB_CONNECTION=${USER_DB_URL} + dependencies: + - postgres-users + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3002/health"] +``` + +### 3.4 Modèle de données complet +```sql +-- Système de permissions +CREATE TABLE roles ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE permissions ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + resource VARCHAR(100) NOT NULL, + action VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE role_permissions ( + role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE, + permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE, + PRIMARY KEY (role_id, permission_id) +); + +-- Audit trail +CREATE TABLE audit_logs ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + action VARCHAR(100) NOT NULL, + resource_type VARCHAR(100) NOT NULL, + resource_id VARCHAR(100), + old_values JSONB, + new_values JSONB, + ip_address INET, + user_agent TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Système de notifications +CREATE TABLE notifications ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + title VARCHAR(255) NOT NULL, + message TEXT, + read_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +## 4. Conception détaillée par couche + +### 4.1 Couche présentation +#### Design System complet +```scss +// Tokens de design +$colors: ( + primary: ( + 50: #f0f9ff, + 100: #e0f2fe, + 500: #0ea5e9, + 900: #0c4a6e + ), + semantic: ( + success: #10b981, + warning: #f59e0b, + error: #ef4444, + info: #3b82f6 + ) +); + +$typography: ( + h1: ( + font-size: 2.25rem, + line-height: 1.2, + font-weight: 700 + ), + body: ( + font-size: 1rem, + line-height: 1.5, + font-weight: 400 + ) +); + +$spacing: ( + xs: 0.25rem, + sm: 0.5rem, + md: 1rem, + lg: 1.5rem, + xl: 3rem +); +``` + +#### Composants React avancés +```typescript +// Hook personnalisé pour la gestion d'état +export const useApiData = (url: string, options?: RequestOptions) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + const response = await apiClient.get(url, options); + setData(response.data); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [url]); + + return { data, loading, error, refetch: fetchData }; +}; + +// Composant de tableau réutilisable +interface DataTableProps { + data: T[]; + columns: ColumnDef[]; + loading?: boolean; + onRowClick?: (row: T) => void; + pagination?: PaginationOptions; +} + +export function DataTable({ + data, + columns, + loading = false, + onRowClick, + pagination +}: DataTableProps) { + // Implémentation avec react-table +} +``` + +### 4.2 Couche métier +#### Domain Services +```typescript +// Service métier pour la gestion des utilisateurs +export class UserDomainService { + constructor( + private userRepository: IUserRepository, + private emailService: IEmailService, + private auditService: IAuditService + ) {} + + async createUser(userData: CreateUserCommand): Promise { + // Validation des règles métier + await this.validateUserCreation(userData); + + // Création de l'utilisateur + const user = await this.userRepository.create({ + ...userData, + status: UserStatus.PENDING_VERIFICATION, + createdAt: new Date() + }); + + // Envoi email de vérification + await this.emailService.sendVerificationEmail(user.email); + + // Audit + await this.auditService.log({ + action: 'USER_CREATED', + userId: user.id, + details: { email: user.email } + }); + + return user; + } + + private async validateUserCreation(userData: CreateUserCommand): Promise { + // Vérification unicité email + const existingUser = await this.userRepository.findByEmail(userData.email); + if (existingUser) { + throw new DomainError('EMAIL_ALREADY_EXISTS', 'Cet email est déjà utilisé'); + } + + // Validation format email + if (!this.isValidEmail(userData.email)) { + throw new DomainError('INVALID_EMAIL_FORMAT', 'Format email invalide'); + } + + // Validation force mot de passe + if (!this.isStrongPassword(userData.password)) { + throw new DomainError('WEAK_PASSWORD', 'Le mot de passe n\'est pas assez fort'); + } + } +} +``` + +### 4.3 Couche données +#### Repositories avec patterns avancés +```typescript +// Pattern Repository avec Unit of Work +export class UserRepository implements IUserRepository { + constructor(private db: Database) {} + + async findById(id: string): Promise { + const query = ` + SELECT u.*, r.name as role_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.id + WHERE u.id = $1 AND u.deleted_at IS NULL + `; + + const result = await this.db.query(query, [id]); + return result.rows[0] ? this.mapToUser(result.rows[0]) : null; + } + + async findWithPagination( + filters: UserFilters, + pagination: PaginationOptions + ): Promise> { + const whereClause = this.buildWhereClause(filters); + const orderClause = this.buildOrderClause(pagination.sortBy, pagination.sortOrder); + + const [dataResult, countResult] = await Promise.all([ + this.db.query(` + SELECT u.*, r.name as role_name + FROM users u + LEFT JOIN roles r ON u.role_id = r.id + ${whereClause} + ${orderClause} + LIMIT $1 OFFSET $2 + `, [pagination.limit, pagination.offset]), + + this.db.query(` + SELECT COUNT(*) as total + FROM users u + ${whereClause} + `) + ]); + + return { + data: dataResult.rows.map(this.mapToUser), + total: parseInt(countResult.rows[0].total), + page: pagination.page, + limit: pagination.limit + }; + } +} +``` + +## 5. Qualité et tests approfondis + +### 5.1 Stratégie de tests complète +```typescript +// Tests unitaires avec mocks +describe('UserDomainService', () => { + let userService: UserDomainService; + let mockUserRepository: jest.Mocked; + let mockEmailService: jest.Mocked; + + beforeEach(() => { + mockUserRepository = { + create: jest.fn(), + findByEmail: jest.fn(), + } as any; + + mockEmailService = { + sendVerificationEmail: jest.fn(), + } as any; + + userService = new UserDomainService( + mockUserRepository, + mockEmailService, + mockAuditService + ); + }); + + describe('createUser', () => { + it('should create user successfully', async () => { + // Arrange + const userData = { + email: 'test@example.com', + password: 'StrongPassword123!', + firstName: 'John', + lastName: 'Doe' + }; + + mockUserRepository.findByEmail.mockResolvedValue(null); + mockUserRepository.create.mockResolvedValue(mockUser); + + // Act + const result = await userService.createUser(userData); + + // Assert + expect(mockUserRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + email: userData.email, + status: UserStatus.PENDING_VERIFICATION + }) + ); + expect(mockEmailService.sendVerificationEmail).toHaveBeenCalledWith(userData.email); + }); + }); +}); +``` + +### 5.2 Tests d'intégration +```typescript +// Tests d'API avec base de données de test +describe('Users API Integration', () => { + let app: Application; + let testDb: TestDatabase; + + beforeAll(async () => { + testDb = await TestDatabase.create(); + app = createApp({ database: testDb }); + }); + + afterAll(async () => { + await testDb.cleanup(); + }); + + describe('POST /api/users', () => { + it('should create user and return 201', async () => { + // Arrange + const userData = { + email: 'test@example.com', + password: 'StrongPassword123!', + firstName: 'John', + lastName: 'Doe' + }; + + // Act + const response = await request(app) + .post('/api/users') + .send(userData) + .expect(201); + + // Assert + expect(response.body.data).toMatchObject({ + email: userData.email, + firstName: userData.firstName, + lastName: userData.lastName, + status: 'PENDING_VERIFICATION' + }); + + // Vérifier en base + const userInDb = await testDb.query( + 'SELECT * FROM users WHERE email = $1', + [userData.email] + ); + expect(userInDb.rows).toHaveLength(1); + }); + }); +}); +``` + +### 5.3 Tests E2E avec Cypress +```typescript +// Tests end-to-end +describe('User Management Flow', () => { + beforeEach(() => { + cy.resetDatabase(); + cy.seedTestData(); + }); + + it('should allow admin to create, edit and delete users', () => { + // Login as admin + cy.login('admin@example.com', 'password'); + + // Navigate to users page + cy.visit('/admin/users'); + cy.get('[data-testid="users-page"]').should('be.visible'); + + // Create new user + cy.get('[data-testid="create-user-btn"]').click(); + cy.get('[data-testid="user-form"]').should('be.visible'); + + cy.get('[data-testid="email-input"]').type('newuser@example.com'); + cy.get('[data-testid="firstname-input"]').type('New'); + cy.get('[data-testid="lastname-input"]').type('User'); + cy.get('[data-testid="role-select"]').select('User'); + + cy.get('[data-testid="submit-btn"]').click(); + + // Verify user was created + cy.get('[data-testid="success-message"]') + .should('contain', 'Utilisateur créé avec succès'); + + cy.get('[data-testid="users-table"]') + .should('contain', 'newuser@example.com'); + }); +}); +``` + +## 6. Sécurité approfondie + +### 6.1 Authentification et autorisation +```typescript +// JWT avec refresh tokens +export class AuthService { + async authenticateUser(credentials: LoginCredentials): Promise { + // Validation et authentification + const user = await this.validateCredentials(credentials); + + // Génération des tokens + const accessToken = this.generateAccessToken(user); + const refreshToken = this.generateRefreshToken(user); + + // Stockage du refresh token + await this.storeRefreshToken(user.id, refreshToken); + + // Audit de connexion + await this.auditService.logAuth(user.id, 'LOGIN_SUCCESS'); + + return { + user, + accessToken, + refreshToken, + expiresIn: this.accessTokenTTL + }; + } + + async refreshAccessToken(refreshToken: string): Promise { + // Validation du refresh token + const tokenData = await this.validateRefreshToken(refreshToken); + + // Génération nouveau access token + const newAccessToken = this.generateAccessToken(tokenData.user); + + return { + accessToken: newAccessToken, + expiresIn: this.accessTokenTTL + }; + } +} + +// Middleware d'autorisation basé sur les rôles +export const requirePermission = (resource: string, action: string) => { + return async (req: Request, res: Response, next: NextFunction) => { + const user = req.user; + + const hasPermission = await permissionService.userHasPermission( + user.id, + resource, + action + ); + + if (!hasPermission) { + return res.status(403).json({ + error: { + code: 'INSUFFICIENT_PERMISSIONS', + message: 'Permission insuffisante pour cette action' + } + }); + } + + next(); + }; +}; +``` + +### 6.2 Validation et sanitisation +```typescript +// Schémas de validation avec Joi +const userCreationSchema = Joi.object({ + email: Joi.string() + .email() + .max(255) + .required() + .messages({ + 'string.email': 'Format email invalide', + 'string.max': 'Email trop long (max 255 caractères)', + 'any.required': 'Email obligatoire' + }), + + password: Joi.string() + .min(8) + .max(128) + .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/) + .required() + .messages({ + 'string.min': 'Mot de passe trop court (min 8 caractères)', + 'string.pattern.base': 'Le mot de passe doit contenir au moins: 1 minuscule, 1 majuscule, 1 chiffre, 1 caractère spécial' + }), + + firstName: Joi.string() + .trim() + .min(1) + .max(100) + .required(), + + lastName: Joi.string() + .trim() + .min(1) + .max(100) + .required() +}); + +// Middleware de validation +export const validateRequest = (schema: Joi.ObjectSchema) => { + return (req: Request, res: Response, next: NextFunction) => { + const { error, value } = schema.validate(req.body, { + abortEarly: false, + stripUnknown: true + }); + + if (error) { + return res.status(400).json({ + error: { + code: 'VALIDATION_ERROR', + message: 'Données invalides', + details: error.details.map(detail => ({ + field: detail.path.join('.'), + message: detail.message + })) + } + }); + } + + req.body = value; + next(); + }; +}; +``` + +## 7. Performance et monitoring + +### 7.1 Optimisations performance +```typescript +// Cache Redis pour améliorer les performances +export class CachedUserService { + constructor( + private userService: UserDomainService, + private cache: RedisClient + ) {} + + async getUser(id: string): Promise { + // Tentative de récupération depuis le cache + const cacheKey = `user:${id}`; + const cachedUser = await this.cache.get(cacheKey); + + if (cachedUser) { + return JSON.parse(cachedUser); + } + + // Récupération depuis la base de données + const user = await this.userService.getUser(id); + + if (user) { + // Mise en cache pour 1 heure + await this.cache.setex(cacheKey, 3600, JSON.stringify(user)); + } + + return user; + } +} + +// Pagination et filtrage optimisés +export class UserQueryService { + async searchUsers(filters: UserSearchFilters): Promise> { + // Construction de la requête avec index appropriés + const query = this.queryBuilder + .select([ + 'u.id', + 'u.email', + 'u.first_name', + 'u.last_name', + 'r.name as role_name' + ]) + .from('users', 'u') + .leftJoin('roles', 'r', 'u.role_id = r.id') + .where('u.deleted_at IS NULL'); + + // Application des filtres + if (filters.email) { + query.andWhere('u.email ILIKE :email', { email: `%${filters.email}%` }); + } + + if (filters.role) { + query.andWhere('r.name = :role', { role: filters.role }); + } + + if (filters.status) { + query.andWhere('u.status = :status', { status: filters.status }); + } + + // Pagination + const total = await query.getCount(); + const users = await query + .offset((filters.page - 1) * filters.limit) + .limit(filters.limit) + .orderBy('u.created_at', 'DESC') + .getMany(); + + return { + data: users, + total, + page: filters.page, + limit: filters.limit, + totalPages: Math.ceil(total / filters.limit) + }; + } +} +``` + +### 7.2 Monitoring et observabilité +```typescript +// Métriques custom avec Prometheus +import { register, Counter, Histogram, Gauge } from 'prom-client'; + +export class MetricsService { + private httpRequestDuration = new Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['method', 'route', 'status_code'], + buckets: [0.1, 0.5, 1, 2, 5] + }); + + private httpRequestsTotal = new Counter({ + name: 'http_requests_total', + help: 'Total number of HTTP requests', + labelNames: ['method', 'route', 'status_code'] + }); + + private activeConnections = new Gauge({ + name: 'websocket_connections_active', + help: 'Number of active WebSocket connections' + }); + + recordHttpRequest(method: string, route: string, statusCode: number, duration: number) { + this.httpRequestsTotal.inc({ method, route, status_code: statusCode }); + this.httpRequestDuration.observe({ method, route, status_code: statusCode }, duration); + } + + incrementActiveConnections() { + this.activeConnections.inc(); + } + + decrementActiveConnections() { + this.activeConnections.dec(); + } +} + +// Middleware de monitoring +export const monitoringMiddleware = (metricsService: MetricsService) => { + return (req: Request, res: Response, next: NextFunction) => { + const start = Date.now(); + + res.on('finish', () => { + const duration = (Date.now() - start) / 1000; + metricsService.recordHttpRequest( + req.method, + req.route?.path || req.path, + res.statusCode, + duration + ); + }); + + next(); + }; +}; +``` + +## 8. DevOps et déploiement + +### 8.1 Pipeline CI/CD complet +```yaml +# .github/workflows/ci-cd.yml +name: CI/CD Pipeline + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:13 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: test_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linting + run: npm run lint + + - name: Run type checking + run: npm run type-check + + - name: Run unit tests + run: npm run test:unit + env: + CI: true + + - name: Run integration tests + run: npm run test:integration + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db + + - name: Run E2E tests + run: npm run test:e2e + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db + + - name: Generate coverage report + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + + security-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run security audit + run: npm audit --audit-level=high + + - name: Run SAST scan + uses: securecodewarrior/github-action-add-sarif@v1 + with: + sarif-file: 'security-scan.sarif' + + build: + needs: [test, security-scan] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Build Docker images + run: | + docker build -t myapp/frontend:${{ github.sha }} ./frontend + docker build -t myapp/backend:${{ github.sha }} ./backend + + - name: Push to registry + run: | + echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin + docker push myapp/frontend:${{ github.sha }} + docker push myapp/backend:${{ github.sha }} + + deploy-staging: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/develop' + steps: + - name: Deploy to staging + run: | + # Déploiement sur l'environnement de staging + kubectl set image deployment/frontend frontend=myapp/frontend:${{ github.sha }} + kubectl set image deployment/backend backend=myapp/backend:${{ github.sha }} + + deploy-production: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + environment: production + steps: + - name: Deploy to production + run: | + # Déploiement sur l'environnement de production + kubectl set image deployment/frontend frontend=myapp/frontend:${{ github.sha }} + kubectl set image deployment/backend backend=myapp/backend:${{ github.sha }} +``` + +### 8.2 Configuration Kubernetes +```yaml +# k8s/production/deployment.yml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: backend + namespace: production +spec: + replicas: 3 + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 1 + maxSurge: 1 + selector: + matchLabels: + app: backend + template: + metadata: + labels: + app: backend + spec: + containers: + - name: backend + image: myapp/backend:latest + ports: + - containerPort: 3000 + env: + - name: NODE_ENV + value: "production" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: app-secrets + key: database-url + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: app-secrets + key: jwt-secret + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 + +--- +apiVersion: v1 +kind: Service +metadata: + name: backend-service + namespace: production +spec: + selector: + app: backend + ports: + - port: 80 + targetPort: 3000 + type: ClusterIP + +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: app-ingress + namespace: production + annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/rate-limit: "100" +spec: + tls: + - hosts: + - api.myapp.com + secretName: api-tls + rules: + - host: api.myapp.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: backend-service + port: + number: 80 +``` + +## 9. Évolution et maintenance + +### 9.1 Roadmap technique +#### Q1 2025 +- [ ] Migration vers TypeScript 5.0 +- [ ] Upgrade React 18 avec Concurrent Features +- [ ] Implémentation GraphQL +- [ ] Microservices avec Service Mesh + +#### Q2 2025 +- [ ] Migration cloud-native (Kubernetes) +- [ ] Intégration IA/ML pour recommandations +- [ ] PWA avec support offline +- [ ] Monitoring avancé avec OpenTelemetry + +#### Q3 2025 +- [ ] Architecture event-driven avec Kafka +- [ ] Support multi-tenant +- [ ] API versioning complet +- [ ] Edge computing avec CDN + +### 9.2 Plan de migration +```typescript +// Migration de données avec versioning +export class MigrationService { + async migrateToV2(): Promise { + const batch = await this.getMigrationBatch(); + + for (const user of batch) { + try { + // Transformation des données + const v2User = this.transformUserToV2(user); + + // Sauvegarde avec nouvelle structure + await this.saveV2User(v2User); + + // Marquer comme migré + await this.markAsMigrated(user.id); + + } catch (error) { + await this.logMigrationError(user.id, error); + } + } + } + + private transformUserToV2(v1User: V1User): V2User { + return { + id: v1User.id, + profile: { + email: v1User.email, + firstName: v1User.firstName, + lastName: v1User.lastName, + avatar: v1User.avatar + }, + preferences: { + language: v1User.language || 'fr', + timezone: v1User.timezone || 'Europe/Paris', + notifications: { + email: v1User.emailNotifications ?? true, + push: false + } + }, + createdAt: v1User.createdAt, + updatedAt: new Date() + }; + } +} +``` + +## 10. Documentation et formation + +### 10.1 Documentation technique complète +- [ ] Architecture Decision Records (ADR) +- [ ] API Documentation (OpenAPI 3.0) +- [ ] Code documentation (JSDoc/TSDoc) +- [ ] Runbooks opérationnels +- [ ] Guide de contribution + +### 10.2 Documentation utilisateur +- [ ] Guide d'onboarding +- [ ] Manuel utilisateur complet +- [ ] FAQ interactive +- [ ] Tutoriels vidéo +- [ ] Base de connaissances + +### 10.3 Formation équipe +- [ ] Sessions de formation technique +- [ ] Code reviews guidelines +- [ ] Best practices documentation +- [ ] Mentoring plan +- [ ] Certification interne + +## 11. Mesures de succès + +### 11.1 KPIs techniques +- **Performance** : P95 response time < 500ms +- **Disponibilité** : SLA 99.9% +- **Qualité** : Code coverage > 90% +- **Sécurité** : 0 vulnérabilité critique + +### 11.2 KPIs business +- **Adoption** : 80% des utilisateurs actifs mensuels +- **Satisfaction** : NPS > 50 +- **Support** : < 2% de tickets critiques +- **ROI** : Retour sur investissement en 18 mois + +## 12. Journal détaillé du projet + +### [Date] - Kickoff projet +[Notes sur le lancement, équipe, planning] + +### [Date] - Architecture review +[Décisions architecturales, trade-offs, consensus équipe] + +### [Date] - Milestone 1 +[Réalisations, défis, ajustements nécessaires] + +### [Date] - Production deployment +[Métriques de lancement, feedback utilisateurs, actions correctives] + +## 13. Annexes techniques + +### Annexe A : Schémas de base de données complets +### Annexe B : Diagrammes d'architecture +### Annexe C : Spécifications d'API détaillées +### Annexe D : Guide de déploiement +### Annexe E : Procédures de récupération après incident \ No newline at end of file diff --git a/templates/informatique/detaille.md b/templates/informatique/detaille.md new file mode 100644 index 0000000..38a474d --- /dev/null +++ b/templates/informatique/detaille.md @@ -0,0 +1,386 @@ +# Projet Informatique - Spécifications Détaillées + +## 1. Introduction + +### 1.1 Contexte et problématique +[Description du contexte métier et des problèmes à résoudre] + +### 1.2 Objectifs du projet +- **Objectif principal** : +- **Objectifs secondaires** : +- **Critères de réussite** : + +### 1.3 Périmètre +#### Inclus dans le projet +- +- +- + +#### Exclus du projet +- +- +- + +### 1.4 Contraintes +- **Techniques** : +- **Temporelles** : +- **Budgétaires** : +- **Réglementaires** : + +## 2. Analyse des besoins + +### 2.1 Besoins fonctionnels +#### RF-001 : [Nom du besoin] +- **Description** : +- **Acteur** : +- **Priorité** : Haute/Moyenne/Basse +- **Critères d'acceptation** : + +#### RF-002 : [Nom du besoin] +- **Description** : +- **Acteur** : +- **Priorité** : +- **Critères d'acceptation** : + +### 2.2 Besoins non-fonctionnels +#### Performance +- Temps de réponse : < 2 secondes +- Charge supportée : X utilisateurs simultanés +- Disponibilité : 99.9% + +#### Sécurité +- Authentification : OAuth 2.0 +- Autorisation : RBAC +- Chiffrement : TLS 1.3 + +#### Compatibilité +- Navigateurs : Chrome, Firefox, Safari, Edge +- Appareils : Desktop, Mobile, Tablette +- OS : Windows, macOS, Linux + +## 3. Architecture système + +### 3.1 Architecture générale +[Diagramme d'architecture global] + +### 3.2 Stack technique + +#### Frontend +- **Framework** : React 18 / Vue 3 / Angular 15 +- **État** : Redux / Pinia / NgRx +- **UI** : Material-UI / Vuetify / Angular Material +- **Build** : Vite / Webpack +- **Tests** : Jest + Testing Library + +#### Backend +- **Framework** : Node.js + Express / Python + FastAPI / Java + Spring +- **API** : REST / GraphQL +- **Authentification** : JWT / OAuth2 +- **Tests** : Jest / pytest / JUnit + +#### Base de données +- **Principale** : PostgreSQL / MySQL / MongoDB +- **Cache** : Redis +- **Recherche** : Elasticsearch (si nécessaire) + +#### Infrastructure +- **Cloud** : AWS / Azure / GCP +- **Conteneurs** : Docker + Kubernetes +- **CI/CD** : GitHub Actions / GitLab CI +- **Monitoring** : Prometheus + Grafana + +### 3.3 Modèle de données + +#### Entités principales + +```sql +-- Utilisateurs +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(50) DEFAULT 'user', + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Contenu principal +CREATE TABLE content ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT, + user_id INTEGER REFERENCES users(id), + status VARCHAR(50) DEFAULT 'draft', + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### Relations +[Diagramme ERD] + +## 4. Conception détaillée + +### 4.1 Frontend - Composants + +#### Composant App +```jsx +// Structure principale + + +
    + + +