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
Personalized Welcome Emails with Reports (and Fewer Glue Layers)
0dev.to
Your Next.js Site Has Four Copies of Every Page. Here Is How to Tell Google Which One Counts
0dev.to
Your age gate shouldn't compare the estimate to 18
0dev.to
I audited 100 public-sector websites in the EU and the US. The regional ranking is noise.
0dev.to
From sitemap.xml to a full audit report
0dev.to
CLAUDE.md best practices: what to include and skip, with a template
0dev.to