public/assets/pages/sys_data_model/sys_data_model.js
/** * sys_data_model — page « Modèle de données ». * * Parcours : Définir → Engendrer → Jouer → Importer → Lier, avec l'état réel de chaque étape. * Entités : arbre par référentiel, éditeur en tiroir, reprise depuis Standards, génération des migrations. * Migrations : fichiers engendrés, « marquer jouée », vue de compatibilité, écarts à la base. * Imports : analyse (n'écrit rien), puis intégration ou refus ; reprise des versions Standards. * Liens : déclaration, reprise de l'existant, instances des liens déclarés (tiroir). * À confirmer : les liens douteux, tranchés un par un. * Test API : les fonctions de l'API Standards, servies par sys-data-model. */ (function () { 'use strict'; const API = '/ajax/sdm'; const racine = document.querySelector('.sdm-page'); const droit = (nom) => racine.dataset[nom] === '1'; const $ = (sel, dans = document) => dans.querySelector(sel); const etat = { entites: [], referentiels: [], importables: [], types: [], roles: [], edition: null }; /** Ce qu'affiche le parcours ; null = pas encore chargé. */ const resume = { entites: false, migrations: null, imports: null, liens: null, confirmer: null }; // ── Outils ───────────────────────────────────────────────────────────── async function appel(methode, url, corps) { const options = { method: methode, headers: {} }; if (corps instanceof FormData) options.body = corps; else if (corps !== undefined) { options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(corps); } const reponse = await fetch(url, options); const json = await reponse.json().catch(() => ({ success: false, error: `Erreur ${reponse.status}` })); if (!json.success) throw new Error(json.error || `Erreur ${reponse.status}`); return json.data; } /** Crée un élément : el('td', { className: 'x' }, 'texte', enfant…). */ function el(tag, props = {}, ...enfants) { const n = document.createElement(tag); for (const [k, v] of Object.entries(props)) { if (k === 'dataset') Object.assign(n.dataset, v); else if (k in n) n[k] = v; else n.setAttribute(k, v); } for (const e of enfants.flat()) if (e != null) n.append(e instanceof Node ? e : String(e)); return n; } const ICONES = { entites: '<path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/>', migrations: '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/>', imports: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>', liens: '<path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/>', confirmer: '<path d="M22 11.1V12a10 10 0 1 1-5.9-9.1"/><path d="m22 4-10 10-3-3"/>', chercher: '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>', actualiser: '<path d="M21 12a9 9 0 1 1-2.6-6.4"/><path d="M21 3v6h-6"/>', jouer: '<path d="m6 4 14 8-14 8z"/>', annuler: '<path d="M9 14 4 9l5-5"/><path d="M4 9h11a5 5 0 0 1 0 10h-3"/>', plus: '<path d="M12 5v14M5 12h14"/>', code: '<path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/>', fichier: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>', fermer: '<path d="M18 6 6 18M6 6l12 12"/>', valider: '<path d="M20 6 9 17l-5-5"/>', definir: '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>', alerte: '<path d="M12 9v4M12 17h.01"/><path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/>', fleche: '<path d="M5 12h14M13 6l6 6-6 6"/>', monter: '<path d="m18 15-6-6-6 6"/>', descendre: '<path d="m6 9 6 6 6-6"/>', poubelle: '<path d="M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6"/>', vide: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.5 5.1 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.5-6.9A2 2 0 0 0 16.8 4H7.2a2 2 0 0 0-1.7 1.1z"/>', donnees: '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M3 15h18M9 3v18"/>', oeil: '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z"/><circle cx="12" cy="12" r="3"/>', }; /** Icône SVG en ligne (chaînes fixes ci-dessus, jamais de donnée). */ function icone(nom) { const n = el('span', { className: 'sdm-icone', 'aria-hidden': 'true' }); n.innerHTML = `<svg viewBox="0 0 24 24">${ICONES[nom] || ''}</svg>`; return n; } /** Remplace les <span data-icone="…"> du gabarit. */ function poserIcones(dans = document) { dans.querySelectorAll('[data-icone]').forEach((n) => { const i = icone(n.dataset.icone); i.className = `sdm-icone ${n.className}`.trim(); n.replaceWith(i); }); } const notifier = (type, texte) => window.x1Notification(type, texte); const libelle = (l) => (l && (l[window.getUserLang()] || l.fr || l.en)) || ''; const ETATS_ENTITE = { creee: 'créées', engendree: 'migration en attente', a_engendrer: 'à engendrer' }; const ETATS_IMPORT = { analyse: 'à valider', integre: 'intégré', refuse: 'refusé', valide: 'validé' }; const ETATS_MIGRATION = { a_jouer: 'à jouer', jouee: 'jouée' }; const ETATS_TABLE = { creee: 'créée', engendree: 'migration en attente', a_engendrer: 'à engendrer', sans_table: '—' }; const ETATS_DECISION = { propose: 'à confirmer', confirme: 'confirmé', reconduit: 'reconduit', rejete: 'rejeté', caduc: 'caduc' }; const date = (d) => (d ? new Date(d).toLocaleString() : ''); const pluriel = (n, un, plusieurs) => `${n} ${n > 1 ? plusieurs : un}`; const puceEtat = (classe, texte, title) => el('span', { className: `sdm-etat sdm-etat-${classe}`, title: title || '' }, texte); const puce = (texte, classe = '') => el('span', { className: `sdm-puce ${classe}`.trim() }, texte); function taille(octets) { if (octets < 1024) return `${octets} o`; if (octets < 1024 * 1024) return `${(octets / 1024).toFixed(0)} Ko`; return `${(octets / 1024 / 1024).toFixed(1)} Mo`; } async function occupe(bouton, fn) { bouton.disabled = true; try { await fn(); } catch (e) { notifier('alert', e.message); } finally { bouton.disabled = Boolean(bouton.dataset.motif); majBoutons(); } } /** * Fenêtre de confirmation (<dialog> natif : fond, Échap, focus piégé). * Résout true si l'utilisateur valide, false sinon. */ function demanderConfirmation({ titre, texte, liste = [], valider = 'Confirmer', danger = false }) { return new Promise((resoudre) => { const boutonValider = el('button', { type: 'submit', value: 'oui', className: `btn btn-sm ${danger ? 'btn-danger' : 'btn-primary'}` }, valider); const dialogue = el('dialog', { className: 'sdm-dialogue', 'aria-labelledby': 'sdmDialogueTitre' }, el('form', { method: 'dialog' }, el('div', { className: 'sdm-dialogue-corps' }, el('h4', { id: 'sdmDialogueTitre' }, titre), el('p', {}, texte), liste.length ? el('ul', { className: 'sdm-dialogue-liste' }, ...liste.map((x) => el('li', { className: 'sdm-code' }, x))) : null), el('div', { className: 'sdm-tiroir-pied' }, el('button', { type: 'submit', value: 'non', className: 'btn btn-outline-secondary btn-sm' }, 'Annuler'), boutonValider))); dialogue.addEventListener('close', () => { resoudre(dialogue.returnValue === 'oui'); dialogue.remove(); }); document.body.append(dialogue); dialogue.showModal(); boutonValider.focus(); }); } /** Grise un bouton qui n'a pas de sens maintenant, et dit pourquoi au survol. */ function griser(bouton, motif, titre = '') { if (!bouton) return; bouton.dataset.motif = motif || ''; bouton.disabled = Boolean(motif); bouton.title = motif ? `Indisponible : ${motif}` : titre; } /** Les boutons de la page selon l'état : `etat.gestes` vient du serveur, avec ses propres contrôles. */ function majBoutons() { const g = etat.gestes; if (!g) return; const filtre = $('#sdmFiltreRef').value; const n = filtre ? (g.aReprendre[filtre] || 0) : Object.values(g.aReprendre).reduce((a, b) => a + b, 0); griser($('#sdmBtnReprise'), n ? null : `toutes les entités de ${filtre || 'Standards'} sont déjà déclarées`, `${pluriel(n, 'entité', 'entités')} à reprendre`); griser($('#sdmBtnJouer'), g.jouer); griser($('#sdmBtnReset'), g.remettre, 'DEV : vide tout ce que sys_data_model a écrit'); griser($('#sdmBtnDiff'), g.diff); const refVue = $('#sdmRefVue').value; griser($('#sdmBtnVue'), refVue ? g.vue[refVue] : 'aucune entité déclarée'); const refImport = $('#sdmFormImport').elements.referentiel.value; const obstacle = !refImport ? 'aucun référentiel importable' : refImport in g.import ? g.import[refImport] : `import non disponible pour ${refImport}`; griser($('#sdmBtnAnalyser'), obstacle); griser($('#sdmBtnRepriseDonnees'), obstacle); griser($('#sdmBtnRepriseLiens'), g.liens); document.querySelectorAll('[data-engendrer]').forEach((b) => griser(b, g.migration[b.dataset.engendrer])); } function appliquerDroits() { document.querySelectorAll('[data-droit]').forEach((n) => { if (!droit(n.dataset.droit)) n.classList.add('d-none'); }); } /** État vide illustré. */ function vide(nom, titre, texte) { return el('div', { className: 'sdm-vide' }, icone(nom), el('strong', {}, titre), texte ? el('p', {}, texte) : null); } /** Une table : la carte si des lignes, sinon l'état vide. */ function remplirTable(carte, corps, lignes, zoneVide, videArgs) { Source:https://dev.to/mi_rak_e7b22a0ae19a6797fd/migrate-8iPreviousThe Autonomous Edge — Issue #13: Cheaper Agents, Harder Governance (Week of November 2, 2026)NextSQLite in Production: A Practical Setup with Litestream Backups
Related posts
How to Select a Chatbot API for Invoice Quality Tradeoffs — Portable Cost Analysis
0dev.to
The guard rails in my PHP download proxy: stopping server-side request forgery on ramrom.io
0dev.to PHP
HTML IN 2026
0dev.to Beginners
OTP Input Practice: Atomic Verification in a Node.js 2FA Login Flow
0dev.to Node
Why UI Tests Break Every Sprint (and What an AI Testing Agent Changes)
0dev.to Testing
The daily loop, as an installable package
0dev.to OpenSource