From a189470d495f5cd15da3b9237510c7e4b6732193 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 27 Aug 2025 11:37:12 +0200 Subject: [PATCH] chore(template-sync): aligner avec 4NK_template (.cursor/.gitea/.gitea_template/scripts/ignores) --- .cursor/rules/00-foundations.mdc | 32 + .cursor/rules/10-project-structure.mdc | 72 + .cursor/rules/20-documentation.mdc | 33 + .cursor/rules/30-testing.mdc | 57 + .cursor/rules/40-dependencies-and-build.mdc | 55 + .cursor/rules/41-ssh-automation.mdc | 65 + .cursor/rules/42-template-sync.mdc | 53 + .cursor/rules/4nkrules.mdc | 156 + .cursor/rules/50-data-csv-models.mdc | 54 + .cursor/rules/60-office-docs.mdc | 41 + .cursor/rules/70-frontend-architecture.mdc | 56 + .cursor/rules/80-versioning-and-release.mdc | 53 + .cursor/rules/85-release-guard.mdc | 37 + .cursor/rules/90-gitea-and-oss.mdc | 59 + .../rules/95-triage-and-problem-solving.mdc | 53 + .cursor/rules/ruleset-index.md | 16 + .cursorignore | 24 + .gitea/ISSUE_TEMPLATE/bug_report.md | 97 + .gitea/ISSUE_TEMPLATE/feature_request.md | 156 + .gitea/PULL_REQUEST_TEMPLATE.md | 180 + .gitea/workflows/LOCAL_OVERRIDES.yml | 14 + .gitea/workflows/ci.yml | 345 + .gitea/workflows/template-sync.yml | 39 + .gitea_template/ISSUE_TEMPLATE/bug_report.md | 99 + .../ISSUE_TEMPLATE/feature_request.md | 158 + .gitea_template/PULL_REQUEST_TEMPLATE.md | 183 + .gitea_template/workflows/LOCAL_OVERRIDES.yml | 14 + .gitea_template/workflows/ci.yml | 346 + .gitea_template/workflows/template-sync.yml | 39 + .gitignore | 39 + AGENTS.md | 263 + CHANGELOG.md | 14 + CODE_OF_CONDUCT.md | 10 + CONTRIBUTING.md | 23 + LICENSE | 22 + docs/ARCHITECTURE.md | 27 + docs/INDEX.md | 6 + docs/INTEGRATION.md | 20 + docs/TESTING.md | 12 + ihm/account-component-DbdHSqFJ.mjs | 6758 +++++++++ ihm/assets/4nk_image.png | Bin 0 -> 62845 bytes ihm/assets/4nk_revoke.jpg | Bin 0 -> 68608 bytes ihm/assets/bgd.webp | Bin 0 -> 520872 bytes ihm/assets/camera.jpg | Bin 0 -> 74603 bytes ihm/assets/home.js | 34 + ihm/assets/qr_code.png | Bin 0 -> 9072 bytes ihm/bgd-I4_In7H5.webp | Bin 0 -> 520872 bytes ihm/index--72nVAQK.css | 877 ++ ihm/index-CZvdFchg.mjs | 11827 ++++++++++++++++ ihm/index.html | 1 + ihm/qr-scanner-worker.min-Dy0qkKA4.mjs | 100 + ihm/sdk_client-CuF7MH2z.mjs | 1454 ++ ihm/sdk_client_bg-B6ah1IVY.wasm | Bin 0 -> 3302073 bytes ihm/style/4nk.css | 877 ++ ihm/style/account.css | 1507 ++ ihm/style/chat.css | 597 + ihm/style/signature.css | 1664 +++ ihm/types/components/header/header.d.ts | 2 + .../components/modal/confirmation-modal.d.ts | 1 + .../qrcode-scanner-component.d.ts | 10 + .../validation-modal/validation-modal.d.ts | 1 + .../validation-rule-modal.d.ts | 14 + ihm/types/index.d.ts | 3 + ihm/types/interface/groupInterface.d.ts | 24 + ihm/types/interface/memberInterface.d.ts | 10 + ihm/types/main.d.ts | 9 + .../mocks/mock-account/constAccountMock.d.ts | 118 + .../mock-account/interfacesAccountMock.d.ts | 38 + .../mocks/mock-signature/messagesMock.d.ts | 9 + ihm/types/models/backup.model.d.ts | 6 + ihm/types/models/notification.model.d.ts | 24 + ihm/types/models/process.model.d.ts | 56 + ihm/types/models/signature.models.d.ts | 60 + .../pages/account/account-component.d.ts | 12 + ihm/types/pages/account/account.d.ts | 98 + .../pages/account/document-validation.d.ts | 33 + .../pages/account/key-value-section.d.ts | 8 + ihm/types/pages/account/process-creation.d.ts | 1 + ihm/types/pages/account/process.d.ts | 4 + ihm/types/pages/home/home-component.d.ts | 8 + ihm/types/pages/home/home.d.ts | 4 + ihm/types/router.d.ts | 5 + ihm/types/services/database.service.d.ts | 43 + ihm/types/services/modal.service.d.ts | 28 + ihm/types/services/service.d.ts | 167 + ihm/types/services/storage.service.d.ts | 4 + ihm/types/services/token.d.ts | 17 + ihm/types/utils/document.utils.d.ts | 1 + ihm/types/utils/html.utils.d.ts | 4 + ihm/types/utils/messageMock.d.ts | 12 + ihm/types/utils/notification.store.d.ts | 23 + ihm/types/utils/service.utils.d.ts | 5 + ihm/types/utils/sp-address.utils.d.ts | 8 + ihm/types/utils/subscription.utils.d.ts | 2 + ihm/types/websockets.d.ts | 5 + jest.config.js | 15 + package-lock.json | 10746 ++++++++++++++ package.json | 40 + scripts/build-ihm.ps1 | 14 + scripts/checks/version_alignment.sh | 20 + scripts/copy-ihm-dist.ps1 | 21 + scripts/release/guard.sh | 65 + scripts/scripts/auto-ssh-push.sh | 151 + scripts/scripts/init-ssh-env.sh | 59 + scripts/scripts/setup-ssh-ci.sh | 54 + src/App.tsx | 18 + src/bridge/IframeBridge.ts | 44 + src/components/WebWallet.tsx | 73 + src/screens/WalletScreen.tsx | 11 + src/store/index.ts | 38 + tests/bridge.test.ts | 23 + tests/setup.ts | 2 + tests/store.test.ts | 17 + tsconfig.json | 26 + web/bridge.js | 69 + web/index.html | 53 + 116 files changed, 41154 insertions(+) create mode 100644 .cursor/rules/00-foundations.mdc create mode 100644 .cursor/rules/10-project-structure.mdc create mode 100644 .cursor/rules/20-documentation.mdc create mode 100644 .cursor/rules/30-testing.mdc create mode 100644 .cursor/rules/40-dependencies-and-build.mdc create mode 100644 .cursor/rules/41-ssh-automation.mdc create mode 100644 .cursor/rules/42-template-sync.mdc create mode 100644 .cursor/rules/4nkrules.mdc create mode 100644 .cursor/rules/50-data-csv-models.mdc create mode 100644 .cursor/rules/60-office-docs.mdc create mode 100644 .cursor/rules/70-frontend-architecture.mdc create mode 100644 .cursor/rules/80-versioning-and-release.mdc create mode 100644 .cursor/rules/85-release-guard.mdc create mode 100644 .cursor/rules/90-gitea-and-oss.mdc create mode 100644 .cursor/rules/95-triage-and-problem-solving.mdc create mode 100644 .cursor/rules/ruleset-index.md create mode 100644 .cursorignore create mode 100644 .gitea/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitea/workflows/LOCAL_OVERRIDES.yml create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/template-sync.yml create mode 100644 .gitea_template/ISSUE_TEMPLATE/bug_report.md create mode 100644 .gitea_template/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitea_template/PULL_REQUEST_TEMPLATE.md create mode 100644 .gitea_template/workflows/LOCAL_OVERRIDES.yml create mode 100644 .gitea_template/workflows/ci.yml create mode 100644 .gitea_template/workflows/template-sync.yml create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/INDEX.md create mode 100644 docs/INTEGRATION.md create mode 100644 docs/TESTING.md create mode 100644 ihm/account-component-DbdHSqFJ.mjs create mode 100644 ihm/assets/4nk_image.png create mode 100644 ihm/assets/4nk_revoke.jpg create mode 100644 ihm/assets/bgd.webp create mode 100644 ihm/assets/camera.jpg create mode 100644 ihm/assets/home.js create mode 100644 ihm/assets/qr_code.png create mode 100644 ihm/bgd-I4_In7H5.webp create mode 100644 ihm/index--72nVAQK.css create mode 100644 ihm/index-CZvdFchg.mjs create mode 100644 ihm/index.html create mode 100644 ihm/qr-scanner-worker.min-Dy0qkKA4.mjs create mode 100644 ihm/sdk_client-CuF7MH2z.mjs create mode 100644 ihm/sdk_client_bg-B6ah1IVY.wasm create mode 100644 ihm/style/4nk.css create mode 100644 ihm/style/account.css create mode 100644 ihm/style/chat.css create mode 100644 ihm/style/signature.css create mode 100644 ihm/types/components/header/header.d.ts create mode 100644 ihm/types/components/modal/confirmation-modal.d.ts create mode 100644 ihm/types/components/qrcode-scanner/qrcode-scanner-component.d.ts create mode 100644 ihm/types/components/validation-modal/validation-modal.d.ts create mode 100644 ihm/types/components/validation-rule-modal/validation-rule-modal.d.ts create mode 100644 ihm/types/index.d.ts create mode 100644 ihm/types/interface/groupInterface.d.ts create mode 100644 ihm/types/interface/memberInterface.d.ts create mode 100644 ihm/types/main.d.ts create mode 100644 ihm/types/mocks/mock-account/constAccountMock.d.ts create mode 100644 ihm/types/mocks/mock-account/interfacesAccountMock.d.ts create mode 100644 ihm/types/mocks/mock-signature/messagesMock.d.ts create mode 100644 ihm/types/models/backup.model.d.ts create mode 100644 ihm/types/models/notification.model.d.ts create mode 100644 ihm/types/models/process.model.d.ts create mode 100644 ihm/types/models/signature.models.d.ts create mode 100644 ihm/types/pages/account/account-component.d.ts create mode 100644 ihm/types/pages/account/account.d.ts create mode 100644 ihm/types/pages/account/document-validation.d.ts create mode 100644 ihm/types/pages/account/key-value-section.d.ts create mode 100644 ihm/types/pages/account/process-creation.d.ts create mode 100644 ihm/types/pages/account/process.d.ts create mode 100644 ihm/types/pages/home/home-component.d.ts create mode 100644 ihm/types/pages/home/home.d.ts create mode 100644 ihm/types/router.d.ts create mode 100644 ihm/types/services/database.service.d.ts create mode 100644 ihm/types/services/modal.service.d.ts create mode 100644 ihm/types/services/service.d.ts create mode 100644 ihm/types/services/storage.service.d.ts create mode 100644 ihm/types/services/token.d.ts create mode 100644 ihm/types/utils/document.utils.d.ts create mode 100644 ihm/types/utils/html.utils.d.ts create mode 100644 ihm/types/utils/messageMock.d.ts create mode 100644 ihm/types/utils/notification.store.d.ts create mode 100644 ihm/types/utils/service.utils.d.ts create mode 100644 ihm/types/utils/sp-address.utils.d.ts create mode 100644 ihm/types/utils/subscription.utils.d.ts create mode 100644 ihm/types/websockets.d.ts create mode 100644 jest.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/build-ihm.ps1 create mode 100644 scripts/checks/version_alignment.sh create mode 100644 scripts/copy-ihm-dist.ps1 create mode 100644 scripts/release/guard.sh create mode 100644 scripts/scripts/auto-ssh-push.sh create mode 100644 scripts/scripts/init-ssh-env.sh create mode 100644 scripts/scripts/setup-ssh-ci.sh create mode 100644 src/App.tsx create mode 100644 src/bridge/IframeBridge.ts create mode 100644 src/components/WebWallet.tsx create mode 100644 src/screens/WalletScreen.tsx create mode 100644 src/store/index.ts create mode 100644 tests/bridge.test.ts create mode 100644 tests/setup.ts create mode 100644 tests/store.test.ts create mode 100644 tsconfig.json create mode 100644 web/bridge.js create mode 100644 web/index.html diff --git a/.cursor/rules/00-foundations.mdc b/.cursor/rules/00-foundations.mdc new file mode 100644 index 0000000..f8c9c6d --- /dev/null +++ b/.cursor/rules/00-foundations.mdc @@ -0,0 +1,32 @@ +--- +alwaysApply: true +--- + +# Fondations de rédaction et de comportement + +[portée] +S’applique à tout le dépôt 4NK/4NK_node pour toute génération, refactorisation, édition inline ou discussion dans Cursor. + +[objectifs] + +- Garantir l’usage exclusif du français. +- Proscrire l’injection d’exemples de code applicatif dans la base de code. +- Assurer une cohérence stricte de terminologie et de ton. +- Exiger une introduction et/ou une conclusion dans toute proposition de texte. + +[directives] + +- Toujours répondre et documenter en français. +- Ne pas inclure d’exemples exécutables ou de quickstarts dans la base ; préférer des descriptions prescriptives. +- Tout contenu produit doit mentionner explicitement les artefacts à mettre à jour lorsqu’il impacte docs/ et tests/. +- Préserver la typographie française (capitaliser uniquement le premier mot d’un titre et les noms propres). + +[validations] + +- Relecture linguistique et technique systématique. +- Refuser toute sortie avec exemples de code applicatif. +- Vérifier que l’issue traitée se conclut par un rappel des fichiers à mettre à jour. + +[artefacts concernés] + +- README.md, docs/**, tests/**, CHANGELOG.md, .gitea/**. diff --git a/.cursor/rules/10-project-structure.mdc b/.cursor/rules/10-project-structure.mdc new file mode 100644 index 0000000..4c1ef95 --- /dev/null +++ b/.cursor/rules/10-project-structure.mdc @@ -0,0 +1,72 @@ +--- +alwaysApply: true +--- + +# Structure projet 4NK_node + +[portée] +Maintenance de l’arborescence canonique, création/mise à jour/suppression de fichiers et répertoires. + +[objectifs] + +- Garantir l’alignement strict avec l’arborescence 4NK_node. +- Prévenir toute dérive structurelle. + +[directives] + +- S’assurer que l’arborescence suivante existe et reste conforme : + + 4NK/4NK_node + ├── archive + ├── CHANGELOG.md + ├── CODE_OF_CONDUCT.md + ├── CONTRIBUTING.md + ├── docker-compose.yml + ├── docs + │ ├── API.md + │ ├── ARCHITECTURE.md + │ ├── COMMUNITY_GUIDE.md + │ ├── CONFIGURATION.md + │ ├── GITEA_SETUP.md + │ ├── INDEX.md + │ ├── INSTALLATION.md + │ ├── MIGRATION.md + │ ├── OPEN_SOURCE_CHECKLIST.md + │ ├── QUICK_REFERENCE.md + │ ├── RELEASE_PLAN.md + │ ├── ROADMAP.md + │ ├── SECURITY_AUDIT.md + │ ├── TESTING.md + │ └── USAGE.md + ├── LICENSE + ├── README.md + ├── tests + │ ├── cleanup.sh + │ ├── connectivity + │ ├── external + │ ├── integration + │ ├── logs + │ ├── performance + │ ├── README.md + │ ├── reports + │ └── unit + └── .gitea + ├── ISSUE_TEMPLATE + │ ├── bug_report.md + │ └── feature_request.md + ├── PULL_REQUEST_TEMPLATE.md + └── workflows + └── ci.yml + +- Tout document obsolète est déplacé vers archive/ avec métadonnées (date, raison). +- Interdire la suppression brute de fichiers sans archivage et note dans CHANGELOG.md. + +[validations] + +- Diff structurel comparé à cette référence. +- Erreur bloquante si un fichier « requis » manque. + +[artefacts concernés] + +- archive/**, docs/**, tests/**, .gitea/**, CHANGELOG.md. + diff --git a/.cursor/rules/20-documentation.mdc b/.cursor/rules/20-documentation.mdc new file mode 100644 index 0000000..fa65b5c --- /dev/null +++ b/.cursor/rules/20-documentation.mdc @@ -0,0 +1,33 @@ +--- +alwaysApply: true +--- + +# Documentation continue + +[portée] +Mises à jour de docs/** corrélées à tout changement de code, configuration, dépendance, données ou CI. + +[objectifs] +- Remplacer toute section générique « RESUME » par des mises à jour ciblées dans les fichiers appropriés. +- Tenir INDEX.md comme table des matières de référence. + +[directives] +- À chaque changement, mettre à jour : + - API.md (spécifications, contrats, schémas, invariants). + - ARCHITECTURE.md (décisions, diagrammes, couplages, performances). + - CONFIGURATION.md (paramètres, formats, valeurs par défaut). + - INSTALLATION.md (pré-requis, étapes, vérifications). + - MIGRATION.md (chemins de migration, scripts, compatibilités). + - USAGE.md (parcours fonctionnels, contraintes). + - TESTING.md (pyramide, critères d’acceptation). + - SECURITY_AUDIT.md (menaces, contrôles, dettes résiduelles). + - RELEASE_PLAN.md, ROADMAP.md (planification), OPEN_SOURCE_CHECKLIST.md, COMMUNITY_GUIDE.md, GITEA_SETUP.md. +- Maintenir QUICK_REFERENCE.md pour les référentiels synthétiques utilisés par l’équipe. +- Ajouter un REX technique en cas d’hypothèses multiples avant résolution dans archive/. + +[validations] +- Cohérence croisée entre README.md et INDEX.md. +- Refus si une modification de code n’a pas de trace dans docs/** correspondants. + +[artefacts concernés] +- docs/**, README.md, archive/**. diff --git a/.cursor/rules/30-testing.mdc b/.cursor/rules/30-testing.mdc new file mode 100644 index 0000000..7178c27 --- /dev/null +++ b/.cursor/rules/30-testing.mdc @@ -0,0 +1,57 @@ +--- +alwaysApply: true +--- + +# Tests et qualité + +[portée] +Stratégie de tests, exécution locale, stabilité, non-régression. + +[objectifs] + +- Exiger des tests verts avant tout commit. +- Couvrir les axes unit, integration, connectivity, performance, external. + +[directives] + +- Ajouter/mettre à jour des tests dans tests/unit, tests/integration, tests/connectivity, tests/performance, tests/external selon l’impact. +- Consigner les journaux dans tests/logs et les rapports dans tests/reports. +- Maintenir tests/README.md (stratégie, outillage, seuils). +- Fournir un nettoyage reproductible via tests/cleanup.sh. +- Bloquer l’édition si des tests échouent tant que la correction n’est pas appliquée. + +[validations] + +- Refus d’un commit si tests en échec. +- Exiger justification et plan de test dans docs/TESTING.md pour toute refonte majeure. + +[artefacts concernés] + +- tests/**, docs/TESTING.md, CHANGELOG.md. + +# Tests et qualité + +[portée] +Stratégie de tests, exécution locale, stabilité, non-régression. + +[objectifs] + +- Exiger des tests verts avant tout commit. +- Couvrir les axes unit, integration, connectivity, performance, external. + +[directives] + +- Ajouter/mettre à jour des tests dans tests/unit, tests/integration, tests/connectivity, tests/performance, tests/external selon l’impact. +- Consigner les journaux dans tests/logs et les rapports dans tests/reports. +- Maintenir tests/README.md (stratégie, outillage, seuils). +- Fournir un nettoyage reproductible via tests/cleanup.sh. +- Bloquer l’édition si des tests échouent tant que la correction n’est pas appliquée. + +[validations] + +- Refus d’un commit si tests en échec. +- Exiger justification et plan de test dans docs/TESTING.md pour toute refonte majeure. + +[artefacts concernés] + +- tests/**, docs/TESTING.md, CHANGELOG.md. diff --git a/.cursor/rules/40-dependencies-and-build.mdc b/.cursor/rules/40-dependencies-and-build.mdc new file mode 100644 index 0000000..c1ece2d --- /dev/null +++ b/.cursor/rules/40-dependencies-and-build.mdc @@ -0,0 +1,55 @@ +--- +alwaysApply: true +--- + +# Dépendances, compilation et build + +[portée] +Gestion des dépendances, compilation fréquente, politique de versions. + +[objectifs] + +- Ajouter automatiquement les dépendances manquantes si justifié. +- Rechercher systématiquement les dernières versions stables. + +[directives] + +- Lorsqu’une fonctionnalité nécessite une dépendance, l’ajouter et la documenter (nom, version, portée, impact) dans docs/ARCHITECTURE.md et docs/CONFIGURATION.md si nécessaire. +- Compiler très régulièrement et « quand nécessaire » (avant refactor, avant push, après mise à jour de dépendances). +- Corriger toute erreur de compilation/exécution avant de poursuivre. +- Documenter tout changement de dépendances (raison, risques, rollback). + +[validations] + +- Interdire la progression si la compilation échoue. +- Vérifier la présence d’une note de changement dans CHANGELOG.md en cas de dépendance ajoutée/retirée. + +[artefacts concernés] + +- docs/ARCHITECTURE.md, docs/CONFIGURATION.md, CHANGELOG.md. + +# Dépendances, compilation et build + +[portée] +Gestion des dépendances, compilation fréquente, politique de versions. + +[objectifs] + +- Ajouter automatiquement les dépendances manquantes si justifié. +- Rechercher systématiquement les dernières versions stables. + +[directives] + +- Lorsqu’une fonctionnalité nécessite une dépendance, l’ajouter et la documenter (nom, version, portée, impact) dans docs/ARCHITECTURE.md et docs/CONFIGURATION.md si nécessaire. +- Compiler très régulièrement et « quand nécessaire » (avant refactor, avant push, après mise à jour de dépendances). +- Corriger toute erreur de compilation/exécution avant de poursuivre. +- Documenter tout changement de dépendances (raison, risques, rollback). + +[validations] + +- Interdire la progression si la compilation échoue. +- Vérifier la présence d’une note de changement dans CHANGELOG.md en cas de dépendance ajoutée/retirée. + +[artefacts concernés] + +- docs/ARCHITECTURE.md, docs/CONFIGURATION.md, CHANGELOG.md. diff --git a/.cursor/rules/41-ssh-automation.mdc b/.cursor/rules/41-ssh-automation.mdc new file mode 100644 index 0000000..1a988d6 --- /dev/null +++ b/.cursor/rules/41-ssh-automation.mdc @@ -0,0 +1,65 @@ +--- +alwaysApply: true +--- + +# Automatisation SSH et scripts + +[portée] +Création, usage et vérification du dossier scripts/ et de ses trois scripts standards liés aux opérations SSH et CI. + +[objectifs] + +- Garantir la présence de scripts/ avec auto-ssh-push.sh, init-ssh-env.sh, setup-ssh-ci.sh. +- Encadrer l’usage de ces scripts (locaux et CI), la sécurité, l’idempotence et la traçabilité. +- Documenter toute mise à jour dans docs/SSH_UPDATE.md et CHANGELOG.md. + +[directives] + +- Créer et maintenir `scripts/auto-ssh-push.sh`, `scripts/init-ssh-env.sh`, `scripts/setup-ssh-ci.sh`. +- Exiger permissions d’exécution adaptées sur scripts/ (exécution locale et CI). +- Interdire le stockage de clés privées ou secrets en clair dans le dépôt. +- Utiliser des variables d’environnement et secrets CI pour toute donnée sensible. +- Rendre chaque script idempotent et verbosable ; produire un code de sortie non-zéro en cas d’échec. +- Tracer les opérations : consigner un résumé dans docs/SSH_UPDATE.md (objectif, variables requises, effets, points d’échec). +- Ajouter un contrôle automatique dans la CI pour vérifier l’existence et l’exécutabilité de ces scripts. + +[validations] + +- Échec bloquant si un des trois scripts manque ou n’est pas exécutable. +- Échec bloquant si docs/SSH_UPDATE.md n’est pas mis à jour lors d’une modification de scripts. +- Échec bloquant si un secret attendu n’est pas fourni en CI. + +[artefacts concernés] + +- scripts/**, docs/SSH_UPDATE.md, .gitea/workflows/ci.yml, CHANGELOG.md, docs/CONFIGURATION.md. + +# Automatisation SSH et scripts + +[portée] +Création, usage et vérification du dossier scripts/ et de ses trois scripts standards liés aux opérations SSH et CI. + +[objectifs] + +- Garantir la présence de scripts/ avec auto-ssh-push.sh, init-ssh-env.sh, setup-ssh-ci.sh. +- Encadrer l’usage de ces scripts (locaux et CI), la sécurité, l’idempotence et la traçabilité. +- Documenter toute mise à jour dans docs/SSH_UPDATE.md et CHANGELOG.md. + +[directives] + +- Créer et maintenir `scripts/auto-ssh-push.sh`, `scripts/init-ssh-env.sh`, `scripts/setup-ssh-ci.sh`. +- Exiger permissions d’exécution adaptées sur scripts/ (exécution locale et CI). +- Interdire le stockage de clés privées ou secrets en clair dans le dépôt. +- Utiliser des variables d’environnement et secrets CI pour toute donnée sensible. +- Rendre chaque script idempotent et verbosable ; produire un code de sortie non-zéro en cas d’échec. +- Tracer les opérations : consigner un résumé dans docs/SSH_UPDATE.md (objectif, variables requises, effets, points d’échec). +- Ajouter un contrôle automatique dans la CI pour vérifier l’existence et l’exécutabilité de ces scripts. + +[validations] + +- Échec bloquant si un des trois scripts manque ou n’est pas exécutable. +- Échec bloquant si docs/SSH_UPDATE.md n’est pas mis à jour lors d’une modification de scripts. +- Échec bloquant si un secret attendu n’est pas fourni en CI. + +[artefacts concernés] + +- scripts/**, docs/SSH_UPDATE.md, .gitea/workflows/ci.yml, CHANGELOG.md, docs/CONFIGURATION.md. diff --git a/.cursor/rules/42-template-sync.mdc b/.cursor/rules/42-template-sync.mdc new file mode 100644 index 0000000..c7cf051 --- /dev/null +++ b/.cursor/rules/42-template-sync.mdc @@ -0,0 +1,53 @@ +--- +alwaysApply: true +--- + +# Synchronisation de template (4NK) + +[portée] +Tous les projets issus de 4NK_project_template. Contrôle de l’alignement sur .cursor/, .gitea/, AGENTS.md, scripts/, docs/SSH_UPDATE.md. + +[objectifs] + +- Garantir l’absence de dérive sur les éléments normatifs. +- Exiger la mise à jour documentaire et du changelog à chaque synchronisation. +- Bloquer la progression en cas d’intégrité non conforme. + +[directives] +- Lire la configuration de .4nk-sync.yml (source_repo, ref, paths, policy). +- Refuser toute modification locale dans le périmètre des paths sans PR de synchronisation. +- Après synchronisation : exiger mises à jour de CHANGELOG.md et docs/INDEX.md. +- Scripts : vérifier présence, permissions d’exécution et absence de secrets en clair. +- SSH : exiger mise à jour de docs/SSH_UPDATE.md si scripts/** modifié. + +[validations] +- Erreur bloquante si manifest_checksum manquant ou invalide. +- Erreur bloquante si un path requis n’existe pas après sync. +- Erreur bloquante si tests/CI signalent des scripts non exécutables ou des fichiers sensibles. + +[artefacts concernés] +- .4nk-sync.yml, TEMPLATE_VERSION, .cursor/**, .gitea/**, AGENTS.md, scripts/**, docs/SSH_UPDATE.md, CHANGELOG.md. +# Synchronisation de template (4NK) + +[portée] +Tous les projets issus de 4NK_project_template. Contrôle de l’alignement sur .cursor/, .gitea/, AGENTS.md, scripts/, docs/SSH_UPDATE.md. + +[objectifs] +- Garantir l’absence de dérive sur les éléments normatifs. +- Exiger la mise à jour documentaire et du changelog à chaque synchronisation. +- Bloquer la progression en cas d’intégrité non conforme. + +[directives] +- Lire la configuration de .4nk-sync.yml (source_repo, ref, paths, policy). +- Refuser toute modification locale dans le périmètre des paths sans PR de synchronisation. +- Après synchronisation : exiger mises à jour de CHANGELOG.md et docs/INDEX.md. +- Scripts : vérifier présence, permissions d’exécution et absence de secrets en clair. +- SSH : exiger mise à jour de docs/SSH_UPDATE.md si scripts/** modifié. + +[validations] +- Erreur bloquante si manifest_checksum manquant ou invalide. +- Erreur bloquante si un path requis n’existe pas après sync. +- Erreur bloquante si tests/CI signalent des scripts non exécutables ou des fichiers sensibles. + +[artefacts concernés] +- .4nk-sync.yml, TEMPLATE_VERSION, .cursor/**, .gitea/**, AGENTS.md, scripts/**, docs/SSH_UPDATE.md, CHANGELOG.md. diff --git a/.cursor/rules/4nkrules.mdc b/.cursor/rules/4nkrules.mdc new file mode 100644 index 0000000..75c8e3c --- /dev/null +++ b/.cursor/rules/4nkrules.mdc @@ -0,0 +1,156 @@ +--- +alwaysApply: true +# cursor.mcd — règles d’or 4NK +language: fr +policies: + respond_in_french: true + no_examples_in_codebase: true + ask_before_push_or_tag: true + +directories: + ensure: + - archive/ + - docs/ + - tests/ + - .gitea/ + docs: + required_files: + - API.md + - ARCHITECTURE.md + - COMMUNITY_GUIDE.md + - CONFIGURATION.md + - GITEA_SETUP.md + - INDEX.md + - INSTALLATION.md + - MIGRATION.md + - OPEN_SOURCE_CHECKLIST.md + - QUICK_REFERENCE.md + - RELEASE_PLAN.md + - ROADMAP.md + - SECURITY_AUDIT.md + - TESTING.md + - USAGE.md + tests: + required_files: + - cleanup.sh + - README.md + required_dirs: + - connectivity + - external + - integration + - logs + - performance + - reports + - unit + gitea: + required_files: + - PULL_REQUEST_TEMPLATE.md + required_dirs: + - ISSUE_TEMPLATE + - workflows + ISSUE_TEMPLATE: + required_files: + - bug_report.md + - feature_request.md + workflows: + required_files: + - ci.yml + +files: + required_root_files: + - CHANGELOG.md + - CODE_OF_CONDUCT.md + - CONTRIBUTING.md + - docker-compose.yml + - LICENSE + - README.md + +documentation: + update_on: + - feature_added + - feature_modified + - feature_removed + - feature_discovered + replace_sections_named: ["RESUME"] + rex_required_on_multiple_hypotheses: true + archive_obsolete_docs: true + +compilation: + compile_often: true + compile_when_needed: true + fail_on_errors: true + +problem_solving: + auto_run_steps: + - minimal_repro + - inspect_logs + - bisect_changes + - form_hypotheses + - targeted_tests + - implement_fix + - non_regression + +office_docs: + docx_reader: docx2txt + fallback: + - pandoc_convert + - request_alternate_source + +dependencies: + auto_add_missing: true + always_check_latest_stable: true + document_changes_in_docs: true + +csv_models: + treat_as_source_of_truth: true + multirow_headers_supported: true + confirm_in_docs: true + require_column_definitions: true + +file_processing: + study_each_file: true + ask_questions_if_needed: true + adapt_code_if_needed: true + propose_solution_if_unreadable: true + +types_and_properties: + auto_correct_incoherences: true + document_transformations: true + +functional_consistency: + always_ask_clarifying_questions: true + +frontend_architecture: + react_code_splitting: true + state_management: ["redux", "context_api"] + data_service_abstraction: true + +execution_discipline: + finish_started_work: true + +open_source_and_gitea: + prepare_every_project: true + gitea_remote: "git.4nkweb.com" + required_files: + - LICENSE + - CONTRIBUTING.md + - CHANGELOG.md + - CODE_OF_CONDUCT.md + align_with_4NK_node_on_creation: true + keep_alignment_updated: true + +tests_and_docs: + update_docs_and_tests_with_code: true + require_green_tests_before_commit: true + +versioning: + manage_with_changelog: true + confirm_before_push: true + confirm_before_tag: true + propose_semver_bump: true + +pre_commit: + run_all_tests: true + block_on_errors: true + +--- \ No newline at end of file diff --git a/.cursor/rules/50-data-csv-models.mdc b/.cursor/rules/50-data-csv-models.mdc new file mode 100644 index 0000000..c686e3d --- /dev/null +++ b/.cursor/rules/50-data-csv-models.mdc @@ -0,0 +1,54 @@ +--- +alwaysApply: false +--- +# Modélisation des données à partir de CSV + +[portée] +Utilisation des CSV comme base des modèles de données, y compris en-têtes multi-lignes. + +[objectifs] + +- Confirmer la structure inférée pour chaque CSV. +- Demander une définition formelle de toutes les colonnes. + +[directives] + +- Gérer explicitement les en-têtes multi-lignes (titre principal + sous-colonnes). +- Confirmer par écrit dans docs/API.md ou docs/ARCHITECTURE.md : nombre de lignes d’en-tête, mapping colonnes→types, unités, domaines de valeurs, nullabilité, contraintes. +- Poser des questions si ambiguïtés ; proposer une normalisation temporaire documentée. +- Corriger automatiquement les incohérences de types si une règle de mapping est établie ailleurs et documenter la transformation. + +[validations] + +- Aucune ingestion sans spécification de colonnes validée. +- Traçabilité des corrections de types (avant/après) dans docs/ARCHITECTURE.md. + +[artefacts concernés] + +- docs/API.md, docs/ARCHITECTURE.md, docs/USAGE.md. + +# Modélisation des données à partir de CSV + +[portée] +Utilisation des CSV comme base des modèles de données, y compris en-têtes multi-lignes. + +[objectifs] + +- Confirmer la structure inférée pour chaque CSV. +- Demander une définition formelle de toutes les colonnes. + +[directives] + +- Gérer explicitement les en-têtes multi-lignes (titre principal + sous-colonnes). +- Confirmer par écrit dans docs/API.md ou docs/ARCHITECTURE.md : nombre de lignes d’en-tête, mapping colonnes→types, unités, domaines de valeurs, nullabilité, contraintes. +- Poser des questions si ambiguïtés ; proposer une normalisation temporaire documentée. +- Corriger automatiquement les incohérences de types si une règle de mapping est établie ailleurs et documenter la transformation. + +[validations] + +- Aucune ingestion sans spécification de colonnes validée. +- Traçabilité des corrections de types (avant/après) dans docs/ARCHITECTURE.md. + +[artefacts concernés] + +- docs/API.md, docs/ARCHITECTURE.md, docs/USAGE.md. diff --git a/.cursor/rules/60-office-docs.mdc b/.cursor/rules/60-office-docs.mdc new file mode 100644 index 0000000..7f57891 --- /dev/null +++ b/.cursor/rules/60-office-docs.mdc @@ -0,0 +1,41 @@ +--- +alwaysApply: false +--- +# Lecture des documents bureautiques + +[portée] +Lecture des fichiers .docx et alternatives. + +[objectifs] +- Utiliser docx2txt par défaut. +- Proposer des solutions de repli si lecture impossible. + +[directives] +- Lire les .docx avec docx2txt. +- En cas d’échec, proposer : conversion via pandoc, demande d’une source alternative, ou extraction textuelle. +- Documenter dans docs/INDEX.md la provenance et le statut des documents importés. + +[validations] +- Vérification que les contenus extraits sont intégrés aux fichiers docs/ concernés. + +[artefacts concernés] +- docs/**, archive/**. +# Lecture des documents bureautiques + +[portée] +Lecture des fichiers .docx et alternatives. + +[objectifs] +- Utiliser docx2txt par défaut. +- Proposer des solutions de repli si lecture impossible. + +[directives] +- Lire les .docx avec docx2txt. +- En cas d’échec, proposer : conversion via pandoc, demande d’une source alternative, ou extraction textuelle. +- Documenter dans docs/INDEX.md la provenance et le statut des documents importés. + +[validations] +- Vérification que les contenus extraits sont intégrés aux fichiers docs/ concernés. + +[artefacts concernés] +- docs/**, archive/**. diff --git a/.cursor/rules/70-frontend-architecture.mdc b/.cursor/rules/70-frontend-architecture.mdc new file mode 100644 index 0000000..65d9c40 --- /dev/null +++ b/.cursor/rules/70-frontend-architecture.mdc @@ -0,0 +1,56 @@ +--- +alwaysApply: false +--- + +# Architecture frontend + +[portée] +Qualité du bundle, découpage, état global et couche de services. + +[objectifs] + +- Réduire la taille du bundle initial via code splitting. +- Éviter le prop drilling via Redux ou Context API. +- Abstraire les services de données pour testabilité et maintenance. + +[directives] + +- Mettre en place React.lazy et Suspense pour le chargement différé des vues/segments. +- Centraliser l’état global via Redux ou Context API. +- Isoler les appels « data » derrière une couche d’abstraction à interface stable. +- Interdire l’ajout d’exemples front dans la base de code. + +[validations] + +- Vérifier que les points d’entrée sont minimes et que les segments non critiques sont chargés à la demande. +- S’assurer que docs/ARCHITECTURE.md décrit les décisions et les points d’extension. + +[artefacts concernés] + +- docs/ARCHITECTURE.md, docs/TESTING.md. +# Architecture frontend + +[portée] +Qualité du bundle, découpage, état global et couche de services. + +[objectifs] + +- Réduire la taille du bundle initial via code splitting. +- Éviter le prop drilling via Redux ou Context API. +- Abstraire les services de données pour testabilité et maintenance. + +[directives] + +- Mettre en place React.lazy et Suspense pour le chargement différé des vues/segments. +- Centraliser l’état global via Redux ou Context API. +- Isoler les appels « data » derrière une couche d’abstraction à interface stable. +- Interdire l’ajout d’exemples front dans la base de code. + +[validations] + +- Vérifier que les points d’entrée sont minimes et que les segments non critiques sont chargés à la demande. +- S’assurer que docs/ARCHITECTURE.md décrit les décisions et les points d’extension. + +[artefacts concernés] + +- docs/ARCHITECTURE.md, docs/TESTING.md. diff --git a/.cursor/rules/80-versioning-and-release.mdc b/.cursor/rules/80-versioning-and-release.mdc new file mode 100644 index 0000000..24d213a --- /dev/null +++ b/.cursor/rules/80-versioning-and-release.mdc @@ -0,0 +1,53 @@ +--- +alwaysApply: false +--- + +# Versionnage et publication + +[portée] +Gestion sémantique des versions, CHANGELOG, confirmation push/tag. + +[objectifs] + +- Tenir CHANGELOG.md comme source unique de vérité. +- Demander confirmation avant push et tag. + +[directives] + +- À chaque changement significatif, mettre à jour CHANGELOG.md (ajouts, changements, corrections, ruptures). +- Proposer un bump semver (major/minor/patch) motivé par l’impact. +- Avant tout push ou tag, demander confirmation explicite. + +[validations] + +- Refus si modification sans entrée correspondante dans CHANGELOG.md. +- Cohérence entre CHANGELOG.md, docs/RELEASE_PLAN.md et docs/ROADMAP.md. + +[artefacts concernés] + +- CHANGELOG.md, docs/RELEASE_PLAN.md, docs/ROADMAP.md. + +# Versionnage et publication + +[portée] +Gestion sémantique des versions, CHANGELOG, confirmation push/tag. + +[objectifs] + +- Tenir CHANGELOG.md comme source unique de vérité. +- Demander confirmation avant push et tag. + +[directives] + +- À chaque changement significatif, mettre à jour CHANGELOG.md (ajouts, changements, corrections, ruptures). +- Proposer un bump semver (major/minor/patch) motivé par l’impact. +- Avant tout push ou tag, demander confirmation explicite. + +[validations] + +- Refus si modification sans entrée correspondante dans CHANGELOG.md. +- Cohérence entre CHANGELOG.md, docs/RELEASE_PLAN.md et docs/ROADMAP.md. + +[artefacts concernés] + +- CHANGELOG.md, docs/RELEASE_PLAN.md, docs/ROADMAP.md. diff --git a/.cursor/rules/85-release-guard.mdc b/.cursor/rules/85-release-guard.mdc new file mode 100644 index 0000000..827ef9a --- /dev/null +++ b/.cursor/rules/85-release-guard.mdc @@ -0,0 +1,37 @@ +--- +alwaysApply: true +--- + +# Garde de release: tests, documentation, compilation, version, changelog, tag + +[portée] +Contrôler systématiquement avant push/tag: tests verts, docs mises à jour, build OK, alignement numéro de version ↔ changelog ↔ tag git, mise à jour de déploiement, confirmation utilisateur (latest vs wip). + +[objectifs] + +- Empêcher toute publication sans vérifications minimales. +- Exiger la cohérence sémantique (VERSION/TEMPLATE_VERSION ↔ CHANGELOG ↔ tag git). +- Demander explicitement « latest » ou « wip » et appliquer la bonne stratégie. + +[directives] + +- Avant push/tag, exécuter: tests, compilation, lints (si configurés). +- Mettre à jour la documentation et le changelog en conséquence. +- Aligner le fichier de version (VERSION ou TEMPLATE_VERSION), l’entrée CHANGELOG et le tag. +- Demander confirmation utilisateur: `latest` (release stable) ou `wip` (travail en cours). + - latest: entrée datée dans CHANGELOG, version stable, tag `vX.Y.Z`. + - wip: suffixe `-wip` recommandé dans version/tag (ex: `vX.Y.Z-wip.N`). +- Mettre à jour le déploiement après publication (si pipeline défini), sinon documenter l’étape. + +[validations] + +- Refuser push/tag si: + - tests/compilation échouent, + - CHANGELOG non mis à jour, + - VERSION/TEMPLATE_VERSION absent ou incohérent, + - release type non fourni (ni latest, ni wip). + +[artefacts concernés] + +- CHANGELOG.md, VERSION ou TEMPLATE_VERSION, docs/**, .gitea/workflows/**, scripts/**. + diff --git a/.cursor/rules/90-gitea-and-oss.mdc b/.cursor/rules/90-gitea-and-oss.mdc new file mode 100644 index 0000000..f9da399 --- /dev/null +++ b/.cursor/rules/90-gitea-and-oss.mdc @@ -0,0 +1,59 @@ +--- +alwaysApply: true +--- + +# Open source et Gitea + +[portée] +Conformité open source, templates Gitea, CI. + +[objectifs] + +- Préparer chaque projet pour un dépôt Gitea (git.4nkweb.com). +- Maintenir les fichiers de gouvernance et la CI. + +[directives] + +- Vérifier la présence et l’actualité de : LICENSE, CONTRIBUTING.md, CODE_OF_CONDUCT.md, OPEN_SOURCE_CHECKLIST.md. +- Maintenir .gitea/ : + - ISSUE_TEMPLATE/bug_report.md, feature_request.md + - PULL_REQUEST_TEMPLATE.md + - workflows/ci.yml +- Documenter dans docs/GITEA_SETUP.md la configuration distante et les permissions. + +[validations] + +- Refus si un des fichiers « gouvernance/CI » manque. +- Cohérence entre docs/OPEN_SOURCE_CHECKLIST.md et l’état du repo. + +[artefacts concernés] + +- .gitea/**, docs/GITEA_SETUP.md, docs/OPEN_SOURCE_CHECKLIST.md. + +# Open source et Gitea + +[portée] +Conformité open source, templates Gitea, CI. + +[objectifs] + +- Préparer chaque projet pour un dépôt Gitea (git.4nkweb.com). +- Maintenir les fichiers de gouvernance et la CI. + +[directives] + +- Vérifier la présence et l’actualité de : LICENSE, CONTRIBUTING.md, CODE_OF_CONDUCT.md, OPEN_SOURCE_CHECKLIST.md. +- Maintenir .gitea/ : + - ISSUE_TEMPLATE/bug_report.md, feature_request.md + - PULL_REQUEST_TEMPLATE.md + - workflows/ci.yml +- Documenter dans docs/GITEA_SETUP.md la configuration distante et les permissions. + +[validations] + +- Refus si un des fichiers « gouvernance/CI » manque. +- Cohérence entre docs/OPEN_SOURCE_CHECKLIST.md et l’état du repo. + +[artefacts concernés] + +- .gitea/**, docs/GITEA_SETUP.md, docs/OPEN_SOURCE_CHECKLIST.md. diff --git a/.cursor/rules/95-triage-and-problem-solving.mdc b/.cursor/rules/95-triage-and-problem-solving.mdc new file mode 100644 index 0000000..4df091a --- /dev/null +++ b/.cursor/rules/95-triage-and-problem-solving.mdc @@ -0,0 +1,53 @@ +--- +alwaysApply: true +--- + +# Tri, diagnostic et résolution de problèmes + +[portée] +Boucle de triage : reproduction, diagnostic, correctif, non-régression. + +[objectifs] + +- Exécuter automatiquement les étapes de résolution. +- Bloquer l’avancement tant que les erreurs ne sont pas corrigées. + +[directives] + +- Étapes obligatoires : reproduction minimale, inspection des logs, bissection des changements, formulation d’hypothèses, tests ciblés, correctif, test de non-régression. +- Lorsque plusieurs hypothèses ont été testées, produire un REX dans archive/ avec liens vers les commits. +- Poser des questions de cohérence fonctionnelle si des ambiguïtés subsistent (contrats d’API, invariants, SLA). + +[validations] + +- Interdiction de clore une tâche si un test échoue ou si une alerte critique subsiste. +- Traçabilité du REX si investigations multiples. + +[artefacts concernés] + +- tests/**, archive/**, docs/TESTING.md, docs/ARCHITECTURE.md. + +# Tri, diagnostic et résolution de problèmes + +[portée] +Boucle de triage : reproduction, diagnostic, correctif, non-régression. + +[objectifs] + +- Exécuter automatiquement les étapes de résolution. +- Bloquer l’avancement tant que les erreurs ne sont pas corrigées. + +[directives] + +- Étapes obligatoires : reproduction minimale, inspection des logs, bissection des changements, formulation d’hypothèses, tests ciblés, correctif, test de non-régression. +- Lorsque plusieurs hypothèses ont été testées, produire un REX dans archive/ avec liens vers les commits. +- Poser des questions de cohérence fonctionnelle si des ambiguïtés subsistent (contrats d’API, invariants, SLA). + +[validations] + +- Interdiction de clore une tâche si un test échoue ou si une alerte critique subsiste. +- Traçabilité du REX si investigations multiples. + +[artefacts concernés] + +- tests/**, archive/**, docs/TESTING.md, docs/ARCHITECTURE.md. diff --git a/.cursor/rules/ruleset-index.md b/.cursor/rules/ruleset-index.md new file mode 100644 index 0000000..e70ef69 --- /dev/null +++ b/.cursor/rules/ruleset-index.md @@ -0,0 +1,16 @@ +# Index des règles .cursor/rules + +- 00-foundations.mdc : règles linguistiques et éditoriales (français, pas d’exemples en base, introduction/conclusion). +- 10-project-structure.mdc : arborescence canonique 4NK_node et garde-fous. +- 20-documentation.mdc : documentation continue, remplacement de « RESUME », INDEX.md. +- 30-testing.mdc : tests (unit, integration, connectivity, performance, external), logs/reports. +- 40-dependencies-and-build.mdc : dépendances, compilation, corrections bloquantes. +- 50-data-csv-models.mdc : CSV avec en-têtes multi-lignes, définition des colonnes. +- 60-office-docs.mdc : lecture .docx via docx2txt + repli. +- 70-frontend-architecture.mdc : React.lazy/Suspense, état global, couche de services. +- 80-versioning-and-release.mdc : CHANGELOG, semver, confirmation push/tag. +- 85-release-guard.mdc : garde de release (tests/doc/build/version/changelog/tag; latest vs wip). +- 90-gitea-and-oss.mdc : fichiers open source, .gitea, CI, Gitea remote. +- 95-triage-and-problem-solving.mdc : boucle de diagnostic, REX, non-régression. + +Ces règles sont conçues pour être ajoutées au contexte de Cursor depuis l’interface (@Cursor Rules) et s’appuient sur le mécanisme de règles projet stockées dans `.cursor/rules/`. :contentReference[oaicite:3]{index=3} diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..4eba16f --- /dev/null +++ b/.cursorignore @@ -0,0 +1,24 @@ +node_modules/ +dist/ +build/ +coverage/ +.cache/ +.turbo/ +.parcel-cache/ +assets/ihm/ +web/ihm/ +logs/ +*.log +**/*.map +**/*.min.* +**/*.wasm +**/*.png +**/*.jpg +**/*.jpeg +**/*.svg +**/*.ico +**/*.pdf + +!.cursor/ + +!AGENTS.md diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..da4e36d --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,97 @@ +--- +name: Bug Report +about: Signaler un bug pour nous aider à améliorer 4NK Node +title: '[BUG] ' +labels: ['bug', 'needs-triage'] +assignees: '' +--- + +## 🐛 Description du Bug + +Description claire et concise du problème. + +## 🔄 Étapes pour Reproduire + +1. Aller à '...' +2. Cliquer sur '...' +3. Faire défiler jusqu'à '...' +4. Voir l'erreur + +## ✅ Comportement Attendu + +Description de ce qui devrait se passer. + +## ❌ Comportement Actuel + +Description de ce qui se passe actuellement. + +## 📸 Capture d'Écran + +Si applicable, ajoutez une capture d'écran pour expliquer votre problème. + +## 💻 Informations Système + +- **OS** : [ex: Ubuntu 20.04, macOS 12.0, Windows 11] +- **Docker** : [ex: 20.10.0] +- **Docker Compose** : [ex: 2.0.0] +- **Version 4NK Node** : [ex: v1.0.0] +- **Architecture** : [ex: x86_64, ARM64] + +## 📋 Configuration + +### Services Actifs +```bash +docker ps +``` + +### Variables d'Environnement +```bash +# Bitcoin Core +BITCOIN_NETWORK=signet +BITCOIN_RPC_PORT=18443 + +# Blindbit +BLINDBIT_PORT=8000 + +# SDK Relay +SDK_RELAY_PORTS=8090-8095 +``` + +## 📝 Logs + +### Logs Pertinents +``` +Logs pertinents ici +``` + +### Logs d'Erreur +``` +Logs d'erreur ici +``` + +### Logs de Debug +``` +Logs de debug ici (si RUST_LOG=debug) +``` + +## 🔧 Tentatives de Résolution + +- [ ] Redémarrage des services +- [ ] Nettoyage des volumes Docker +- [ ] Vérification de la connectivité réseau +- [ ] Mise à jour des dépendances +- [ ] Vérification de la configuration + +## 📚 Contexte Supplémentaire + +Toute autre information pertinente sur le problème. + +## 🔗 Liens Utiles + +- [Documentation](docs/) +- [Guide de Dépannage](docs/TROUBLESHOOTING.md) +- [Issues Similaires](https://git.4nkweb.com/4nk/4NK_node/issues?q=is%3Aissue+is%3Aopen+label%3Abug) + +--- + +**Merci de votre contribution !** 🙏 diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6041f4a --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,156 @@ +--- +name: Feature Request +about: Proposer une nouvelle fonctionnalité pour 4NK Node +title: '[FEATURE] ' +labels: ['enhancement', 'needs-triage'] +assignees: '' +--- + +## 🚀 Résumé + +Description claire et concise de la fonctionnalité souhaitée. + +## 💡 Motivation + +Pourquoi cette fonctionnalité est-elle nécessaire ? Quels problèmes résout-elle ? + +### Problèmes Actuels +- Problème 1 +- Problème 2 +- Problème 3 + +### Avantages de la Solution +- Avantage 1 +- Avantage 2 +- Avantage 3 + +## 🎯 Proposition + +Description détaillée de la fonctionnalité proposée. + +### Fonctionnalités Principales +- [ ] Fonctionnalité 1 +- [ ] Fonctionnalité 2 +- [ ] Fonctionnalité 3 + +### Interface Utilisateur +Description de l'interface utilisateur si applicable. + +### API Changes +Description des changements d'API si applicable. + +## 🔄 Alternatives Considérées + +Autres solutions envisagées et pourquoi elles n'ont pas été choisies. + +### Alternative 1 +- **Description** : ... +- **Pourquoi rejetée** : ... + +### Alternative 2 +- **Description** : ... +- **Pourquoi rejetée** : ... + +## 📊 Impact + +### Impact sur les Utilisateurs +- Impact positif 1 +- Impact positif 2 +- Impact négatif potentiel (si applicable) + +### Impact sur l'Architecture +- Changements nécessaires +- Compatibilité avec l'existant +- Performance + +### Impact sur la Maintenance +- Complexité ajoutée +- Tests nécessaires +- Documentation requise + +## 💻 Exemples d'Utilisation + +### Cas d'Usage 1 +```bash +# Exemple de commande ou configuration +``` + +### Cas d'Usage 2 +```python +# Exemple de code Python +``` + +### Cas d'Usage 3 +```javascript +// Exemple de code JavaScript +``` + +## 🧪 Tests + +### Tests Nécessaires +- [ ] Tests unitaires +- [ ] Tests d'intégration +- [ ] Tests de performance +- [ ] Tests de sécurité +- [ ] Tests de compatibilité + +### Scénarios de Test +- Scénario 1 +- Scénario 2 +- Scénario 3 + +## 📚 Documentation + +### Documentation Requise +- [ ] Guide d'utilisation +- [ ] Documentation API +- [ ] Exemples de code +- [ ] Guide de migration +- [ ] FAQ + +## 🔧 Implémentation + +### Étapes Proposées +1. **Phase 1** : [Description] +2. **Phase 2** : [Description] +3. **Phase 3** : [Description] + +### Estimation de Temps +- **Développement** : X jours/semaines +- **Tests** : X jours/semaines +- **Documentation** : X jours/semaines +- **Total** : X jours/semaines + +### Ressources Nécessaires +- Développeur(s) +- Testeur(s) +- Documentateur(s) +- Infrastructure + +## 🎯 Critères de Succès + +Comment mesurer le succès de cette fonctionnalité ? + +- [ ] Critère 1 +- [ ] Critère 2 +- [ ] Critère 3 + +## 🔗 Liens Utiles + +- [Documentation existante](docs/) +- [Issues similaires](https://git.4nkweb.com/4nk/4NK_node/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) +- [Roadmap](https://git.4nkweb.com/4nk/4NK_node/projects) +- [Discussions](https://git.4nkweb.com/4nk/4NK_node/issues) + +## 📋 Checklist + +- [ ] J'ai vérifié que cette fonctionnalité n'existe pas déjà +- [ ] J'ai lu la documentation existante +- [ ] J'ai vérifié les issues similaires +- [ ] J'ai fourni des exemples d'utilisation +- [ ] J'ai considéré l'impact sur l'existant +- [ ] J'ai proposé des tests + +--- + +**Merci de votre contribution à l'amélioration de 4NK Node !** 🌟 diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..621d01a --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,180 @@ +# Pull Request - 4NK Node + +## 📋 Description + +Description claire et concise des changements apportés. + +### Type de Changement +- [ ] 🐛 Bug fix +- [ ] ✨ Nouvelle fonctionnalité +- [ ] 📚 Documentation +- [ ] 🧪 Tests +- [ ] 🔧 Refactoring +- [ ] 🚀 Performance +- [ ] 🔒 Sécurité +- [ ] 🎨 Style/UI +- [ ] 🏗️ Architecture +- [ ] 📦 Build/CI + +### Composants Affectés +- [ ] Bitcoin Core +- [ ] Blindbit +- [ ] SDK Relay +- [ ] Tor +- [ ] Docker/Infrastructure +- [ ] Tests +- [ ] Documentation +- [ ] Scripts + +## 🔗 Issue(s) Liée(s) + +Fixes #(issue) +Relates to #(issue) + +## 🧪 Tests + +### Tests Exécutés +- [ ] Tests unitaires +- [ ] Tests d'intégration +- [ ] Tests de connectivité +- [ ] Tests externes +- [ ] Tests de performance + +### Commandes de Test +```bash +# Tests complets +./tests/run_all_tests.sh + +# Tests spécifiques +./tests/run_unit_tests.sh +./tests/run_integration_tests.sh +``` + +### Résultats des Tests +``` +Résultats des tests ici +``` + +## 📸 Captures d'Écran + +Si applicable, ajoutez des captures d'écran pour les changements visuels. + +## 🔧 Changements Techniques + +### Fichiers Modifiés +- `fichier1.rs` - Description des changements +- `fichier2.py` - Description des changements +- `docker-compose.yml` - Description des changements + +### Nouveaux Fichiers +- `nouveau_fichier.rs` - Description +- `nouveau_script.sh` - Description + +### Fichiers Supprimés +- `ancien_fichier.rs` - Raison de la suppression + +### Changements de Configuration +```yaml +# Exemple de changement de configuration +service: + new_option: value +``` + +## 📚 Documentation + +### Documentation Mise à Jour +- [ ] README.md +- [ ] docs/INSTALLATION.md +- [ ] docs/USAGE.md +- [ ] docs/API.md +- [ ] docs/ARCHITECTURE.md + +### Nouvelle Documentation +- [ ] Nouveau guide créé +- [ ] Exemples ajoutés +- [ ] API documentée + +## 🔍 Code Review Checklist + +### Code Quality +- [ ] Le code suit les standards du projet +- [ ] Les noms de variables/fonctions sont clairs +- [ ] Les commentaires sont appropriés +- [ ] Pas de code mort ou commenté +- [ ] Gestion d'erreurs appropriée + +### Performance +- [ ] Pas de régression de performance +- [ ] Optimisations appliquées si nécessaire +- [ ] Tests de performance ajoutés + +### Sécurité +- [ ] Pas de vulnérabilités introduites +- [ ] Validation des entrées utilisateur +- [ ] Gestion sécurisée des secrets + +### Tests +- [ ] Couverture de tests suffisante +- [ ] Tests pour les cas d'erreur +- [ ] Tests d'intégration si nécessaire + +### Documentation +- [ ] Code auto-documenté +- [ ] Documentation mise à jour +- [ ] Exemples fournis + +## 🚀 Déploiement + +### Impact sur le Déploiement +- [ ] Aucun impact +- [ ] Migration de données requise +- [ ] Changement de configuration +- [ ] Redémarrage des services + +### Étapes de Déploiement +```bash +# Étapes pour déployer les changements +``` + +## 📊 Métriques + +### Impact sur les Performances +- Temps de réponse : +/- X% +- Utilisation mémoire : +/- X% +- Utilisation CPU : +/- X% + +### Impact sur la Stabilité +- Taux d'erreur : +/- X% +- Disponibilité : +/- X% + +## 🔄 Compatibilité + +### Compatibilité Ascendante +- [ ] Compatible avec les versions précédentes +- [ ] Migration automatique +- [ ] Migration manuelle requise + +### Compatibilité Descendante +- [ ] Compatible avec les futures versions +- [ ] API stable +- [ ] Configuration stable + +## 🎯 Critères de Succès + +- [ ] Critère 1 +- [ ] Critère 2 +- [ ] Critère 3 + +## 📝 Notes Supplémentaires + +Informations supplémentaires importantes pour les reviewers. + +## 🔗 Liens Utiles + +- [Documentation](docs/) +- [Tests](tests/) +- [Issues liées](https://git.4nkweb.com/4nk/4NK_node/issues) + +--- + +**Merci pour votre contribution !** 🙏 diff --git a/.gitea/workflows/LOCAL_OVERRIDES.yml b/.gitea/workflows/LOCAL_OVERRIDES.yml new file mode 100644 index 0000000..12c8c45 --- /dev/null +++ b/.gitea/workflows/LOCAL_OVERRIDES.yml @@ -0,0 +1,14 @@ +# LOCAL_OVERRIDES.yml — dérogations locales contrôlées +overrides: + - path: ".gitea/workflows/ci.yml" + reason: "spécificité d’environnement" + owner: "@maintainer_handle" + expires: "2025-12-31" + - path: "scripts/auto-ssh-push.sh" + reason: "flux particulier temporaire" + owner: "@maintainer_handle" + expires: "2025-10-01" +policy: + allow_only_listed_paths: true + require_expiry: true + audit_in_ci: true diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..5dd8de7 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,345 @@ +name: CI - 4NK Node + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +env: + RUST_VERSION: '1.70' + DOCKER_COMPOSE_VERSION: '2.20.0' + +jobs: + # Job de vérification du code + code-quality: + name: Code Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run clippy + run: | + cd sdk_relay + cargo clippy --all-targets --all-features -- -D warnings + + - name: Run rustfmt + run: | + cd sdk_relay + cargo fmt --all -- --check + + - name: Check documentation + run: | + cd sdk_relay + cargo doc --no-deps + + - name: Check for TODO/FIXME + run: | + if grep -r "TODO\|FIXME" . --exclude-dir=.git --exclude-dir=target; then + echo "Found TODO/FIXME comments. Please address them." + exit 1 + fi + + # Job de tests unitaires + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run unit tests + run: | + cd sdk_relay + cargo test --lib --bins + + - name: Run integration tests + run: | + cd sdk_relay + cargo test --tests + + # Job de tests d'intégration + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + + services: + docker: + image: docker:24.0.5 + options: >- + --health-cmd "docker info" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 2375:2375 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker images + run: | + docker build -t 4nk-node-bitcoin ./bitcoin + docker build -t 4nk-node-blindbit ./blindbit + docker build -t 4nk-node-sdk-relay -f ./sdk_relay/Dockerfile .. + + - name: Run integration tests + run: | + # Tests de connectivité de base + ./tests/run_connectivity_tests.sh || true + + # Tests d'intégration + ./tests/run_integration_tests.sh || true + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: | + tests/logs/ + tests/reports/ + retention-days: 7 + + # Job de tests de sécurité + security-tests: + name: Security Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Run cargo audit + run: | + cd sdk_relay + cargo audit --deny warnings + + - name: Check for secrets + run: | + # Vérifier les secrets potentiels + if grep -r "password\|secret\|key\|token" . --exclude-dir=.git --exclude-dir=target --exclude=*.md; then + echo "Potential secrets found. Please review." + exit 1 + fi + + - name: Check file permissions + run: | + # Vérifier les permissions sensibles + find . -type f -perm /0111 -name "*.conf" -o -name "*.key" -o -name "*.pem" | while read file; do + if [[ $(stat -c %a "$file") != "600" ]]; then + echo "Warning: $file has insecure permissions" + fi + done + + # Job de build et test Docker + docker-build: + name: Docker Build & Test + runs-on: ubuntu-latest + + services: + docker: + image: docker:24.0.5 + options: >- + --health-cmd "docker info" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 2375:2375 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and test Bitcoin Core + run: | + docker build -t 4nk-node-bitcoin:test ./bitcoin + docker run --rm 4nk-node-bitcoin:test bitcoin-cli --version + + - name: Build and test Blindbit + run: | + docker build -t 4nk-node-blindbit:test ./blindbit + docker run --rm 4nk-node-blindbit:test --version || true + + - name: Build and test SDK Relay + run: | + docker build -t 4nk-node-sdk-relay:test -f ./sdk_relay/Dockerfile .. + docker run --rm 4nk-node-sdk-relay:test --version || true + + - name: Test Docker Compose + run: | + docker-compose config + docker-compose build --no-cache + + # Job de tests de documentation + documentation-tests: + name: Documentation Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Check markdown links + run: | + # Vérification basique des liens markdown + find . -name "*.md" -exec grep -l "\[.*\](" {} \; | while read file; do + echo "Checking links in $file" + done + + - name: Check documentation structure + run: | + # Vérifier la présence des fichiers de documentation essentiels + required_files=( + "README.md" + "LICENSE" + "CONTRIBUTING.md" + "CHANGELOG.md" + "CODE_OF_CONDUCT.md" + "SECURITY.md" + "docs/INDEX.md" + "docs/INSTALLATION.md" + "docs/USAGE.md" + ) + + for file in "${required_files[@]}"; do + if [[ ! -f "$file" ]]; then + echo "Missing required documentation file: $file" + exit 1 + fi + done + + - name: Validate documentation + run: | + # Vérifier la cohérence de la documentation + if ! grep -q "4NK Node" README.md; then + echo "README.md should mention '4NK Node'" + exit 1 + fi + + # Job de release guard (cohérence release) + release-guard: + name: Release Guard + runs-on: ubuntu-latest + needs: [code-quality, unit-tests, documentation-tests] + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Ensure guard scripts are executable + run: | + chmod +x scripts/release/guard.sh || true + chmod +x scripts/checks/version_alignment.sh || true + + - name: Version alignment check + run: | + if [ -f scripts/checks/version_alignment.sh ]; then + ./scripts/checks/version_alignment.sh + else + echo "No version alignment script (ok)" + fi + + - name: Release guard (CI verify) + env: + RELEASE_TYPE: ci-verify + run: | + if [ -f scripts/release/guard.sh ]; then + ./scripts/release/guard.sh + else + echo "No guard script (ok)" + fi + + # Job de tests de performance + performance-tests: + name: Performance Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Run performance tests + run: | + cd sdk_relay + cargo test --release --test performance_tests || true + + - name: Check memory usage + run: | + # Tests de base de consommation mémoire + echo "Performance tests completed" + + # Job de notification + notify: + name: Notify + runs-on: ubuntu-latest + needs: [code-quality, unit-tests, integration-tests, security-tests, docker-build, documentation-tests] + if: always() + + steps: + - name: Notify success + if: needs.code-quality.result == 'success' && needs.unit-tests.result == 'success' && needs.integration-tests.result == 'success' && needs.security-tests.result == 'success' && needs.docker-build.result == 'success' && needs.documentation-tests.result == 'success' + run: | + echo "✅ All tests passed successfully!" + + - name: Notify failure + if: needs.code-quality.result == 'failure' || needs.unit-tests.result == 'failure' || needs.integration-tests.result == 'failure' || needs.security-tests.result == 'failure' || needs.docker-build.result == 'failure' || needs.documentation-tests.result == 'failure' + run: | + echo "❌ Some tests failed!" + exit 1 diff --git a/.gitea/workflows/template-sync.yml b/.gitea/workflows/template-sync.yml new file mode 100644 index 0000000..e6710df --- /dev/null +++ b/.gitea/workflows/template-sync.yml @@ -0,0 +1,39 @@ +# .gitea/workflows/template-sync.yml — synchronisation et contrôles d’intégrité +name: 4NK Template Sync +on: + schedule: # planification régulière + - cron: "0 4 * * 1" # exécution hebdomadaire (UTC) + workflow_dispatch: {} # déclenchement manuel + +jobs: + check-and-sync: + runs-on: linux + steps: + - name: Lire TEMPLATE_VERSION et .4nk-sync.yml + # Doit charger ref courant, source_repo et périmètre paths + + - name: Récupérer la version publiée du template/4NK_rules + # Doit comparer TEMPLATE_VERSION avec ref amont + + - name: Créer branche de synchronisation si divergence + # Doit créer chore/template-sync- et préparer un commit + + - name: Synchroniser les chemins autoritatifs + # Doit mettre à jour .cursor/**, .gitea/**, AGENTS.md, scripts/**, docs/SSH_UPDATE.md + + - name: Contrôles post-sync (bloquants) + # 1) Vérifier présence et exécutable des scripts/*.sh + # 2) Vérifier mise à jour CHANGELOG.md et docs/INDEX.md + # 3) Vérifier docs/SSH_UPDATE.md si scripts/** a changé + # 4) Vérifier absence de secrets en clair dans scripts/** + # 5) Vérifier manifest_checksum si publié + + - name: Tests, lint, sécurité statique + # Doit exiger un état vert + + - name: Ouvrir PR de synchronisation + # Titre: "[template-sync] chore: aligner .cursor/.gitea/AGENTS.md/scripts" + # Doit inclure résumé des fichiers modifiés et la version appliquée + + - name: Mettre à jour TEMPLATE_VERSION (dans PR) + # Doit remplacer la valeur par la ref appliquée diff --git a/.gitea_template/ISSUE_TEMPLATE/bug_report.md b/.gitea_template/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..0b144dc --- /dev/null +++ b/.gitea_template/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,99 @@ +--- +name: Bug Report +about: Signaler un bug pour nous aider à améliorer 4NK Node +title: '[BUG] ' +labels: ['bug', 'needs-triage'] +assignees: '' +--- + +> Ce fichier est un modèle (template). Adaptez les champs à votre projet dérivé. + +## 🐛 Description du Bug + +Description claire et concise du problème. + +## 🔄 Étapes pour Reproduire + +1. Aller à '...' +2. Cliquer sur '...' +3. Faire défiler jusqu'à '...' +4. Voir l'erreur + +## ✅ Comportement Attendu + +Description de ce qui devrait se passer. + +## ❌ Comportement Actuel + +Description de ce qui se passe actuellement. + +## 📸 Capture d'Écran + +Si applicable, ajoutez une capture d'écran pour expliquer votre problème. + +## 💻 Informations Système + +- **OS** : [ex: Ubuntu 20.04, macOS 12.0, Windows 11] +- **Docker** : [ex: 20.10.0] +- **Docker Compose** : [ex: 2.0.0] +- **Version 4NK Node** : [ex: v1.0.0] +- **Architecture** : [ex: x86_64, ARM64] + +## 📋 Configuration + +### Services Actifs +```bash +docker ps +``` + +### Variables d'Environnement +```bash +# Bitcoin Core +BITCOIN_NETWORK=signet +BITCOIN_RPC_PORT=18443 + +# Blindbit +BLINDBIT_PORT=8000 + +# SDK Relay +SDK_RELAY_PORTS=8090-8095 +``` + +## 📝 Logs + +### Logs Pertinents +``` +Logs pertinents ici +``` + +### Logs d'Erreur +``` +Logs d'erreur ici +``` + +### Logs de Debug +``` +Logs de debug ici (si RUST_LOG=debug) +``` + +## 🔧 Tentatives de Résolution + +- [ ] Redémarrage des services +- [ ] Nettoyage des volumes Docker +- [ ] Vérification de la connectivité réseau +- [ ] Mise à jour des dépendances +- [ ] Vérification de la configuration + +## 📚 Contexte Supplémentaire + +Toute autre information pertinente sur le problème. + +## 🔗 Liens Utiles + +- [Documentation](docs/) +- [Guide de Dépannage](docs/TROUBLESHOOTING.md) +- [Issues Similaires](https://git.4nkweb.com/4nk/4NK_node/issues?q=is%3Aissue+is%3Aopen+label%3Abug) + +--- + +**Merci de votre contribution !** 🙏 diff --git a/.gitea_template/ISSUE_TEMPLATE/feature_request.md b/.gitea_template/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..4b8c506 --- /dev/null +++ b/.gitea_template/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,158 @@ +--- +name: Feature Request +about: Proposer une nouvelle fonctionnalité pour 4NK Node +title: '[FEATURE] ' +labels: ['enhancement', 'needs-triage'] +assignees: '' +--- + +> Ce fichier est un modèle (template). Adaptez les champs à votre projet dérivé. + +## 🚀 Résumé + +Description claire et concise de la fonctionnalité souhaitée. + +## 💡 Motivation + +Pourquoi cette fonctionnalité est-elle nécessaire ? Quels problèmes résout-elle ? + +### Problèmes Actuels +- Problème 1 +- Problème 2 +- Problème 3 + +### Avantages de la Solution +- Avantage 1 +- Avantage 2 +- Avantage 3 + +## 🎯 Proposition + +Description détaillée de la fonctionnalité proposée. + +### Fonctionnalités Principales +- [ ] Fonctionnalité 1 +- [ ] Fonctionnalité 2 +- [ ] Fonctionnalité 3 + +### Interface Utilisateur +Description de l'interface utilisateur si applicable. + +### API Changes +Description des changements d'API si applicable. + +## 🔄 Alternatives Considérées + +Autres solutions envisagées et pourquoi elles n'ont pas été choisies. + +### Alternative 1 +- **Description** : ... +- **Pourquoi rejetée** : ... + +### Alternative 2 +- **Description** : ... +- **Pourquoi rejetée** : ... + +## 📊 Impact + +### Impact sur les Utilisateurs +- Impact positif 1 +- Impact positif 2 +- Impact négatif potentiel (si applicable) + +### Impact sur l'Architecture +- Changements nécessaires +- Compatibilité avec l'existant +- Performance + +### Impact sur la Maintenance +- Complexité ajoutée +- Tests nécessaires +- Documentation requise + +## 💻 Exemples d'Utilisation + +### Cas d'Usage 1 +```bash +# Exemple de commande ou configuration +``` + +### Cas d'Usage 2 +```python +# Exemple de code Python +``` + +### Cas d'Usage 3 +```javascript +// Exemple de code JavaScript +``` + +## 🧪 Tests + +### Tests Nécessaires +- [ ] Tests unitaires +- [ ] Tests d'intégration +- [ ] Tests de performance +- [ ] Tests de sécurité +- [ ] Tests de compatibilité + +### Scénarios de Test +- Scénario 1 +- Scénario 2 +- Scénario 3 + +## 📚 Documentation + +### Documentation Requise +- [ ] Guide d'utilisation +- [ ] Documentation API +- [ ] Exemples de code +- [ ] Guide de migration +- [ ] FAQ + +## 🔧 Implémentation + +### Étapes Proposées +1. **Phase 1** : [Description] +2. **Phase 2** : [Description] +3. **Phase 3** : [Description] + +### Estimation de Temps +- **Développement** : X jours/semaines +- **Tests** : X jours/semaines +- **Documentation** : X jours/semaines +- **Total** : X jours/semaines + +### Ressources Nécessaires +- Développeur(s) +- Testeur(s) +- Documentateur(s) +- Infrastructure + +## 🎯 Critères de Succès + +Comment mesurer le succès de cette fonctionnalité ? + +- [ ] Critère 1 +- [ ] Critère 2 +- [ ] Critère 3 + +## 🔗 Liens Utiles + +- [Documentation existante](docs/) +- [Issues similaires](https://git.4nkweb.com/4nk/4NK_node/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) +- [Roadmap](https://git.4nkweb.com/4nk/4NK_node/projects) +- [Discussions](https://git.4nkweb.com/4nk/4NK_node/issues) + +## 📋 Checklist + +- [ ] J'ai vérifié que cette fonctionnalité n'existe pas déjà +- [ ] J'ai lu la documentation existante +- [ ] J'ai vérifié les issues similaires +- [ ] J'ai fourni des exemples d'utilisation +- [ ] J'ai considéré l'impact sur l'existant +- [ ] J'ai proposé des tests + +--- + +**Merci de votre contribution à l'amélioration de 4NK Node !** 🌟 diff --git a/.gitea_template/PULL_REQUEST_TEMPLATE.md b/.gitea_template/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..a406182 --- /dev/null +++ b/.gitea_template/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,183 @@ +# Pull Request - 4NK Node + +> Ce fichier est un modèle PR. Adaptez les sections à votre projet dérivé. + +## 📋 Description + +Description claire et concise des changements apportés. + +### Type de Changement +- [ ] 🐛 Bug fix +- [ ] ✨ Nouvelle fonctionnalité +- [ ] 📚 Documentation +- [ ] 🧪 Tests +- [ ] 🔧 Refactoring +- [ ] 🚀 Performance +- [ ] 🔒 Sécurité +- [ ] 🎨 Style/UI +- [ ] 🏗️ Architecture +- [ ] 📦 Build/CI + +### Composants Affectés +- [ ] Bitcoin Core +- [ ] Blindbit +- [ ] SDK Relay +- [ ] Tor +- [ ] Docker/Infrastructure +- [ ] Tests +- [ ] Documentation +- [ ] Scripts + +## 🔗 Issue(s) Liée(s) + +Fixes #(issue) +Relates to #(issue) + +## 🧪 Tests + +### Tests Exécutés +- [ ] Tests unitaires +- [ ] Tests d'intégration +- [ ] Tests de connectivité +- [ ] Tests externes +- [ ] Tests de performance +- [ ] Release Guard local (`RELEASE_TYPE=ci-verify scripts/release/guard.sh`) + +### Commandes de Test +```bash +# Tests complets +./tests/run_all_tests.sh + +# Tests spécifiques +./tests/run_unit_tests.sh +./tests/run_integration_tests.sh +``` + +### Résultats des Tests +``` +Résultats des tests ici +``` + +## 📸 Captures d'Écran + +Si applicable, ajoutez des captures d'écran pour les changements visuels. + +## 🔧 Changements Techniques + +### Fichiers Modifiés +- `fichier1.rs` - Description des changements +- `fichier2.py` - Description des changements +- `docker-compose.yml` - Description des changements + +### Nouveaux Fichiers +- `nouveau_fichier.rs` - Description +- `nouveau_script.sh` - Description + +### Fichiers Supprimés +- `ancien_fichier.rs` - Raison de la suppression + +### Changements de Configuration +```yaml +# Exemple de changement de configuration +service: + new_option: value +``` + +## 📚 Documentation + +### Documentation Mise à Jour +- [ ] README.md +- [ ] docs/INSTALLATION.md +- [ ] docs/USAGE.md +- [ ] docs/API.md +- [ ] docs/ARCHITECTURE.md + +### Nouvelle Documentation +- [ ] Nouveau guide créé +- [ ] Exemples ajoutés +- [ ] API documentée + +## 🔍 Code Review Checklist + +### Code Quality +- [ ] Le code suit les standards du projet +- [ ] Les noms de variables/fonctions sont clairs +- [ ] Les commentaires sont appropriés +- [ ] Pas de code mort ou commenté +- [ ] Gestion d'erreurs appropriée + +### Performance +- [ ] Pas de régression de performance +- [ ] Optimisations appliquées si nécessaire +- [ ] Tests de performance ajoutés + +### Sécurité +- [ ] Pas de vulnérabilités introduites +- [ ] Validation des entrées utilisateur +- [ ] Gestion sécurisée des secrets + +### Tests +- [ ] Couverture de tests suffisante +- [ ] Tests pour les cas d'erreur +- [ ] Tests d'intégration si nécessaire + +### Documentation +- [ ] Code auto-documenté +- [ ] Documentation mise à jour +- [ ] Exemples fournis + +## 🚀 Déploiement + +### Impact sur le Déploiement +- [ ] Aucun impact +- [ ] Migration de données requise +- [ ] Changement de configuration +- [ ] Redémarrage des services + +### Étapes de Déploiement +```bash +# Étapes pour déployer les changements +``` + +## 📊 Métriques + +### Impact sur les Performances +- Temps de réponse : +/- X% +- Utilisation mémoire : +/- X% +- Utilisation CPU : +/- X% + +### Impact sur la Stabilité +- Taux d'erreur : +/- X% +- Disponibilité : +/- X% + +## 🔄 Compatibilité + +### Compatibilité Ascendante +- [ ] Compatible avec les versions précédentes +- [ ] Migration automatique +- [ ] Migration manuelle requise + +### Compatibilité Descendante +- [ ] Compatible avec les futures versions +- [ ] API stable +- [ ] Configuration stable + +## 🎯 Critères de Succès + +- [ ] Critère 1 +- [ ] Critère 2 +- [ ] Critère 3 + +## 📝 Notes Supplémentaires + +Informations supplémentaires importantes pour les reviewers. + +## 🔗 Liens Utiles + +- [Documentation](docs/) +- [Tests](tests/) +- [Issues liées](https://git.4nkweb.com/4nk/4NK_node/issues) + +--- + +**Merci pour votre contribution !** 🙏 diff --git a/.gitea_template/workflows/LOCAL_OVERRIDES.yml b/.gitea_template/workflows/LOCAL_OVERRIDES.yml new file mode 100644 index 0000000..789bc91 --- /dev/null +++ b/.gitea_template/workflows/LOCAL_OVERRIDES.yml @@ -0,0 +1,14 @@ +# LOCAL_OVERRIDES.yml — dérogations locales contrôlées (fichier modèle) +overrides: + - path: ".gitea/workflows/ci.yml" + reason: "spécificité d’environnement" + owner: "@maintainer_handle" + expires: "2025-12-31" + - path: "scripts/scripts/auto-ssh-push.sh" + reason: "flux particulier temporaire" + owner: "@maintainer_handle" + expires: "2025-10-01" +policy: + allow_only_listed_paths: true + require_expiry: true + audit_in_ci: true diff --git a/.gitea_template/workflows/ci.yml b/.gitea_template/workflows/ci.yml new file mode 100644 index 0000000..058515a --- /dev/null +++ b/.gitea_template/workflows/ci.yml @@ -0,0 +1,346 @@ +# Template CI - 4NK Node (ce fichier est un modèle, adaptez selon votre projet) +name: CI - 4NK Node + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +env: + RUST_VERSION: '1.70' + DOCKER_COMPOSE_VERSION: '2.20.0' + +jobs: + # Job de vérification du code + code-quality: + name: Code Quality + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run clippy + run: | + cd sdk_relay + cargo clippy --all-targets --all-features -- -D warnings + + - name: Run rustfmt + run: | + cd sdk_relay + cargo fmt --all -- --check + + - name: Check documentation + run: | + cd sdk_relay + cargo doc --no-deps + + - name: Check for TODO/FIXME + run: | + if grep -r "TODO\|FIXME" . --exclude-dir=.git --exclude-dir=target; then + echo "Found TODO/FIXME comments. Please address them." + exit 1 + fi + + # Job de tests unitaires + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - name: Run unit tests + run: | + cd sdk_relay + cargo test --lib --bins + + - name: Run integration tests + run: | + cd sdk_relay + cargo test --tests + + # Job de tests d'intégration + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + + services: + docker: + image: docker:24.0.5 + options: >- + --health-cmd "docker info" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 2375:2375 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker images + run: | + docker build -t 4nk-node-bitcoin ./bitcoin + docker build -t 4nk-node-blindbit ./blindbit + docker build -t 4nk-node-sdk-relay -f ./sdk_relay/Dockerfile .. + + - name: Run integration tests + run: | + # Tests de connectivité de base + ./tests/run_connectivity_tests.sh || true + + # Tests d'intégration + ./tests/run_integration_tests.sh || true + + - name: Upload test results + uses: actions/upload-artifact@v3 + if: always() + with: + name: test-results + path: | + tests/logs/ + tests/reports/ + retention-days: 7 + + # Job de tests de sécurité + security-tests: + name: Security Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Run cargo audit + run: | + cd sdk_relay + cargo audit --deny warnings + + - name: Check for secrets + run: | + # Vérifier les secrets potentiels + if grep -r "password\|secret\|key\|token" . --exclude-dir=.git --exclude-dir=target --exclude=*.md; then + echo "Potential secrets found. Please review." + exit 1 + fi + + - name: Check file permissions + run: | + # Vérifier les permissions sensibles + find . -type f -perm /0111 -name "*.conf" -o -name "*.key" -o -name "*.pem" | while read file; do + if [[ $(stat -c %a "$file") != "600" ]]; then + echo "Warning: $file has insecure permissions" + fi + done + + # Job de build et test Docker + docker-build: + name: Docker Build & Test + runs-on: ubuntu-latest + + services: + docker: + image: docker:24.0.5 + options: >- + --health-cmd "docker info" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 2375:2375 + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and test Bitcoin Core + run: | + docker build -t 4nk-node-bitcoin:test ./bitcoin + docker run --rm 4nk-node-bitcoin:test bitcoin-cli --version + + - name: Build and test Blindbit + run: | + docker build -t 4nk-node-blindbit:test ./blindbit + docker run --rm 4nk-node-blindbit:test --version || true + + - name: Build and test SDK Relay + run: | + docker build -t 4nk-node-sdk-relay:test -f ./sdk_relay/Dockerfile .. + docker run --rm 4nk-node-sdk-relay:test --version || true + + - name: Test Docker Compose + run: | + docker-compose config + docker-compose build --no-cache + + # Job de tests de documentation + documentation-tests: + name: Documentation Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Check markdown links + run: | + # Vérification basique des liens markdown + find . -name "*.md" -exec grep -l "\[.*\](" {} \; | while read file; do + echo "Checking links in $file" + done + + - name: Check documentation structure + run: | + # Vérifier la présence des fichiers de documentation essentiels + required_files=( + "README.md" + "LICENSE" + "CONTRIBUTING.md" + "CHANGELOG.md" + "CODE_OF_CONDUCT.md" + "SECURITY.md" + "docs/INDEX.md" + "docs/INSTALLATION.md" + "docs/USAGE.md" + ) + + for file in "${required_files[@]}"; do + if [[ ! -f "$file" ]]; then + echo "Missing required documentation file: $file" + exit 1 + fi + done + + - name: Validate documentation + run: | + # Vérifier la cohérence de la documentation + if ! grep -q "4NK Node" README.md; then + echo "README.md should mention '4NK Node'" + exit 1 + fi + + # Job de release guard (cohérence release) + release-guard: + name: Release Guard + runs-on: ubuntu-latest + needs: [code-quality, unit-tests, documentation-tests] + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Ensure guard scripts are executable + run: | + chmod +x scripts/release/guard.sh || true + chmod +x scripts/checks/version_alignment.sh || true + + - name: Version alignment check + run: | + if [ -f scripts/checks/version_alignment.sh ]; then + ./scripts/checks/version_alignment.sh + else + echo "No version alignment script (ok)" + fi + + - name: Release guard (CI verify) + env: + RELEASE_TYPE: ci-verify + run: | + if [ -f scripts/release/guard.sh ]; then + ./scripts/release/guard.sh + else + echo "No guard script (ok)" + fi + + # Job de tests de performance + performance-tests: + name: Performance Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + override: true + + - name: Run performance tests + run: | + cd sdk_relay + cargo test --release --test performance_tests || true + + - name: Check memory usage + run: | + # Tests de base de consommation mémoire + echo "Performance tests completed" + + # Job de notification + notify: + name: Notify + runs-on: ubuntu-latest + needs: [code-quality, unit-tests, integration-tests, security-tests, docker-build, documentation-tests] + if: always() + + steps: + - name: Notify success + if: needs.code-quality.result == 'success' && needs.unit-tests.result == 'success' && needs.integration-tests.result == 'success' && needs.security-tests.result == 'success' && needs.docker-build.result == 'success' && needs.documentation-tests.result == 'success' + run: | + echo "✅ All tests passed successfully!" + + - name: Notify failure + if: needs.code-quality.result == 'failure' || needs.unit-tests.result == 'failure' || needs.integration-tests.result == 'failure' || needs.security-tests.result == 'failure' || needs.docker-build.result == 'failure' || needs.documentation-tests.result == 'failure' + run: | + echo "❌ Some tests failed!" + exit 1 diff --git a/.gitea_template/workflows/template-sync.yml b/.gitea_template/workflows/template-sync.yml new file mode 100644 index 0000000..132c4af --- /dev/null +++ b/.gitea_template/workflows/template-sync.yml @@ -0,0 +1,39 @@ +# .gitea/workflows/template-sync.yml — synchronisation et contrôles d’intégrité (fichier modèle) +name: 4NK Template Sync +on: + schedule: # planification régulière + - cron: "0 4 * * 1" # exécution hebdomadaire (UTC) + workflow_dispatch: {} # déclenchement manuel + +jobs: + check-and-sync: + runs-on: ubuntu-latest + steps: + - name: Lire TEMPLATE_VERSION et .4nk-sync.yml + # Doit charger ref courant, source_repo et périmètre paths + + - name: Récupérer la version publiée du template/4NK_rules + # Doit comparer TEMPLATE_VERSION avec ref amont + + - name: Créer branche de synchronisation si divergence + # Doit créer chore/template-sync- et préparer un commit + + - name: Synchroniser les chemins autoritatifs + # Doit mettre à jour .cursor/**, .gitea/**, AGENTS.md, scripts/**, docs/SSH_UPDATE.md + + - name: Contrôles post-sync (bloquants) + # 1) Vérifier présence et exécutable des scripts/*.sh + # 2) Vérifier mise à jour CHANGELOG.md et docs/INDEX.md + # 3) Vérifier docs/SSH_UPDATE.md si scripts/** a changé + # 4) Vérifier absence de secrets en clair dans scripts/** + # 5) Vérifier manifest_checksum si publié + + - name: Tests, lint, sécurité statique + # Doit exiger un état vert + + - name: Ouvrir PR de synchronisation + # Titre: "[template-sync] chore: aligner .cursor/.gitea/AGENTS.md/scripts" + # Doit inclure résumé des fichiers modifiés et la version appliquée + + - name: Mettre à jour TEMPLATE_VERSION (dans PR) + # Doit remplacer la valeur par la ref appliquée diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6eb056c --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Dépendances +node_modules/ + +# Builds / artefacts +/dist/ +/build/ +/coverage/ +/.cache/ +/.turbo/ +/.parcel-cache/ +/tmp/ + +# Sorties synchronisées depuis ihm_client +/assets/ihm/ +/web/ihm/ + +# Journaux +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +*.log + +# Environnements +.env +.env.*.local + +# Éditeurs / OS +.DS_Store +.vscode/ +.idea/ + +# Jest +.jest-cache/ +.jest/ + +!.cursor/ + +!AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3350766 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,263 @@ +# AGENTS.md + +## Table des matières +- [Introduction](#introduction) +- [Principes communs](#principes-communs) +- [Agents fondamentaux](#agents-fondamentaux) +- [Agents spécialisés documentation](#agents-spécialisés-documentation) +- [Agents spécialisés tests](#agents-spécialisés-tests) +- [Agents techniques](#agents-techniques) +- [Agents frontend](#agents-frontend) +- [Agents open source et CI](#agents-open-source-et-ci) +- [Agents de synchronisation et dérogations](#agents-de-synchronisation-et-d%C3%A9rogations) +- [Matrice de coordination](#matrice-de-coordination) +- [Conclusion](#conclusion) + +--- + +## Introduction +Ce document définit les agents, leurs rôles et leurs responsabilités dans le projet `4NK/4NK_node` et, par extension, tout dépôt dérivé de `4NK_project_template`. +Il impose une coordination stricte entre code, documentation, tests, dépendances, CI/CD, synchronisation de template et gouvernance open source. +Les règles opérationnelles détaillées sont précisées dans `.cursor/rules/` (notamment `41-ssh-automation.mdc` et `42-template-sync.mdc`). + +--- + +## Principes communs +- Langue exclusive : français. +- Pas d’exemples de code applicatif injectés dans la base. +- Toute contribution doit contenir une introduction et/ou une conclusion. +- Interdiction de secrets en clair dans le dépôt. +- Confirmation nécessaire avant `push` et `tag`. +- Toute modification impactant des éléments normatifs doit mettre à jour la documentation et le changelog. + - Le flux de publication applique un garde de release (tests/doc/build/alignement version/changelog/tag, latest vs wip). + +--- + +## Agents fondamentaux + +### Agent Fondation (Responsable) +**Missions** +- Garantir la conformité éditoriale : français, pas d’exemples applicatifs, introduction/conclusion. +- Vérifier la cohérence terminologique. + +**Artefacts** +- Tous fichiers. + +--- + +### Agent Structure (Responsable) +**Missions** +- Maintenir l’arborescence canonique (incluant `.cursor/`, `.gitea/`, `scripts/`, `docs/SSH_UPDATE.md`). +- Archiver le contenu obsolète dans `archive/` avec métadonnées. +- Interdire toute suppression non tracée. + +**Artefacts** +- `archive/`, `docs/**`, `tests/**`, `.cursor/**`, `.gitea/**`, `scripts/**`, `CHANGELOG.md`. + +--- + +## Agents spécialisés documentation + +### Agent Documentation (Responsable) +**Missions** +- Mettre à jour `docs/**` selon l’impact des changements. +- Tenir `docs/INDEX.md` comme table des matières centrale. +- Produire des REX techniques dans `archive/` en cas d’investigations multiples. + +**Artefacts** +- `docs/**`, `README.md`, `archive/**`. + +--- + +### Agent Données CSV (Responsable) +**Missions** +- Traiter les CSV comme source des modèles de données (en-têtes multi-lignes inclus). +- Exiger une définition complète de toutes les colonnes. +- Corriger et documenter les incohérences de types. + +**Artefacts** +- `docs/API.md`, `docs/ARCHITECTURE.md`, `docs/USAGE.md`. + +--- + +### Agent Documents bureautiques (Consulté) +**Missions** +- Lire `.docx` via `docx2txt`; proposer des alternatives en cas d’échec. +- Documenter les imports dans `docs/INDEX.md`. + +**Artefacts** +- `docs/**`, `archive/**`. + +--- + +## Agents spécialisés tests + +### Agent Tests (Responsable) +**Missions** +- Couvrir `unit`, `integration`, `connectivity`, `performance`, `external`. +- Gérer `tests/logs` et `tests/reports`. +- Exiger des tests verts avant commit. + +**Artefacts** +- `tests/**`, `docs/TESTING.md`. + +--- + +### Agent Performance (Consulté) +**Missions** +- Réaliser des benchmarks reproductibles. +- Valider l’impact performance avant fusion. + +**Artefacts** +- `tests/performance/`, `tests/reports/`, `docs/TESTING.md`. + +--- + +## Agents techniques + +### Agent Dépendances (Responsable) +**Missions** +- Ajouter les dépendances manquantes lorsque justifié. +- Vérifier les dernières versions stables. +- Documenter les impacts dans `ARCHITECTURE.md`, `CONFIGURATION.md`, `CHANGELOG.md`. + +**Artefacts** +- `docs/ARCHITECTURE.md`, `docs/CONFIGURATION.md`, `CHANGELOG.md`. + +--- + +### Agent Compilation (Responsable) +**Missions** +- Compiler très régulièrement et aux étapes critiques. +- Bloquer toute progression en cas d’erreurs de build/runtime. + +**Artefacts** +- Artefacts de build, scripts d’outillage. + +--- + +### Agent Résolution (Responsable) +**Missions** +- Conduire la boucle de diagnostic complète : reproduction minimale, logs, bissection, hypothèses, tests ciblés, correctif, non-régression. +- Produire un REX quand plusieurs hypothèses ont été testées. + +**Artefacts** +- `tests/**`, `archive/**` + +--- + +### Agent SSH & scripts (Responsable) +**Missions** +- Garantir la présence et l’usage correct de `scripts/auto-ssh-push.sh`, `scripts/init-ssh-env.sh`, `scripts/setup-ssh-ci.sh`. +- Assurer permissions d’exécution, idempotence, journalisation non sensible, gestion d’erreurs robuste. +- Interdire secrets en clair, gérer via secrets CI et variables d’environnement. +- Exiger la mise à jour de `docs/SSH_UPDATE.md` à toute évolution des scripts ou des flux SSH. + +**Artefacts** +- `scripts/**`, `.gitea/workflows/ci.yml`, `docs/SSH_UPDATE.md`, `docs/CONFIGURATION.md`, `CHANGELOG.md`. + +--- + +## Agents frontend + +### Agent Frontend (Responsable) +**Missions** +- Mettre en place `React.lazy`/`Suspense` (code splitting). +- Centraliser l’état (Redux ou Context API). +- Abstraire les services de données. + +**Artefacts** +- `docs/ARCHITECTURE.md`, `docs/TESTING.md`. + +--- + +## Agents open source et CI + +### Agent Open Source (Responsable) +**Missions** +- Maintenir : `LICENSE`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `docs/OPEN_SOURCE_CHECKLIST.md`. +- Vérifier l’alignement continu avec `4NK_node`. + +**Artefacts** +- Fichiers de gouvernance cités ci-dessus. + +--- + +### Agent Gitea (Responsable) +**Missions** +- Garantir `.gitea/ISSUE_TEMPLATE/*`, `PULL_REQUEST_TEMPLATE.md`, `.gitea/workflows/ci.yml`. +- Documenter la configuration distante dans `docs/GITEA_SETUP.md`. +- Déclencher les étapes CI pertinentes (tests, lint, sécurité, vérifs `scripts/`). + +**Artefacts** +- `.gitea/**`, `docs/GITEA_SETUP.md`. + +--- + +### Agent Versionnage (Responsable) +**Missions** +- Tenir `CHANGELOG.md` comme source unique de vérité. +- Proposer un bump sémantique justifié. +- Demander confirmation avant push et tag. + - Orchestrer le `release-guard` (CI + scripts) et consigner latest vs wip. + +**Artefacts** +- `CHANGELOG.md`, `docs/RELEASE_PLAN.md`, `docs/ROADMAP.md`. + +--- + +## Agents de synchronisation et dérogations + +### Agent Synchronisation de template (Accountable) +**Références** +- `.4nk-sync.yml` (manifeste), `TEMPLATE_VERSION` (pointeur), `.gitea/workflows/template-sync.yml` (CI dédiée). +- Règles Cursor : `.cursor/rules/42-template-sync.mdc` et `.cursor/rules/10-project-structure.mdc`. + +**Missions** +- Assurer l’alignement automatique sur le template pour : `.cursor/**`, `.gitea/**`, `AGENTS.md`, `scripts/**`, `docs/SSH_UPDATE.md`. +- Déclencher/valider la PR `[template-sync]` créée par la CI. +- Exiger la mise à jour de `CHANGELOG.md` et `docs/INDEX.md` après synchronisation. +- Vérifier l’intégrité (`manifest_checksum`, checksums de fichiers si publiés), les permissions, l’absence de secrets. +- Mettre à jour `TEMPLATE_VERSION` dans la PR. + +**Artefacts** +- `.4nk-sync.yml`, `TEMPLATE_VERSION`, `.cursor/**`, `.gitea/**`, `AGENTS.md`, `scripts/**`, `docs/SSH_UPDATE.md`, `CHANGELOG.md`. + +--- + +### Agent Dérogations locales (Responsable) +**Références** +- `LOCAL_OVERRIDES.yml` (facultatif, mais recommandé). + +**Missions** +- Enregistrer toute divergence locale dans le périmètre synchronisé (path, raison, propriétaire, échéance). +- Faire respecter : seules les dérogations listées et non expirées sont tolérées par la CI. +- Auditer périodiquement et résorber les dérogations. + +**Artefacts** +- `LOCAL_OVERRIDES.yml`, `CHANGELOG.md` (mentionner les dérogations significatives). + +--- + +## Matrice de coordination + +| Type de changement | Agents impliqués | Artefacts principaux | Validation obligatoire | +|----------------------------------|----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------|------------------------| +| Ajout de fonctionnalité | Documentation, Tests, Dépendances, Frontend | `docs/API.md`, `docs/USAGE.md`, `docs/ARCHITECTURE.md`, `tests/unit`, `tests/integration`, `CHANGELOG.md` (*Added*) | Oui | +| Correction de bug | Résolution, Tests, Documentation | `tests/unit`, `docs/TESTING.md`, `archive/` (REX si nécessaire), `CHANGELOG.md` (*Fixed*) | Oui | +| Refactorisation / amélioration | Structure, Documentation, Compilation | `docs/ARCHITECTURE.md`, `archive/`, `CHANGELOG.md` (*Changed*) | Oui | +| Dépendance ajoutée/mise à jour | Dépendances, Compilation, Documentation | `docs/ARCHITECTURE.md`, `docs/CONFIGURATION.md`, `CHANGELOG.md` (*Dependencies*) | Oui | +| Données CSV modifiées | Données CSV, Documentation, Tests | `docs/API.md`, `docs/ARCHITECTURE.md`, `docs/USAGE.md`, `tests/unit`, `CHANGELOG.md` (*Data model update*) | Oui | +| Migration / breaking change | Documentation, Tests, Résolution, Versionnage | `docs/MIGRATION.md`, `docs/INSTALLATION.md`, `docs/RELEASE_PLAN.md`, `docs/ROADMAP.md`, `tests/integration`, `CHANGELOG.md` (*Breaking*) | Oui | +| Sécurité / audit | Documentation, Tests, Open Source, Sécurité proactive | `docs/SECURITY_AUDIT.md`, `tests/external`, `tests/connectivity`, `CHANGELOG.md` (*Security*) | Oui | +| Préparation open source / CI | Open Source, Gitea, Versionnage, Documentation communautaire, Contributeurs externes | `.gitea/**`, `docs/GITEA_SETUP.md`, `docs/OPEN_SOURCE_CHECKLIST.md`, `CHANGELOG.md` (*CI/CD* / *Governance*) | Oui | +| Optimisation performance | Performance, Tests, Documentation | `tests/performance`, `tests/reports`, `docs/ARCHITECTURE.md`, `CHANGELOG.md` (*Performance*) | Oui | +| Évolution frontend | Frontend, Documentation, Tests | `docs/ARCHITECTURE.md`, `docs/USAGE.md`, `tests/integration`, `CHANGELOG.md` (*Frontend*) | Oui | +| Évolution CI/CD ou scripts SSH | SSH & scripts, Gitea, Versionnage, Documentation | `scripts/**`, `.gitea/workflows/ci.yml`, `docs/SSH_UPDATE.md`, `docs/CONFIGURATION.md`, `CHANGELOG.md` (*CI/CD*) | Oui | +| **Synchronisation de template** | **Synchronisation de template**, Gitea, Versionnage, Structure, Documentation, SSH & scripts | `.4nk-sync.yml`, `TEMPLATE_VERSION`, `.cursor/**`, `.gitea/**`, `AGENTS.md`, `scripts/**`, `docs/SSH_UPDATE.md`, `CHANGELOG.md` | **Oui** | +| Dérogation locale contrôlée | Dérogations locales, Gitea, Synchronisation de template, Versionnage | `LOCAL_OVERRIDES.yml`, `CHANGELOG.md` (mention), CI tolérante uniquement sur chemins listés et non expirés | Oui | + +--- + +## Conclusion +Ce `AGENTS.md` mis à jour introduit l’**Agent Synchronisation de template** et l’**Agent Dérogations locales**, renforce l’**Agent SSH & scripts**, et rattache l’ensemble aux règles Cursor et à la CI Gitea. La matrice de coordination formalise les validations obligatoires pour chaque type de changement, garantissant cohérence structurelle, qualité documentaire, sécurité, traçabilité et stabilité à long terme sur tous les projets issus de `4NK_project_template`. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8b00082 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog - sdk_wallet + +Ce projet suit le format Keep a Changelog et le Semantic Versioning. + +## [0.1.0] - 2025-08-27 +### Added +- Squelette React Native (Redux, React.lazy/Suspense) +- Pont WebView ↔ ihm_client via postMessage +- Scripts PowerShell: build-ihm, copy-ihm, sync:ihm +- TypeScript, Jest (jsdom), config TS/Jest +- Écran `WalletScreen` et composant `WebWallet` + +### Notes +- Chargement local des assets d’`ihm_client` prévu via `assets/ihm/` diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d4d5aaa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,10 @@ +# Code de Conduite + +Nous nous engageons à fournir un environnement ouvert et accueillant. Soyez respectueux, constructif et professionnel. Tout comportement abusif, discriminatoire ou harcelant n’est pas toléré. + +- Agissez de bonne foi, assumez des intentions positives +- Pas de harcèlement, d’intimidation ou de propos discriminatoires +- Respectez la confidentialité et la sécurité +- Utilisez des canaux appropriés pour signaler des incidents + +Contact: contact@4nkweb.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..83df6d4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# Contribuer à sdk_wallet + +Merci de votre intérêt ! + +## Démarrage +- Node 18+ / 20+, pnpm ou npm +- `npm install` +- `npm run typecheck && npm test` + +## Branches et commits +- Feature branches: `feat/` +- Fixes: `fix/` +- Commits conventionnels (conventional commits) + +## Tests +- Ajoutez des tests dans `tests/` +- Isolez l’état et les ressources, pas d’effets globaux + +## Documentation +- Mettez à jour `docs/` et `CHANGELOG.md` pour chaque changement utilisateur + +## Licence +- MIT diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..19df345 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 4NK + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..0b4149e --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,27 @@ +# Architecture - sdk_wallet + +## Vue d’ensemble +- Application mobile React Native +- État centralisé avec Redux Toolkit +- UI Web intégrée via `react-native-webview` qui charge `ihm_client` (build Vite) +- Pont de messages `window.postMessage` redirigé vers `ReactNativeWebView.postMessage` + +## Flux +1. L’app charge `assets/ihm/index.html` (build de `ihm_client`) +2. Le script injecté remappe `window.postMessage` et expose `window.__RN_RECEIVE__` +3. `ihm_client` émet `LISTENING`, `LINK_ACCEPTED`, etc. → captés côté RN +4. RN met à jour Redux (tokens, état), puis peut envoyer des messages: `REQUEST_LINK`, `VALIDATE_TOKEN`, etc. + +## Découpage +- `src/bridge/` : sérialisation et gestion des messages +- `src/components/` : `WebWallet` (WebView) +- `src/screens/` : `WalletScreen` +- `src/store/` : état (tokens, dernier message) + +## Sécurité +- Respect de l’origine dans `ihm_client` (réponses vers `event.origin`) +- Les tokens ne sortent pas du store sans action explicite + +## Performances +- Code splitting avec `React.lazy`/`Suspense` +- Build `ihm_client` optimisé via Vite diff --git a/docs/INDEX.md b/docs/INDEX.md new file mode 100644 index 0000000..52755f4 --- /dev/null +++ b/docs/INDEX.md @@ -0,0 +1,6 @@ +# Documentation - sdk_wallet + +- Architecture: `ARCHITECTURE.md` +- Intégration iframe/WebView: `INTEGRATION.md` +- Tests: `TESTING.md` +- Notes de version: `../CHANGELOG.md` diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md new file mode 100644 index 0000000..6a84e18 --- /dev/null +++ b/docs/INTEGRATION.md @@ -0,0 +1,20 @@ +# Intégration WebView ↔ ihm_client + +## Principes +- `ihm_client` parle via `window.postMessage` (cf. `ihm_client/docs/INTEGRATION_IFRAME.md`) +- En mobile, on charge `ihm_client` dans une WebView +- On redirige `window.postMessage` vers `ReactNativeWebView.postMessage` +- Canal entrant: RN appelle `window.__RN_RECEIVE__(jsonString)` pour simuler un `MessageEvent` + +## Messages pris en charge (extraits) +- REQUEST_LINK → LINK_ACCEPTED|ERROR +- VALIDATE_TOKEN → VALIDATE_TOKEN +- RENEW_TOKEN → RENEW_TOKEN + +## Mapping côté RN +- Sortant: RN → `__RN_RECEIVE__(jsonString)` (déclenche un `message` côté page) +- Entrant: page → `postMessage(any)` redirigé vers RN `onMessage` + +## Sécurité +- `ihm_client` valide l’origine et les tokens +- RN ne manipule pas directement les tokens côté page diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..40284b1 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,12 @@ +# Tests - sdk_wallet + +## Portée +- Bridge: sérialisation, réception des messages, mise à jour Redux +- Store: reducers `setTokens`, `setLastMessageType` + +## Commandes +- `npm test` (Jest + ts-jest, jsdom) + +## Isolation +- Pas d’accès réseau +- Pas d’exemples exécutables; tests en mémoire diff --git a/ihm/account-component-DbdHSqFJ.mjs b/ihm/account-component-DbdHSqFJ.mjs new file mode 100644 index 0000000..667baf6 --- /dev/null +++ b/ihm/account-component-DbdHSqFJ.mjs @@ -0,0 +1,6758 @@ +import { S as Services, a as addressToEmoji } from './index-CZvdFchg.mjs'; + +/*! +* sweetalert2 v11.22.4 +* Released under the MIT License. +*/ +function _assertClassBrand(e, t, n) { + if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; + throw new TypeError("Private element is not present on this object"); +} +function _checkPrivateRedeclaration(e, t) { + if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); +} +function _classPrivateFieldGet2(s, a) { + return s.get(_assertClassBrand(s, a)); +} +function _classPrivateFieldInitSpec(e, t, a) { + _checkPrivateRedeclaration(e, t), t.set(e, a); +} +function _classPrivateFieldSet2(s, a, r) { + return s.set(_assertClassBrand(s, a), r), r; +} + +const RESTORE_FOCUS_TIMEOUT = 100; + +/** @type {GlobalState} */ +const globalState = {}; +const focusPreviousActiveElement = () => { + if (globalState.previousActiveElement instanceof HTMLElement) { + globalState.previousActiveElement.focus(); + globalState.previousActiveElement = null; + } else if (document.body) { + document.body.focus(); + } +}; + +/** + * Restore previous active (focused) element + * + * @param {boolean} returnFocus + * @returns {Promise} + */ +const restoreActiveElement = returnFocus => { + return new Promise(resolve => { + if (!returnFocus) { + return resolve(); + } + const x = window.scrollX; + const y = window.scrollY; + globalState.restoreFocusTimeout = setTimeout(() => { + focusPreviousActiveElement(); + resolve(); + }, RESTORE_FOCUS_TIMEOUT); // issues/900 + + window.scrollTo(x, y); + }); +}; + +const swalPrefix = 'swal2-'; + +/** + * @typedef {Record} SwalClasses + */ + +/** + * @typedef {'success' | 'warning' | 'info' | 'question' | 'error'} SwalIcon + * @typedef {Record} SwalIcons + */ + +/** @type {SwalClass[]} */ +const classNames = ['container', 'shown', 'height-auto', 'iosfix', 'popup', 'modal', 'no-backdrop', 'no-transition', 'toast', 'toast-shown', 'show', 'hide', 'close', 'title', 'html-container', 'actions', 'confirm', 'deny', 'cancel', 'footer', 'icon', 'icon-content', 'image', 'input', 'file', 'range', 'select', 'radio', 'checkbox', 'label', 'textarea', 'inputerror', 'input-label', 'validation-message', 'progress-steps', 'active-progress-step', 'progress-step', 'progress-step-line', 'loader', 'loading', 'styled', 'top', 'top-start', 'top-end', 'top-left', 'top-right', 'center', 'center-start', 'center-end', 'center-left', 'center-right', 'bottom', 'bottom-start', 'bottom-end', 'bottom-left', 'bottom-right', 'grow-row', 'grow-column', 'grow-fullscreen', 'rtl', 'timer-progress-bar', 'timer-progress-bar-container', 'scrollbar-measure', 'icon-success', 'icon-warning', 'icon-info', 'icon-question', 'icon-error', 'draggable', 'dragging']; +const swalClasses = classNames.reduce((acc, className) => { + acc[className] = swalPrefix + className; + return acc; +}, /** @type {SwalClasses} */{}); + +/** @type {SwalIcon[]} */ +const icons = ['success', 'warning', 'info', 'question', 'error']; +const iconTypes = icons.reduce((acc, icon) => { + acc[icon] = swalPrefix + icon; + return acc; +}, /** @type {SwalIcons} */{}); + +const consolePrefix = 'SweetAlert2:'; + +/** + * Capitalize the first letter of a string + * + * @param {string} str + * @returns {string} + */ +const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1); + +/** + * Standardize console warnings + * + * @param {string | string[]} message + */ +const warn = message => { + console.warn(`${consolePrefix} ${typeof message === 'object' ? message.join(' ') : message}`); +}; + +/** + * Standardize console errors + * + * @param {string} message + */ +const error = message => { + console.error(`${consolePrefix} ${message}`); +}; + +/** + * Private global state for `warnOnce` + * + * @type {string[]} + * @private + */ +const previousWarnOnceMessages = []; + +/** + * Show a console warning, but only if it hasn't already been shown + * + * @param {string} message + */ +const warnOnce = message => { + if (!previousWarnOnceMessages.includes(message)) { + previousWarnOnceMessages.push(message); + warn(message); + } +}; + +/** + * Show a one-time console warning about deprecated params/methods + * + * @param {string} deprecatedParam + * @param {string?} useInstead + */ +const warnAboutDeprecation = (deprecatedParam, useInstead = null) => { + warnOnce(`"${deprecatedParam}" is deprecated and will be removed in the next major release.${useInstead ? ` Use "${useInstead}" instead.` : ''}`); +}; + +/** + * If `arg` is a function, call it (with no arguments or context) and return the result. + * Otherwise, just pass the value through + * + * @param {Function | any} arg + * @returns {any} + */ +const callIfFunction = arg => typeof arg === 'function' ? arg() : arg; + +/** + * @param {any} arg + * @returns {boolean} + */ +const hasToPromiseFn = arg => arg && typeof arg.toPromise === 'function'; + +/** + * @param {any} arg + * @returns {Promise} + */ +const asPromise = arg => hasToPromiseFn(arg) ? arg.toPromise() : Promise.resolve(arg); + +/** + * @param {any} arg + * @returns {boolean} + */ +const isPromise = arg => arg && Promise.resolve(arg) === arg; + +/** + * Gets the popup container which contains the backdrop and the popup itself. + * + * @returns {HTMLElement | null} + */ +const getContainer = () => document.body.querySelector(`.${swalClasses.container}`); + +/** + * @param {string} selectorString + * @returns {HTMLElement | null} + */ +const elementBySelector = selectorString => { + const container = getContainer(); + return container ? container.querySelector(selectorString) : null; +}; + +/** + * @param {string} className + * @returns {HTMLElement | null} + */ +const elementByClass = className => { + return elementBySelector(`.${className}`); +}; + +/** + * @returns {HTMLElement | null} + */ +const getPopup = () => elementByClass(swalClasses.popup); + +/** + * @returns {HTMLElement | null} + */ +const getIcon = () => elementByClass(swalClasses.icon); + +/** + * @returns {HTMLElement | null} + */ +const getIconContent = () => elementByClass(swalClasses['icon-content']); + +/** + * @returns {HTMLElement | null} + */ +const getTitle = () => elementByClass(swalClasses.title); + +/** + * @returns {HTMLElement | null} + */ +const getHtmlContainer = () => elementByClass(swalClasses['html-container']); + +/** + * @returns {HTMLElement | null} + */ +const getImage = () => elementByClass(swalClasses.image); + +/** + * @returns {HTMLElement | null} + */ +const getProgressSteps = () => elementByClass(swalClasses['progress-steps']); + +/** + * @returns {HTMLElement | null} + */ +const getValidationMessage = () => elementByClass(swalClasses['validation-message']); + +/** + * @returns {HTMLButtonElement | null} + */ +const getConfirmButton = () => (/** @type {HTMLButtonElement} */elementBySelector(`.${swalClasses.actions} .${swalClasses.confirm}`)); + +/** + * @returns {HTMLButtonElement | null} + */ +const getCancelButton = () => (/** @type {HTMLButtonElement} */elementBySelector(`.${swalClasses.actions} .${swalClasses.cancel}`)); + +/** + * @returns {HTMLButtonElement | null} + */ +const getDenyButton = () => (/** @type {HTMLButtonElement} */elementBySelector(`.${swalClasses.actions} .${swalClasses.deny}`)); + +/** + * @returns {HTMLElement | null} + */ +const getInputLabel = () => elementByClass(swalClasses['input-label']); + +/** + * @returns {HTMLElement | null} + */ +const getLoader = () => elementBySelector(`.${swalClasses.loader}`); + +/** + * @returns {HTMLElement | null} + */ +const getActions = () => elementByClass(swalClasses.actions); + +/** + * @returns {HTMLElement | null} + */ +const getFooter = () => elementByClass(swalClasses.footer); + +/** + * @returns {HTMLElement | null} + */ +const getTimerProgressBar = () => elementByClass(swalClasses['timer-progress-bar']); + +/** + * @returns {HTMLElement | null} + */ +const getCloseButton = () => elementByClass(swalClasses.close); + +// https://github.com/jkup/focusable/blob/master/index.js +const focusable = ` + a[href], + area[href], + input:not([disabled]), + select:not([disabled]), + textarea:not([disabled]), + button:not([disabled]), + iframe, + object, + embed, + [tabindex="0"], + [contenteditable], + audio[controls], + video[controls], + summary +`; +/** + * @returns {HTMLElement[]} + */ +const getFocusableElements = () => { + const popup = getPopup(); + if (!popup) { + return []; + } + /** @type {NodeListOf} */ + const focusableElementsWithTabindex = popup.querySelectorAll('[tabindex]:not([tabindex="-1"]):not([tabindex="0"])'); + const focusableElementsWithTabindexSorted = Array.from(focusableElementsWithTabindex) + // sort according to tabindex + .sort((a, b) => { + const tabindexA = parseInt(a.getAttribute('tabindex') || '0'); + const tabindexB = parseInt(b.getAttribute('tabindex') || '0'); + if (tabindexA > tabindexB) { + return 1; + } else if (tabindexA < tabindexB) { + return -1; + } + return 0; + }); + + /** @type {NodeListOf} */ + const otherFocusableElements = popup.querySelectorAll(focusable); + const otherFocusableElementsFiltered = Array.from(otherFocusableElements).filter(el => el.getAttribute('tabindex') !== '-1'); + return [...new Set(focusableElementsWithTabindexSorted.concat(otherFocusableElementsFiltered))].filter(el => isVisible$1(el)); +}; + +/** + * @returns {boolean} + */ +const isModal = () => { + return hasClass(document.body, swalClasses.shown) && !hasClass(document.body, swalClasses['toast-shown']) && !hasClass(document.body, swalClasses['no-backdrop']); +}; + +/** + * @returns {boolean} + */ +const isToast = () => { + const popup = getPopup(); + if (!popup) { + return false; + } + return hasClass(popup, swalClasses.toast); +}; + +/** + * @returns {boolean} + */ +const isLoading = () => { + const popup = getPopup(); + if (!popup) { + return false; + } + return popup.hasAttribute('data-loading'); +}; + +/** + * Securely set innerHTML of an element + * https://github.com/sweetalert2/sweetalert2/issues/1926 + * + * @param {HTMLElement} elem + * @param {string} html + */ +const setInnerHtml = (elem, html) => { + elem.textContent = ''; + if (html) { + const parser = new DOMParser(); + const parsed = parser.parseFromString(html, `text/html`); + const head = parsed.querySelector('head'); + if (head) { + Array.from(head.childNodes).forEach(child => { + elem.appendChild(child); + }); + } + const body = parsed.querySelector('body'); + if (body) { + Array.from(body.childNodes).forEach(child => { + if (child instanceof HTMLVideoElement || child instanceof HTMLAudioElement) { + elem.appendChild(child.cloneNode(true)); // https://github.com/sweetalert2/sweetalert2/issues/2507 + } else { + elem.appendChild(child); + } + }); + } + } +}; + +/** + * @param {HTMLElement} elem + * @param {string} className + * @returns {boolean} + */ +const hasClass = (elem, className) => { + if (!className) { + return false; + } + const classList = className.split(/\s+/); + for (let i = 0; i < classList.length; i++) { + if (!elem.classList.contains(classList[i])) { + return false; + } + } + return true; +}; + +/** + * @param {HTMLElement} elem + * @param {SweetAlertOptions} params + */ +const removeCustomClasses = (elem, params) => { + Array.from(elem.classList).forEach(className => { + if (!Object.values(swalClasses).includes(className) && !Object.values(iconTypes).includes(className) && !Object.values(params.showClass || {}).includes(className)) { + elem.classList.remove(className); + } + }); +}; + +/** + * @param {HTMLElement} elem + * @param {SweetAlertOptions} params + * @param {string} className + */ +const applyCustomClass = (elem, params, className) => { + removeCustomClasses(elem, params); + if (!params.customClass) { + return; + } + const customClass = params.customClass[(/** @type {keyof SweetAlertCustomClass} */className)]; + if (!customClass) { + return; + } + if (typeof customClass !== 'string' && !customClass.forEach) { + warn(`Invalid type of customClass.${className}! Expected string or iterable object, got "${typeof customClass}"`); + return; + } + addClass(elem, customClass); +}; + +/** + * @param {HTMLElement} popup + * @param {import('./renderers/renderInput').InputClass | SweetAlertInput} inputClass + * @returns {HTMLInputElement | null} + */ +const getInput$1 = (popup, inputClass) => { + if (!inputClass) { + return null; + } + switch (inputClass) { + case 'select': + case 'textarea': + case 'file': + return popup.querySelector(`.${swalClasses.popup} > .${swalClasses[inputClass]}`); + case 'checkbox': + return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.checkbox} input`); + case 'radio': + return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.radio} input:checked`) || popup.querySelector(`.${swalClasses.popup} > .${swalClasses.radio} input:first-child`); + case 'range': + return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.range} input`); + default: + return popup.querySelector(`.${swalClasses.popup} > .${swalClasses.input}`); + } +}; + +/** + * @param {HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement} input + */ +const focusInput = input => { + input.focus(); + + // place cursor at end of text in text input + if (input.type !== 'file') { + // http://stackoverflow.com/a/2345915 + const val = input.value; + input.value = ''; + input.value = val; + } +}; + +/** + * @param {HTMLElement | HTMLElement[] | null} target + * @param {string | string[] | readonly string[] | undefined} classList + * @param {boolean} condition + */ +const toggleClass = (target, classList, condition) => { + if (!target || !classList) { + return; + } + if (typeof classList === 'string') { + classList = classList.split(/\s+/).filter(Boolean); + } + classList.forEach(className => { + if (Array.isArray(target)) { + target.forEach(elem => { + if (condition) { + elem.classList.add(className); + } else { + elem.classList.remove(className); + } + }); + } else { + if (condition) { + target.classList.add(className); + } else { + target.classList.remove(className); + } + } + }); +}; + +/** + * @param {HTMLElement | HTMLElement[] | null} target + * @param {string | string[] | readonly string[] | undefined} classList + */ +const addClass = (target, classList) => { + toggleClass(target, classList, true); +}; + +/** + * @param {HTMLElement | HTMLElement[] | null} target + * @param {string | string[] | readonly string[] | undefined} classList + */ +const removeClass = (target, classList) => { + toggleClass(target, classList, false); +}; + +/** + * Get direct child of an element by class name + * + * @param {HTMLElement} elem + * @param {string} className + * @returns {HTMLElement | undefined} + */ +const getDirectChildByClass = (elem, className) => { + const children = Array.from(elem.children); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child instanceof HTMLElement && hasClass(child, className)) { + return child; + } + } +}; + +/** + * @param {HTMLElement} elem + * @param {string} property + * @param {*} value + */ +const applyNumericalStyle = (elem, property, value) => { + if (value === `${parseInt(value)}`) { + value = parseInt(value); + } + if (value || parseInt(value) === 0) { + elem.style.setProperty(property, typeof value === 'number' ? `${value}px` : value); + } else { + elem.style.removeProperty(property); + } +}; + +/** + * @param {HTMLElement | null} elem + * @param {string} display + */ +const show = (elem, display = 'flex') => { + if (!elem) { + return; + } + elem.style.display = display; +}; + +/** + * @param {HTMLElement | null} elem + */ +const hide = elem => { + if (!elem) { + return; + } + elem.style.display = 'none'; +}; + +/** + * @param {HTMLElement | null} elem + * @param {string} display + */ +const showWhenInnerHtmlPresent = (elem, display = 'block') => { + if (!elem) { + return; + } + new MutationObserver(() => { + toggle(elem, elem.innerHTML, display); + }).observe(elem, { + childList: true, + subtree: true + }); +}; + +/** + * @param {HTMLElement} parent + * @param {string} selector + * @param {string} property + * @param {string} value + */ +const setStyle = (parent, selector, property, value) => { + /** @type {HTMLElement | null} */ + const el = parent.querySelector(selector); + if (el) { + el.style.setProperty(property, value); + } +}; + +/** + * @param {HTMLElement} elem + * @param {any} condition + * @param {string} display + */ +const toggle = (elem, condition, display = 'flex') => { + if (condition) { + show(elem, display); + } else { + hide(elem); + } +}; + +/** + * borrowed from jquery $(elem).is(':visible') implementation + * + * @param {HTMLElement | null} elem + * @returns {boolean} + */ +const isVisible$1 = elem => !!(elem && (elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length)); + +/** + * @returns {boolean} + */ +const allButtonsAreHidden = () => !isVisible$1(getConfirmButton()) && !isVisible$1(getDenyButton()) && !isVisible$1(getCancelButton()); + +/** + * @param {HTMLElement} elem + * @returns {boolean} + */ +const isScrollable = elem => !!(elem.scrollHeight > elem.clientHeight); + +/** + * @param {HTMLElement} element + * @param {HTMLElement} stopElement + * @returns {boolean} + */ +const selfOrParentIsScrollable = (element, stopElement) => { + let parent = element; + while (parent && parent !== stopElement) { + if (isScrollable(parent)) { + return true; + } + parent = parent.parentElement; + } + return false; +}; + +/** + * borrowed from https://stackoverflow.com/a/46352119 + * + * @param {HTMLElement} elem + * @returns {boolean} + */ +const hasCssAnimation = elem => { + const style = window.getComputedStyle(elem); + const animDuration = parseFloat(style.getPropertyValue('animation-duration') || '0'); + const transDuration = parseFloat(style.getPropertyValue('transition-duration') || '0'); + return animDuration > 0 || transDuration > 0; +}; + +/** + * @param {number} timer + * @param {boolean} reset + */ +const animateTimerProgressBar = (timer, reset = false) => { + const timerProgressBar = getTimerProgressBar(); + if (!timerProgressBar) { + return; + } + if (isVisible$1(timerProgressBar)) { + if (reset) { + timerProgressBar.style.transition = 'none'; + timerProgressBar.style.width = '100%'; + } + setTimeout(() => { + timerProgressBar.style.transition = `width ${timer / 1000}s linear`; + timerProgressBar.style.width = '0%'; + }, 10); + } +}; +const stopTimerProgressBar = () => { + const timerProgressBar = getTimerProgressBar(); + if (!timerProgressBar) { + return; + } + const timerProgressBarWidth = parseInt(window.getComputedStyle(timerProgressBar).width); + timerProgressBar.style.removeProperty('transition'); + timerProgressBar.style.width = '100%'; + const timerProgressBarFullWidth = parseInt(window.getComputedStyle(timerProgressBar).width); + const timerProgressBarPercent = timerProgressBarWidth / timerProgressBarFullWidth * 100; + timerProgressBar.style.width = `${timerProgressBarPercent}%`; +}; + +/** + * Detect Node env + * + * @returns {boolean} + */ +const isNodeEnv = () => typeof window === 'undefined' || typeof document === 'undefined'; + +const sweetHTML = ` +
+ +
    +
    + +

    +
    + + +
    + + +
    + +
    + + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    +`.replace(/(^|\n)\s*/g, ''); + +/** + * @returns {boolean} + */ +const resetOldContainer = () => { + const oldContainer = getContainer(); + if (!oldContainer) { + return false; + } + oldContainer.remove(); + removeClass([document.documentElement, document.body], [swalClasses['no-backdrop'], swalClasses['toast-shown'], swalClasses['has-column']]); + return true; +}; +const resetValidationMessage$1 = () => { + globalState.currentInstance.resetValidationMessage(); +}; +const addInputChangeListeners = () => { + const popup = getPopup(); + const input = getDirectChildByClass(popup, swalClasses.input); + const file = getDirectChildByClass(popup, swalClasses.file); + /** @type {HTMLInputElement} */ + const range = popup.querySelector(`.${swalClasses.range} input`); + /** @type {HTMLOutputElement} */ + const rangeOutput = popup.querySelector(`.${swalClasses.range} output`); + const select = getDirectChildByClass(popup, swalClasses.select); + /** @type {HTMLInputElement} */ + const checkbox = popup.querySelector(`.${swalClasses.checkbox} input`); + const textarea = getDirectChildByClass(popup, swalClasses.textarea); + input.oninput = resetValidationMessage$1; + file.onchange = resetValidationMessage$1; + select.onchange = resetValidationMessage$1; + checkbox.onchange = resetValidationMessage$1; + textarea.oninput = resetValidationMessage$1; + range.oninput = () => { + resetValidationMessage$1(); + rangeOutput.value = range.value; + }; + range.onchange = () => { + resetValidationMessage$1(); + rangeOutput.value = range.value; + }; +}; + +/** + * @param {string | HTMLElement} target + * @returns {HTMLElement} + */ +const getTarget = target => typeof target === 'string' ? document.querySelector(target) : target; + +/** + * @param {SweetAlertOptions} params + */ +const setupAccessibility = params => { + const popup = getPopup(); + popup.setAttribute('role', params.toast ? 'alert' : 'dialog'); + popup.setAttribute('aria-live', params.toast ? 'polite' : 'assertive'); + if (!params.toast) { + popup.setAttribute('aria-modal', 'true'); + } +}; + +/** + * @param {HTMLElement} targetElement + */ +const setupRTL = targetElement => { + if (window.getComputedStyle(targetElement).direction === 'rtl') { + addClass(getContainer(), swalClasses.rtl); + } +}; + +/** + * Add modal + backdrop + no-war message for Russians to DOM + * + * @param {SweetAlertOptions} params + */ +const init = params => { + // Clean up the old popup container if it exists + const oldContainerExisted = resetOldContainer(); + if (isNodeEnv()) { + error('SweetAlert2 requires document to initialize'); + return; + } + const container = document.createElement('div'); + container.className = swalClasses.container; + if (oldContainerExisted) { + addClass(container, swalClasses['no-transition']); + } + setInnerHtml(container, sweetHTML); + container.dataset['swal2Theme'] = params.theme; + const targetElement = getTarget(params.target); + targetElement.appendChild(container); + if (params.topLayer) { + container.setAttribute('popover', ''); + container.showPopover(); + } + setupAccessibility(params); + setupRTL(targetElement); + addInputChangeListeners(); +}; + +/** + * @param {HTMLElement | object | string} param + * @param {HTMLElement} target + */ +const parseHtmlToContainer = (param, target) => { + // DOM element + if (param instanceof HTMLElement) { + target.appendChild(param); + } + + // Object + else if (typeof param === 'object') { + handleObject(param, target); + } + + // Plain string + else if (param) { + setInnerHtml(target, param); + } +}; + +/** + * @param {any} param + * @param {HTMLElement} target + */ +const handleObject = (param, target) => { + // JQuery element(s) + if (param.jquery) { + handleJqueryElem(target, param); + } + + // For other objects use their string representation + else { + setInnerHtml(target, param.toString()); + } +}; + +/** + * @param {HTMLElement} target + * @param {any} elem + */ +const handleJqueryElem = (target, elem) => { + target.textContent = ''; + if (0 in elem) { + for (let i = 0; i in elem; i++) { + target.appendChild(elem[i].cloneNode(true)); + } + } else { + target.appendChild(elem.cloneNode(true)); + } +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderActions = (instance, params) => { + const actions = getActions(); + const loader = getLoader(); + if (!actions || !loader) { + return; + } + + // Actions (buttons) wrapper + if (!params.showConfirmButton && !params.showDenyButton && !params.showCancelButton) { + hide(actions); + } else { + show(actions); + } + + // Custom class + applyCustomClass(actions, params, 'actions'); + + // Render all the buttons + renderButtons(actions, loader, params); + + // Loader + setInnerHtml(loader, params.loaderHtml || ''); + applyCustomClass(loader, params, 'loader'); +}; + +/** + * @param {HTMLElement} actions + * @param {HTMLElement} loader + * @param {SweetAlertOptions} params + */ +function renderButtons(actions, loader, params) { + const confirmButton = getConfirmButton(); + const denyButton = getDenyButton(); + const cancelButton = getCancelButton(); + if (!confirmButton || !denyButton || !cancelButton) { + return; + } + + // Render buttons + renderButton(confirmButton, 'confirm', params); + renderButton(denyButton, 'deny', params); + renderButton(cancelButton, 'cancel', params); + handleButtonsStyling(confirmButton, denyButton, cancelButton, params); + if (params.reverseButtons) { + if (params.toast) { + actions.insertBefore(cancelButton, confirmButton); + actions.insertBefore(denyButton, confirmButton); + } else { + actions.insertBefore(cancelButton, loader); + actions.insertBefore(denyButton, loader); + actions.insertBefore(confirmButton, loader); + } + } +} + +/** + * @param {HTMLElement} confirmButton + * @param {HTMLElement} denyButton + * @param {HTMLElement} cancelButton + * @param {SweetAlertOptions} params + */ +function handleButtonsStyling(confirmButton, denyButton, cancelButton, params) { + if (!params.buttonsStyling) { + removeClass([confirmButton, denyButton, cancelButton], swalClasses.styled); + return; + } + addClass([confirmButton, denyButton, cancelButton], swalClasses.styled); + + // Apply custom background colors to action buttons + if (params.confirmButtonColor) { + confirmButton.style.setProperty('--swal2-confirm-button-background-color', params.confirmButtonColor); + } + if (params.denyButtonColor) { + denyButton.style.setProperty('--swal2-deny-button-background-color', params.denyButtonColor); + } + if (params.cancelButtonColor) { + cancelButton.style.setProperty('--swal2-cancel-button-background-color', params.cancelButtonColor); + } + + // Apply the outline color to action buttons + applyOutlineColor(confirmButton); + applyOutlineColor(denyButton); + applyOutlineColor(cancelButton); +} + +/** + * @param {HTMLElement} button + */ +function applyOutlineColor(button) { + const buttonStyle = window.getComputedStyle(button); + if (buttonStyle.getPropertyValue('--swal2-action-button-focus-box-shadow')) { + // If the button already has a custom outline color, no need to change it + return; + } + const outlineColor = buttonStyle.backgroundColor.replace(/rgba?\((\d+), (\d+), (\d+).*/, 'rgba($1, $2, $3, 0.5)'); + button.style.setProperty('--swal2-action-button-focus-box-shadow', buttonStyle.getPropertyValue('--swal2-outline').replace(/ rgba\(.*/, ` ${outlineColor}`)); +} + +/** + * @param {HTMLElement} button + * @param {'confirm' | 'deny' | 'cancel'} buttonType + * @param {SweetAlertOptions} params + */ +function renderButton(button, buttonType, params) { + const buttonName = /** @type {'Confirm' | 'Deny' | 'Cancel'} */capitalizeFirstLetter(buttonType); + toggle(button, params[`show${buttonName}Button`], 'inline-block'); + setInnerHtml(button, params[`${buttonType}ButtonText`] || ''); // Set caption text + button.setAttribute('aria-label', params[`${buttonType}ButtonAriaLabel`] || ''); // ARIA label + + // Add buttons custom classes + button.className = swalClasses[buttonType]; + applyCustomClass(button, params, `${buttonType}Button`); +} + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderCloseButton = (instance, params) => { + const closeButton = getCloseButton(); + if (!closeButton) { + return; + } + setInnerHtml(closeButton, params.closeButtonHtml || ''); + + // Custom class + applyCustomClass(closeButton, params, 'closeButton'); + toggle(closeButton, params.showCloseButton); + closeButton.setAttribute('aria-label', params.closeButtonAriaLabel || ''); +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderContainer = (instance, params) => { + const container = getContainer(); + if (!container) { + return; + } + handleBackdropParam(container, params.backdrop); + handlePositionParam(container, params.position); + handleGrowParam(container, params.grow); + + // Custom class + applyCustomClass(container, params, 'container'); +}; + +/** + * @param {HTMLElement} container + * @param {SweetAlertOptions['backdrop']} backdrop + */ +function handleBackdropParam(container, backdrop) { + if (typeof backdrop === 'string') { + container.style.background = backdrop; + } else if (!backdrop) { + addClass([document.documentElement, document.body], swalClasses['no-backdrop']); + } +} + +/** + * @param {HTMLElement} container + * @param {SweetAlertOptions['position']} position + */ +function handlePositionParam(container, position) { + if (!position) { + return; + } + if (position in swalClasses) { + addClass(container, swalClasses[position]); + } else { + warn('The "position" parameter is not valid, defaulting to "center"'); + addClass(container, swalClasses.center); + } +} + +/** + * @param {HTMLElement} container + * @param {SweetAlertOptions['grow']} grow + */ +function handleGrowParam(container, grow) { + if (!grow) { + return; + } + addClass(container, swalClasses[`grow-${grow}`]); +} + +/** + * This module contains `WeakMap`s for each effectively-"private property" that a `Swal` has. + * For example, to set the private property "foo" of `this` to "bar", you can `privateProps.foo.set(this, 'bar')` + * This is the approach that Babel will probably take to implement private methods/fields + * https://github.com/tc39/proposal-private-methods + * https://github.com/babel/babel/pull/7555 + * Once we have the changes from that PR in Babel, and our core class fits reasonable in *one module* + * then we can use that language feature. + */ + +var privateProps = { + innerParams: new WeakMap(), + domCache: new WeakMap() +}; + +/// + + +/** @type {InputClass[]} */ +const inputClasses = ['input', 'file', 'range', 'select', 'radio', 'checkbox', 'textarea']; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderInput = (instance, params) => { + const popup = getPopup(); + if (!popup) { + return; + } + const innerParams = privateProps.innerParams.get(instance); + const rerender = !innerParams || params.input !== innerParams.input; + inputClasses.forEach(inputClass => { + const inputContainer = getDirectChildByClass(popup, swalClasses[inputClass]); + if (!inputContainer) { + return; + } + + // set attributes + setAttributes(inputClass, params.inputAttributes); + + // set class + inputContainer.className = swalClasses[inputClass]; + if (rerender) { + hide(inputContainer); + } + }); + if (params.input) { + if (rerender) { + showInput(params); + } + // set custom class + setCustomClass(params); + } +}; + +/** + * @param {SweetAlertOptions} params + */ +const showInput = params => { + if (!params.input) { + return; + } + if (!renderInputType[params.input]) { + error(`Unexpected type of input! Expected ${Object.keys(renderInputType).join(' | ')}, got "${params.input}"`); + return; + } + const inputContainer = getInputContainer(params.input); + if (!inputContainer) { + return; + } + const input = renderInputType[params.input](inputContainer, params); + show(inputContainer); + + // input autofocus + if (params.inputAutoFocus) { + setTimeout(() => { + focusInput(input); + }); + } +}; + +/** + * @param {HTMLInputElement} input + */ +const removeAttributes = input => { + for (let i = 0; i < input.attributes.length; i++) { + const attrName = input.attributes[i].name; + if (!['id', 'type', 'value', 'style'].includes(attrName)) { + input.removeAttribute(attrName); + } + } +}; + +/** + * @param {InputClass} inputClass + * @param {SweetAlertOptions['inputAttributes']} inputAttributes + */ +const setAttributes = (inputClass, inputAttributes) => { + const popup = getPopup(); + if (!popup) { + return; + } + const input = getInput$1(popup, inputClass); + if (!input) { + return; + } + removeAttributes(input); + for (const attr in inputAttributes) { + input.setAttribute(attr, inputAttributes[attr]); + } +}; + +/** + * @param {SweetAlertOptions} params + */ +const setCustomClass = params => { + if (!params.input) { + return; + } + const inputContainer = getInputContainer(params.input); + if (inputContainer) { + applyCustomClass(inputContainer, params, 'input'); + } +}; + +/** + * @param {HTMLInputElement | HTMLTextAreaElement} input + * @param {SweetAlertOptions} params + */ +const setInputPlaceholder = (input, params) => { + if (!input.placeholder && params.inputPlaceholder) { + input.placeholder = params.inputPlaceholder; + } +}; + +/** + * @param {Input} input + * @param {Input} prependTo + * @param {SweetAlertOptions} params + */ +const setInputLabel = (input, prependTo, params) => { + if (params.inputLabel) { + const label = document.createElement('label'); + const labelClass = swalClasses['input-label']; + label.setAttribute('for', input.id); + label.className = labelClass; + if (typeof params.customClass === 'object') { + addClass(label, params.customClass.inputLabel); + } + label.innerText = params.inputLabel; + prependTo.insertAdjacentElement('beforebegin', label); + } +}; + +/** + * @param {SweetAlertInput} inputType + * @returns {HTMLElement | undefined} + */ +const getInputContainer = inputType => { + const popup = getPopup(); + if (!popup) { + return; + } + return getDirectChildByClass(popup, swalClasses[(/** @type {SwalClass} */inputType)] || swalClasses.input); +}; + +/** + * @param {HTMLInputElement | HTMLOutputElement | HTMLTextAreaElement} input + * @param {SweetAlertOptions['inputValue']} inputValue + */ +const checkAndSetInputValue = (input, inputValue) => { + if (['string', 'number'].includes(typeof inputValue)) { + input.value = `${inputValue}`; + } else if (!isPromise(inputValue)) { + warn(`Unexpected type of inputValue! Expected "string", "number" or "Promise", got "${typeof inputValue}"`); + } +}; + +/** @type {Record Input>} */ +const renderInputType = {}; + +/** + * @param {HTMLInputElement} input + * @param {SweetAlertOptions} params + * @returns {HTMLInputElement} + */ +renderInputType.text = renderInputType.email = renderInputType.password = renderInputType.number = renderInputType.tel = renderInputType.url = renderInputType.search = renderInputType.date = renderInputType['datetime-local'] = renderInputType.time = renderInputType.week = renderInputType.month = /** @type {(input: Input | HTMLElement, params: SweetAlertOptions) => Input} */ +(input, params) => { + checkAndSetInputValue(input, params.inputValue); + setInputLabel(input, input, params); + setInputPlaceholder(input, params); + input.type = params.input; + return input; +}; + +/** + * @param {HTMLInputElement} input + * @param {SweetAlertOptions} params + * @returns {HTMLInputElement} + */ +renderInputType.file = (input, params) => { + setInputLabel(input, input, params); + setInputPlaceholder(input, params); + return input; +}; + +/** + * @param {HTMLInputElement} range + * @param {SweetAlertOptions} params + * @returns {HTMLInputElement} + */ +renderInputType.range = (range, params) => { + const rangeInput = range.querySelector('input'); + const rangeOutput = range.querySelector('output'); + checkAndSetInputValue(rangeInput, params.inputValue); + rangeInput.type = params.input; + checkAndSetInputValue(rangeOutput, params.inputValue); + setInputLabel(rangeInput, range, params); + return range; +}; + +/** + * @param {HTMLSelectElement} select + * @param {SweetAlertOptions} params + * @returns {HTMLSelectElement} + */ +renderInputType.select = (select, params) => { + select.textContent = ''; + if (params.inputPlaceholder) { + const placeholder = document.createElement('option'); + setInnerHtml(placeholder, params.inputPlaceholder); + placeholder.value = ''; + placeholder.disabled = true; + placeholder.selected = true; + select.appendChild(placeholder); + } + setInputLabel(select, select, params); + return select; +}; + +/** + * @param {HTMLInputElement} radio + * @returns {HTMLInputElement} + */ +renderInputType.radio = radio => { + radio.textContent = ''; + return radio; +}; + +/** + * @param {HTMLLabelElement} checkboxContainer + * @param {SweetAlertOptions} params + * @returns {HTMLInputElement} + */ +renderInputType.checkbox = (checkboxContainer, params) => { + const checkbox = getInput$1(getPopup(), 'checkbox'); + checkbox.value = '1'; + checkbox.checked = Boolean(params.inputValue); + const label = checkboxContainer.querySelector('span'); + setInnerHtml(label, params.inputPlaceholder || params.inputLabel); + return checkbox; +}; + +/** + * @param {HTMLTextAreaElement} textarea + * @param {SweetAlertOptions} params + * @returns {HTMLTextAreaElement} + */ +renderInputType.textarea = (textarea, params) => { + checkAndSetInputValue(textarea, params.inputValue); + setInputPlaceholder(textarea, params); + setInputLabel(textarea, textarea, params); + + /** + * @param {HTMLElement} el + * @returns {number} + */ + const getMargin = el => parseInt(window.getComputedStyle(el).marginLeft) + parseInt(window.getComputedStyle(el).marginRight); + + // https://github.com/sweetalert2/sweetalert2/issues/2291 + setTimeout(() => { + // https://github.com/sweetalert2/sweetalert2/issues/1699 + if ('MutationObserver' in window) { + const initialPopupWidth = parseInt(window.getComputedStyle(getPopup()).width); + const textareaResizeHandler = () => { + // check if texarea is still in document (i.e. popup wasn't closed in the meantime) + if (!document.body.contains(textarea)) { + return; + } + const textareaWidth = textarea.offsetWidth + getMargin(textarea); + if (textareaWidth > initialPopupWidth) { + getPopup().style.width = `${textareaWidth}px`; + } else { + applyNumericalStyle(getPopup(), 'width', params.width); + } + }; + new MutationObserver(textareaResizeHandler).observe(textarea, { + attributes: true, + attributeFilter: ['style'] + }); + } + }); + return textarea; +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderContent = (instance, params) => { + const htmlContainer = getHtmlContainer(); + if (!htmlContainer) { + return; + } + showWhenInnerHtmlPresent(htmlContainer); + applyCustomClass(htmlContainer, params, 'htmlContainer'); + + // Content as HTML + if (params.html) { + parseHtmlToContainer(params.html, htmlContainer); + show(htmlContainer, 'block'); + } + + // Content as plain text + else if (params.text) { + htmlContainer.textContent = params.text; + show(htmlContainer, 'block'); + } + + // No content + else { + hide(htmlContainer); + } + renderInput(instance, params); +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderFooter = (instance, params) => { + const footer = getFooter(); + if (!footer) { + return; + } + showWhenInnerHtmlPresent(footer); + toggle(footer, params.footer, 'block'); + if (params.footer) { + parseHtmlToContainer(params.footer, footer); + } + + // Custom class + applyCustomClass(footer, params, 'footer'); +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderIcon = (instance, params) => { + const innerParams = privateProps.innerParams.get(instance); + const icon = getIcon(); + if (!icon) { + return; + } + + // if the given icon already rendered, apply the styling without re-rendering the icon + if (innerParams && params.icon === innerParams.icon) { + // Custom or default content + setContent(icon, params); + applyStyles(icon, params); + return; + } + if (!params.icon && !params.iconHtml) { + hide(icon); + return; + } + if (params.icon && Object.keys(iconTypes).indexOf(params.icon) === -1) { + error(`Unknown icon! Expected "success", "error", "warning", "info" or "question", got "${params.icon}"`); + hide(icon); + return; + } + show(icon); + + // Custom or default content + setContent(icon, params); + applyStyles(icon, params); + + // Animate icon + addClass(icon, params.showClass && params.showClass.icon); + + // Re-adjust the success icon on system theme change + const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: dark)'); + colorSchemeQueryList.addEventListener('change', adjustSuccessIconBackgroundColor); +}; + +/** + * @param {HTMLElement} icon + * @param {SweetAlertOptions} params + */ +const applyStyles = (icon, params) => { + for (const [iconType, iconClassName] of Object.entries(iconTypes)) { + if (params.icon !== iconType) { + removeClass(icon, iconClassName); + } + } + addClass(icon, params.icon && iconTypes[params.icon]); + + // Icon color + setColor(icon, params); + + // Success icon background color + adjustSuccessIconBackgroundColor(); + + // Custom class + applyCustomClass(icon, params, 'icon'); +}; + +// Adjust success icon background color to match the popup background color +const adjustSuccessIconBackgroundColor = () => { + const popup = getPopup(); + if (!popup) { + return; + } + const popupBackgroundColor = window.getComputedStyle(popup).getPropertyValue('background-color'); + /** @type {NodeListOf} */ + const successIconParts = popup.querySelectorAll('[class^=swal2-success-circular-line], .swal2-success-fix'); + for (let i = 0; i < successIconParts.length; i++) { + successIconParts[i].style.backgroundColor = popupBackgroundColor; + } +}; + +/** + * + * @param {SweetAlertOptions} params + * @returns {string} + */ +const successIconHtml = params => ` + ${params.animation ? '
    ' : ''} + +
    + ${params.animation ? '
    ' : ''} + ${params.animation ? '
    ' : ''} +`; +const errorIconHtml = ` + + + + +`; + +/** + * @param {HTMLElement} icon + * @param {SweetAlertOptions} params + */ +const setContent = (icon, params) => { + if (!params.icon && !params.iconHtml) { + return; + } + let oldContent = icon.innerHTML; + let newContent = ''; + if (params.iconHtml) { + newContent = iconContent(params.iconHtml); + } else if (params.icon === 'success') { + newContent = successIconHtml(params); + oldContent = oldContent.replace(/ style=".*?"/g, ''); // undo adjustSuccessIconBackgroundColor() + } else if (params.icon === 'error') { + newContent = errorIconHtml; + } else if (params.icon) { + const defaultIconHtml = { + question: '?', + warning: '!', + info: 'i' + }; + newContent = iconContent(defaultIconHtml[params.icon]); + } + if (oldContent.trim() !== newContent.trim()) { + setInnerHtml(icon, newContent); + } +}; + +/** + * @param {HTMLElement} icon + * @param {SweetAlertOptions} params + */ +const setColor = (icon, params) => { + if (!params.iconColor) { + return; + } + icon.style.color = params.iconColor; + icon.style.borderColor = params.iconColor; + for (const sel of ['.swal2-success-line-tip', '.swal2-success-line-long', '.swal2-x-mark-line-left', '.swal2-x-mark-line-right']) { + setStyle(icon, sel, 'background-color', params.iconColor); + } + setStyle(icon, '.swal2-success-ring', 'border-color', params.iconColor); +}; + +/** + * @param {string} content + * @returns {string} + */ +const iconContent = content => `
    ${content}
    `; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderImage = (instance, params) => { + const image = getImage(); + if (!image) { + return; + } + if (!params.imageUrl) { + hide(image); + return; + } + show(image, ''); + + // Src, alt + image.setAttribute('src', params.imageUrl); + image.setAttribute('alt', params.imageAlt || ''); + + // Width, height + applyNumericalStyle(image, 'width', params.imageWidth); + applyNumericalStyle(image, 'height', params.imageHeight); + + // Class + image.className = swalClasses.image; + applyCustomClass(image, params, 'image'); +}; + +let dragging = false; +let mousedownX = 0; +let mousedownY = 0; +let initialX = 0; +let initialY = 0; + +/** + * @param {HTMLElement} popup + */ +const addDraggableListeners = popup => { + popup.addEventListener('mousedown', down); + document.body.addEventListener('mousemove', move); + popup.addEventListener('mouseup', up); + popup.addEventListener('touchstart', down); + document.body.addEventListener('touchmove', move); + popup.addEventListener('touchend', up); +}; + +/** + * @param {HTMLElement} popup + */ +const removeDraggableListeners = popup => { + popup.removeEventListener('mousedown', down); + document.body.removeEventListener('mousemove', move); + popup.removeEventListener('mouseup', up); + popup.removeEventListener('touchstart', down); + document.body.removeEventListener('touchmove', move); + popup.removeEventListener('touchend', up); +}; + +/** + * @param {MouseEvent | TouchEvent} event + */ +const down = event => { + const popup = getPopup(); + if (event.target === popup || getIcon().contains(/** @type {HTMLElement} */event.target)) { + dragging = true; + const clientXY = getClientXY(event); + mousedownX = clientXY.clientX; + mousedownY = clientXY.clientY; + initialX = parseInt(popup.style.insetInlineStart) || 0; + initialY = parseInt(popup.style.insetBlockStart) || 0; + addClass(popup, 'swal2-dragging'); + } +}; + +/** + * @param {MouseEvent | TouchEvent} event + */ +const move = event => { + const popup = getPopup(); + if (dragging) { + let { + clientX, + clientY + } = getClientXY(event); + popup.style.insetInlineStart = `${initialX + (clientX - mousedownX)}px`; + popup.style.insetBlockStart = `${initialY + (clientY - mousedownY)}px`; + } +}; +const up = () => { + const popup = getPopup(); + dragging = false; + removeClass(popup, 'swal2-dragging'); +}; + +/** + * @param {MouseEvent | TouchEvent} event + * @returns {{ clientX: number, clientY: number }} + */ +const getClientXY = event => { + let clientX = 0, + clientY = 0; + if (event.type.startsWith('mouse')) { + clientX = /** @type {MouseEvent} */event.clientX; + clientY = /** @type {MouseEvent} */event.clientY; + } else if (event.type.startsWith('touch')) { + clientX = /** @type {TouchEvent} */event.touches[0].clientX; + clientY = /** @type {TouchEvent} */event.touches[0].clientY; + } + return { + clientX, + clientY + }; +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderPopup = (instance, params) => { + const container = getContainer(); + const popup = getPopup(); + if (!container || !popup) { + return; + } + + // Width + // https://github.com/sweetalert2/sweetalert2/issues/2170 + if (params.toast) { + applyNumericalStyle(container, 'width', params.width); + popup.style.width = '100%'; + const loader = getLoader(); + if (loader) { + popup.insertBefore(loader, getIcon()); + } + } else { + applyNumericalStyle(popup, 'width', params.width); + } + + // Padding + applyNumericalStyle(popup, 'padding', params.padding); + + // Color + if (params.color) { + popup.style.color = params.color; + } + + // Background + if (params.background) { + popup.style.background = params.background; + } + hide(getValidationMessage()); + + // Classes + addClasses$1(popup, params); + if (params.draggable && !params.toast) { + addClass(popup, swalClasses.draggable); + addDraggableListeners(popup); + } else { + removeClass(popup, swalClasses.draggable); + removeDraggableListeners(popup); + } +}; + +/** + * @param {HTMLElement} popup + * @param {SweetAlertOptions} params + */ +const addClasses$1 = (popup, params) => { + const showClass = params.showClass || {}; + // Default Class + showClass when updating Swal.update({}) + popup.className = `${swalClasses.popup} ${isVisible$1(popup) ? showClass.popup : ''}`; + if (params.toast) { + addClass([document.documentElement, document.body], swalClasses['toast-shown']); + addClass(popup, swalClasses.toast); + } else { + addClass(popup, swalClasses.modal); + } + + // Custom class + applyCustomClass(popup, params, 'popup'); + // TODO: remove in the next major + if (typeof params.customClass === 'string') { + addClass(popup, params.customClass); + } + + // Icon class (#1842) + if (params.icon) { + addClass(popup, swalClasses[`icon-${params.icon}`]); + } +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderProgressSteps = (instance, params) => { + const progressStepsContainer = getProgressSteps(); + if (!progressStepsContainer) { + return; + } + const { + progressSteps, + currentProgressStep + } = params; + if (!progressSteps || progressSteps.length === 0 || currentProgressStep === undefined) { + hide(progressStepsContainer); + return; + } + show(progressStepsContainer); + progressStepsContainer.textContent = ''; + if (currentProgressStep >= progressSteps.length) { + warn('Invalid currentProgressStep parameter, it should be less than progressSteps.length ' + '(currentProgressStep like JS arrays starts from 0)'); + } + progressSteps.forEach((step, index) => { + const stepEl = createStepElement(step); + progressStepsContainer.appendChild(stepEl); + if (index === currentProgressStep) { + addClass(stepEl, swalClasses['active-progress-step']); + } + if (index !== progressSteps.length - 1) { + const lineEl = createLineElement(params); + progressStepsContainer.appendChild(lineEl); + } + }); +}; + +/** + * @param {string} step + * @returns {HTMLLIElement} + */ +const createStepElement = step => { + const stepEl = document.createElement('li'); + addClass(stepEl, swalClasses['progress-step']); + setInnerHtml(stepEl, step); + return stepEl; +}; + +/** + * @param {SweetAlertOptions} params + * @returns {HTMLLIElement} + */ +const createLineElement = params => { + const lineEl = document.createElement('li'); + addClass(lineEl, swalClasses['progress-step-line']); + if (params.progressStepsDistance) { + applyNumericalStyle(lineEl, 'width', params.progressStepsDistance); + } + return lineEl; +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const renderTitle = (instance, params) => { + const title = getTitle(); + if (!title) { + return; + } + showWhenInnerHtmlPresent(title); + toggle(title, params.title || params.titleText, 'block'); + if (params.title) { + parseHtmlToContainer(params.title, title); + } + if (params.titleText) { + title.innerText = params.titleText; + } + + // Custom class + applyCustomClass(title, params, 'title'); +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const render = (instance, params) => { + renderPopup(instance, params); + renderContainer(instance, params); + renderProgressSteps(instance, params); + renderIcon(instance, params); + renderImage(instance, params); + renderTitle(instance, params); + renderCloseButton(instance, params); + renderContent(instance, params); + renderActions(instance, params); + renderFooter(instance, params); + const popup = getPopup(); + if (typeof params.didRender === 'function' && popup) { + params.didRender(popup); + } + globalState.eventEmitter.emit('didRender', popup); +}; + +/* + * Global function to determine if SweetAlert2 popup is shown + */ +const isVisible = () => { + return isVisible$1(getPopup()); +}; + +/* + * Global function to click 'Confirm' button + */ +const clickConfirm = () => { + var _dom$getConfirmButton; + return (_dom$getConfirmButton = getConfirmButton()) === null || _dom$getConfirmButton === void 0 ? void 0 : _dom$getConfirmButton.click(); +}; + +/* + * Global function to click 'Deny' button + */ +const clickDeny = () => { + var _dom$getDenyButton; + return (_dom$getDenyButton = getDenyButton()) === null || _dom$getDenyButton === void 0 ? void 0 : _dom$getDenyButton.click(); +}; + +/* + * Global function to click 'Cancel' button + */ +const clickCancel = () => { + var _dom$getCancelButton; + return (_dom$getCancelButton = getCancelButton()) === null || _dom$getCancelButton === void 0 ? void 0 : _dom$getCancelButton.click(); +}; + +/** @typedef {'cancel' | 'backdrop' | 'close' | 'esc' | 'timer'} DismissReason */ + +/** @type {Record} */ +const DismissReason = Object.freeze({ + cancel: 'cancel', + backdrop: 'backdrop', + close: 'close', + esc: 'esc', + timer: 'timer' +}); + +/** + * @param {GlobalState} globalState + */ +const removeKeydownHandler = globalState => { + if (globalState.keydownTarget && globalState.keydownHandlerAdded) { + globalState.keydownTarget.removeEventListener('keydown', globalState.keydownHandler, { + capture: globalState.keydownListenerCapture + }); + globalState.keydownHandlerAdded = false; + } +}; + +/** + * @param {GlobalState} globalState + * @param {SweetAlertOptions} innerParams + * @param {*} dismissWith + */ +const addKeydownHandler = (globalState, innerParams, dismissWith) => { + removeKeydownHandler(globalState); + if (!innerParams.toast) { + globalState.keydownHandler = e => keydownHandler(innerParams, e, dismissWith); + globalState.keydownTarget = innerParams.keydownListenerCapture ? window : getPopup(); + globalState.keydownListenerCapture = innerParams.keydownListenerCapture; + globalState.keydownTarget.addEventListener('keydown', globalState.keydownHandler, { + capture: globalState.keydownListenerCapture + }); + globalState.keydownHandlerAdded = true; + } +}; + +/** + * @param {number} index + * @param {number} increment + */ +const setFocus = (index, increment) => { + var _dom$getPopup; + const focusableElements = getFocusableElements(); + // search for visible elements and select the next possible match + if (focusableElements.length) { + index = index + increment; + + // shift + tab when .swal2-popup is focused + if (index === -2) { + index = focusableElements.length - 1; + } + + // rollover to first item + if (index === focusableElements.length) { + index = 0; + + // go to last item + } else if (index === -1) { + index = focusableElements.length - 1; + } + focusableElements[index].focus(); + return; + } + // no visible focusable elements, focus the popup + (_dom$getPopup = getPopup()) === null || _dom$getPopup === void 0 || _dom$getPopup.focus(); +}; +const arrowKeysNextButton = ['ArrowRight', 'ArrowDown']; +const arrowKeysPreviousButton = ['ArrowLeft', 'ArrowUp']; + +/** + * @param {SweetAlertOptions} innerParams + * @param {KeyboardEvent} event + * @param {Function} dismissWith + */ +const keydownHandler = (innerParams, event, dismissWith) => { + if (!innerParams) { + return; // This instance has already been destroyed + } + + // Ignore keydown during IME composition + // https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event#ignoring_keydown_during_ime_composition + // https://github.com/sweetalert2/sweetalert2/issues/720 + // https://github.com/sweetalert2/sweetalert2/issues/2406 + if (event.isComposing || event.keyCode === 229) { + return; + } + if (innerParams.stopKeydownPropagation) { + event.stopPropagation(); + } + + // ENTER + if (event.key === 'Enter') { + handleEnter(event, innerParams); + } + + // TAB + else if (event.key === 'Tab') { + handleTab(event); + } + + // ARROWS - switch focus between buttons + else if ([...arrowKeysNextButton, ...arrowKeysPreviousButton].includes(event.key)) { + handleArrows(event.key); + } + + // ESC + else if (event.key === 'Escape') { + handleEsc(event, innerParams, dismissWith); + } +}; + +/** + * @param {KeyboardEvent} event + * @param {SweetAlertOptions} innerParams + */ +const handleEnter = (event, innerParams) => { + // https://github.com/sweetalert2/sweetalert2/issues/2386 + if (!callIfFunction(innerParams.allowEnterKey)) { + return; + } + const input = getInput$1(getPopup(), innerParams.input); + if (event.target && input && event.target instanceof HTMLElement && event.target.outerHTML === input.outerHTML) { + if (['textarea', 'file'].includes(innerParams.input)) { + return; // do not submit + } + clickConfirm(); + event.preventDefault(); + } +}; + +/** + * @param {KeyboardEvent} event + */ +const handleTab = event => { + const targetElement = event.target; + const focusableElements = getFocusableElements(); + let btnIndex = -1; + for (let i = 0; i < focusableElements.length; i++) { + if (targetElement === focusableElements[i]) { + btnIndex = i; + break; + } + } + + // Cycle to the next button + if (!event.shiftKey) { + setFocus(btnIndex, 1); + } + + // Cycle to the prev button + else { + setFocus(btnIndex, -1); + } + event.stopPropagation(); + event.preventDefault(); +}; + +/** + * @param {string} key + */ +const handleArrows = key => { + const actions = getActions(); + const confirmButton = getConfirmButton(); + const denyButton = getDenyButton(); + const cancelButton = getCancelButton(); + if (!actions || !confirmButton || !denyButton || !cancelButton) { + return; + } + /** @type HTMLElement[] */ + const buttons = [confirmButton, denyButton, cancelButton]; + if (document.activeElement instanceof HTMLElement && !buttons.includes(document.activeElement)) { + return; + } + const sibling = arrowKeysNextButton.includes(key) ? 'nextElementSibling' : 'previousElementSibling'; + let buttonToFocus = document.activeElement; + if (!buttonToFocus) { + return; + } + for (let i = 0; i < actions.children.length; i++) { + buttonToFocus = buttonToFocus[sibling]; + if (!buttonToFocus) { + return; + } + if (buttonToFocus instanceof HTMLButtonElement && isVisible$1(buttonToFocus)) { + break; + } + } + if (buttonToFocus instanceof HTMLButtonElement) { + buttonToFocus.focus(); + } +}; + +/** + * @param {KeyboardEvent} event + * @param {SweetAlertOptions} innerParams + * @param {Function} dismissWith + */ +const handleEsc = (event, innerParams, dismissWith) => { + event.preventDefault(); + if (callIfFunction(innerParams.allowEscapeKey)) { + dismissWith(DismissReason.esc); + } +}; + +/** + * This module contains `WeakMap`s for each effectively-"private property" that a `Swal` has. + * For example, to set the private property "foo" of `this` to "bar", you can `privateProps.foo.set(this, 'bar')` + * This is the approach that Babel will probably take to implement private methods/fields + * https://github.com/tc39/proposal-private-methods + * https://github.com/babel/babel/pull/7555 + * Once we have the changes from that PR in Babel, and our core class fits reasonable in *one module* + * then we can use that language feature. + */ + +var privateMethods = { + swalPromiseResolve: new WeakMap(), + swalPromiseReject: new WeakMap() +}; + +// From https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/ +// Adding aria-hidden="true" to elements outside of the active modal dialog ensures that +// elements not within the active modal dialog will not be surfaced if a user opens a screen +// reader’s list of elements (headings, form controls, landmarks, etc.) in the document. + +const setAriaHidden = () => { + const container = getContainer(); + const bodyChildren = Array.from(document.body.children); + bodyChildren.forEach(el => { + if (el.contains(container)) { + return; + } + if (el.hasAttribute('aria-hidden')) { + el.setAttribute('data-previous-aria-hidden', el.getAttribute('aria-hidden') || ''); + } + el.setAttribute('aria-hidden', 'true'); + }); +}; +const unsetAriaHidden = () => { + const bodyChildren = Array.from(document.body.children); + bodyChildren.forEach(el => { + if (el.hasAttribute('data-previous-aria-hidden')) { + el.setAttribute('aria-hidden', el.getAttribute('data-previous-aria-hidden') || ''); + el.removeAttribute('data-previous-aria-hidden'); + } else { + el.removeAttribute('aria-hidden'); + } + }); +}; + +// @ts-ignore +const isSafariOrIOS = typeof window !== 'undefined' && !!window.GestureEvent; // true for Safari desktop + all iOS browsers https://stackoverflow.com/a/70585394 + +/** + * Fix iOS scrolling + * http://stackoverflow.com/q/39626302 + */ +const iOSfix = () => { + if (isSafariOrIOS && !hasClass(document.body, swalClasses.iosfix)) { + const offset = document.body.scrollTop; + document.body.style.top = `${offset * -1}px`; + addClass(document.body, swalClasses.iosfix); + lockBodyScroll(); + } +}; + +/** + * https://github.com/sweetalert2/sweetalert2/issues/1246 + */ +const lockBodyScroll = () => { + const container = getContainer(); + if (!container) { + return; + } + /** @type {boolean} */ + let preventTouchMove; + /** + * @param {TouchEvent} event + */ + container.ontouchstart = event => { + preventTouchMove = shouldPreventTouchMove(event); + }; + /** + * @param {TouchEvent} event + */ + container.ontouchmove = event => { + if (preventTouchMove) { + event.preventDefault(); + event.stopPropagation(); + } + }; +}; + +/** + * @param {TouchEvent} event + * @returns {boolean} + */ +const shouldPreventTouchMove = event => { + const target = event.target; + const container = getContainer(); + const htmlContainer = getHtmlContainer(); + if (!container || !htmlContainer) { + return false; + } + if (isStylus(event) || isZoom(event)) { + return false; + } + if (target === container) { + return true; + } + if (!isScrollable(container) && target instanceof HTMLElement && !selfOrParentIsScrollable(target, htmlContainer) && + // #2823 + target.tagName !== 'INPUT' && + // #1603 + target.tagName !== 'TEXTAREA' && + // #2266 + !(isScrollable(htmlContainer) && + // #1944 + htmlContainer.contains(target))) { + return true; + } + return false; +}; + +/** + * https://github.com/sweetalert2/sweetalert2/issues/1786 + * + * @param {*} event + * @returns {boolean} + */ +const isStylus = event => { + return event.touches && event.touches.length && event.touches[0].touchType === 'stylus'; +}; + +/** + * https://github.com/sweetalert2/sweetalert2/issues/1891 + * + * @param {TouchEvent} event + * @returns {boolean} + */ +const isZoom = event => { + return event.touches && event.touches.length > 1; +}; +const undoIOSfix = () => { + if (hasClass(document.body, swalClasses.iosfix)) { + const offset = parseInt(document.body.style.top, 10); + removeClass(document.body, swalClasses.iosfix); + document.body.style.top = ''; + document.body.scrollTop = offset * -1; + } +}; + +/** + * Measure scrollbar width for padding body during modal show/hide + * https://github.com/twbs/bootstrap/blob/master/js/src/modal.js + * + * @returns {number} + */ +const measureScrollbar = () => { + const scrollDiv = document.createElement('div'); + scrollDiv.className = swalClasses['scrollbar-measure']; + document.body.appendChild(scrollDiv); + const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; +}; + +/** + * Remember state in cases where opening and handling a modal will fiddle with it. + * @type {number | null} + */ +let previousBodyPadding = null; + +/** + * @param {string} initialBodyOverflow + */ +const replaceScrollbarWithPadding = initialBodyOverflow => { + // for queues, do not do this more than once + if (previousBodyPadding !== null) { + return; + } + // if the body has overflow + if (document.body.scrollHeight > window.innerHeight || initialBodyOverflow === 'scroll' // https://github.com/sweetalert2/sweetalert2/issues/2663 + ) { + // add padding so the content doesn't shift after removal of scrollbar + previousBodyPadding = parseInt(window.getComputedStyle(document.body).getPropertyValue('padding-right')); + document.body.style.paddingRight = `${previousBodyPadding + measureScrollbar()}px`; + } +}; +const undoReplaceScrollbarWithPadding = () => { + if (previousBodyPadding !== null) { + document.body.style.paddingRight = `${previousBodyPadding}px`; + previousBodyPadding = null; + } +}; + +/** + * @param {SweetAlert} instance + * @param {HTMLElement} container + * @param {boolean} returnFocus + * @param {Function} didClose + */ +function removePopupAndResetState(instance, container, returnFocus, didClose) { + if (isToast()) { + triggerDidCloseAndDispose(instance, didClose); + } else { + restoreActiveElement(returnFocus).then(() => triggerDidCloseAndDispose(instance, didClose)); + removeKeydownHandler(globalState); + } + + // workaround for https://github.com/sweetalert2/sweetalert2/issues/2088 + // for some reason removing the container in Safari will scroll the document to bottom + if (isSafariOrIOS) { + container.setAttribute('style', 'display:none !important'); + container.removeAttribute('class'); + container.innerHTML = ''; + } else { + container.remove(); + } + if (isModal()) { + undoReplaceScrollbarWithPadding(); + undoIOSfix(); + unsetAriaHidden(); + } + removeBodyClasses(); +} + +/** + * Remove SweetAlert2 classes from body + */ +function removeBodyClasses() { + removeClass([document.documentElement, document.body], [swalClasses.shown, swalClasses['height-auto'], swalClasses['no-backdrop'], swalClasses['toast-shown']]); +} + +/** + * Instance method to close sweetAlert + * + * @param {any} resolveValue + */ +function close(resolveValue) { + resolveValue = prepareResolveValue(resolveValue); + const swalPromiseResolve = privateMethods.swalPromiseResolve.get(this); + const didClose = triggerClosePopup(this); + if (this.isAwaitingPromise) { + // A swal awaiting for a promise (after a click on Confirm or Deny) cannot be dismissed anymore #2335 + if (!resolveValue.isDismissed) { + handleAwaitingPromise(this); + swalPromiseResolve(resolveValue); + } + } else if (didClose) { + // Resolve Swal promise + swalPromiseResolve(resolveValue); + } +} +const triggerClosePopup = instance => { + const popup = getPopup(); + if (!popup) { + return false; + } + const innerParams = privateProps.innerParams.get(instance); + if (!innerParams || hasClass(popup, innerParams.hideClass.popup)) { + return false; + } + removeClass(popup, innerParams.showClass.popup); + addClass(popup, innerParams.hideClass.popup); + const backdrop = getContainer(); + removeClass(backdrop, innerParams.showClass.backdrop); + addClass(backdrop, innerParams.hideClass.backdrop); + handlePopupAnimation(instance, popup, innerParams); + return true; +}; + +/** + * @param {any} error + */ +function rejectPromise(error) { + const rejectPromise = privateMethods.swalPromiseReject.get(this); + handleAwaitingPromise(this); + if (rejectPromise) { + // Reject Swal promise + rejectPromise(error); + } +} + +/** + * @param {SweetAlert} instance + */ +const handleAwaitingPromise = instance => { + if (instance.isAwaitingPromise) { + delete instance.isAwaitingPromise; + // The instance might have been previously partly destroyed, we must resume the destroy process in this case #2335 + if (!privateProps.innerParams.get(instance)) { + instance._destroy(); + } + } +}; + +/** + * @param {any} resolveValue + * @returns {SweetAlertResult} + */ +const prepareResolveValue = resolveValue => { + // When user calls Swal.close() + if (typeof resolveValue === 'undefined') { + return { + isConfirmed: false, + isDenied: false, + isDismissed: true + }; + } + return Object.assign({ + isConfirmed: false, + isDenied: false, + isDismissed: false + }, resolveValue); +}; + +/** + * @param {SweetAlert} instance + * @param {HTMLElement} popup + * @param {SweetAlertOptions} innerParams + */ +const handlePopupAnimation = (instance, popup, innerParams) => { + var _globalState$eventEmi; + const container = getContainer(); + // If animation is supported, animate + const animationIsSupported = hasCssAnimation(popup); + if (typeof innerParams.willClose === 'function') { + innerParams.willClose(popup); + } + (_globalState$eventEmi = globalState.eventEmitter) === null || _globalState$eventEmi === void 0 || _globalState$eventEmi.emit('willClose', popup); + if (animationIsSupported) { + animatePopup(instance, popup, container, innerParams.returnFocus, innerParams.didClose); + } else { + // Otherwise, remove immediately + removePopupAndResetState(instance, container, innerParams.returnFocus, innerParams.didClose); + } +}; + +/** + * @param {SweetAlert} instance + * @param {HTMLElement} popup + * @param {HTMLElement} container + * @param {boolean} returnFocus + * @param {Function} didClose + */ +const animatePopup = (instance, popup, container, returnFocus, didClose) => { + globalState.swalCloseEventFinishedCallback = removePopupAndResetState.bind(null, instance, container, returnFocus, didClose); + /** + * @param {AnimationEvent | TransitionEvent} e + */ + const swalCloseAnimationFinished = function (e) { + if (e.target === popup) { + var _globalState$swalClos; + (_globalState$swalClos = globalState.swalCloseEventFinishedCallback) === null || _globalState$swalClos === void 0 || _globalState$swalClos.call(globalState); + delete globalState.swalCloseEventFinishedCallback; + popup.removeEventListener('animationend', swalCloseAnimationFinished); + popup.removeEventListener('transitionend', swalCloseAnimationFinished); + } + }; + popup.addEventListener('animationend', swalCloseAnimationFinished); + popup.addEventListener('transitionend', swalCloseAnimationFinished); +}; + +/** + * @param {SweetAlert} instance + * @param {Function} didClose + */ +const triggerDidCloseAndDispose = (instance, didClose) => { + setTimeout(() => { + var _globalState$eventEmi2; + if (typeof didClose === 'function') { + didClose.bind(instance.params)(); + } + (_globalState$eventEmi2 = globalState.eventEmitter) === null || _globalState$eventEmi2 === void 0 || _globalState$eventEmi2.emit('didClose'); + // instance might have been destroyed already + if (instance._destroy) { + instance._destroy(); + } + }); +}; + +/** + * Shows loader (spinner), this is useful with AJAX requests. + * By default the loader be shown instead of the "Confirm" button. + * + * @param {HTMLButtonElement | null} [buttonToReplace] + */ +const showLoading = buttonToReplace => { + let popup = getPopup(); + if (!popup) { + new Swal(); + } + popup = getPopup(); + if (!popup) { + return; + } + const loader = getLoader(); + if (isToast()) { + hide(getIcon()); + } else { + replaceButton(popup, buttonToReplace); + } + show(loader); + popup.setAttribute('data-loading', 'true'); + popup.setAttribute('aria-busy', 'true'); + popup.focus(); +}; + +/** + * @param {HTMLElement} popup + * @param {HTMLButtonElement | null} [buttonToReplace] + */ +const replaceButton = (popup, buttonToReplace) => { + const actions = getActions(); + const loader = getLoader(); + if (!actions || !loader) { + return; + } + if (!buttonToReplace && isVisible$1(getConfirmButton())) { + buttonToReplace = getConfirmButton(); + } + show(actions); + if (buttonToReplace) { + hide(buttonToReplace); + loader.setAttribute('data-button-to-replace', buttonToReplace.className); + actions.insertBefore(loader, buttonToReplace); + } + addClass([popup, actions], swalClasses.loading); +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const handleInputOptionsAndValue = (instance, params) => { + if (params.input === 'select' || params.input === 'radio') { + handleInputOptions(instance, params); + } else if (['text', 'email', 'number', 'tel', 'textarea'].some(i => i === params.input) && (hasToPromiseFn(params.inputValue) || isPromise(params.inputValue))) { + showLoading(getConfirmButton()); + handleInputValue(instance, params); + } +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} innerParams + * @returns {SweetAlertInputValue} + */ +const getInputValue = (instance, innerParams) => { + const input = instance.getInput(); + if (!input) { + return null; + } + switch (innerParams.input) { + case 'checkbox': + return getCheckboxValue(input); + case 'radio': + return getRadioValue(input); + case 'file': + return getFileValue(input); + default: + return innerParams.inputAutoTrim ? input.value.trim() : input.value; + } +}; + +/** + * @param {HTMLInputElement} input + * @returns {number} + */ +const getCheckboxValue = input => input.checked ? 1 : 0; + +/** + * @param {HTMLInputElement} input + * @returns {string | null} + */ +const getRadioValue = input => input.checked ? input.value : null; + +/** + * @param {HTMLInputElement} input + * @returns {FileList | File | null} + */ +const getFileValue = input => input.files && input.files.length ? input.getAttribute('multiple') !== null ? input.files : input.files[0] : null; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const handleInputOptions = (instance, params) => { + const popup = getPopup(); + if (!popup) { + return; + } + /** + * @param {Record} inputOptions + */ + const processInputOptions = inputOptions => { + if (params.input === 'select') { + populateSelectOptions(popup, formatInputOptions(inputOptions), params); + } else if (params.input === 'radio') { + populateRadioOptions(popup, formatInputOptions(inputOptions), params); + } + }; + if (hasToPromiseFn(params.inputOptions) || isPromise(params.inputOptions)) { + showLoading(getConfirmButton()); + asPromise(params.inputOptions).then(inputOptions => { + instance.hideLoading(); + processInputOptions(inputOptions); + }); + } else if (typeof params.inputOptions === 'object') { + processInputOptions(params.inputOptions); + } else { + error(`Unexpected type of inputOptions! Expected object, Map or Promise, got ${typeof params.inputOptions}`); + } +}; + +/** + * @param {SweetAlert} instance + * @param {SweetAlertOptions} params + */ +const handleInputValue = (instance, params) => { + const input = instance.getInput(); + if (!input) { + return; + } + hide(input); + asPromise(params.inputValue).then(inputValue => { + input.value = params.input === 'number' ? `${parseFloat(inputValue) || 0}` : `${inputValue}`; + show(input); + input.focus(); + instance.hideLoading(); + }).catch(err => { + error(`Error in inputValue promise: ${err}`); + input.value = ''; + show(input); + input.focus(); + instance.hideLoading(); + }); +}; + +/** + * @param {HTMLElement} popup + * @param {InputOptionFlattened[]} inputOptions + * @param {SweetAlertOptions} params + */ +function populateSelectOptions(popup, inputOptions, params) { + const select = getDirectChildByClass(popup, swalClasses.select); + if (!select) { + return; + } + /** + * @param {HTMLElement} parent + * @param {string} optionLabel + * @param {string} optionValue + */ + const renderOption = (parent, optionLabel, optionValue) => { + const option = document.createElement('option'); + option.value = optionValue; + setInnerHtml(option, optionLabel); + option.selected = isSelected(optionValue, params.inputValue); + parent.appendChild(option); + }; + inputOptions.forEach(inputOption => { + const optionValue = inputOption[0]; + const optionLabel = inputOption[1]; + // spec: + // https://www.w3.org/TR/html401/interact/forms.html#h-17.6 + // "...all OPTGROUP elements must be specified directly within a SELECT element (i.e., groups may not be nested)..." + // check whether this is a + if (Array.isArray(optionLabel)) { + // if it is an array, then it is an + const optgroup = document.createElement('optgroup'); + optgroup.label = optionValue; + optgroup.disabled = false; // not configurable for now + select.appendChild(optgroup); + optionLabel.forEach(o => renderOption(optgroup, o[1], o[0])); + } else { + // case of