diff --git a/ansible/portail.yml b/ansible/portail.yml new file mode 100644 index 0000000..0d227b4 --- /dev/null +++ b/ansible/portail.yml @@ -0,0 +1,8 @@ +--- +# Étape 8 du déploiement Syllabus : portail Laravel. +- name: Portail Laravel + hosts: syllabus + become: true + gather_facts: true + roles: + - portail diff --git a/ansible/roles/portail/defaults/main.yml b/ansible/roles/portail/defaults/main.yml new file mode 100644 index 0000000..74de857 --- /dev/null +++ b/ansible/roles/portail/defaults/main.yml @@ -0,0 +1,8 @@ +--- +portail_mysql_port: 3307 +portail_redis_port: 6380 +portail_mysql_database: portail +portail_mysql_user: portail +portail_dossier_docker: /etc/portail +portail_dossier_app: /var/www/portail +portail_php_version: "8.3" diff --git a/ansible/roles/portail/handlers/main.yml b/ansible/roles/portail/handlers/main.yml new file mode 100644 index 0000000..9b181f6 --- /dev/null +++ b/ansible/roles/portail/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: recharger nginx + ansible.builtin.shell: nginx -t && systemctl reload nginx + changed_when: true diff --git a/ansible/roles/portail/tasks/01-systeme.yml b/ansible/roles/portail/tasks/01-systeme.yml new file mode 100644 index 0000000..cafe982 --- /dev/null +++ b/ansible/roles/portail/tasks/01-systeme.yml @@ -0,0 +1,91 @@ +--- +- name: installer les prérequis pour le dépôt PHP Sury + ansible.builtin.apt: + name: [lsb-release, ca-certificates, curl, gnupg, rsync] + state: present + update_cache: true + cache_valid_time: 3600 + +- name: télécharger le trousseau de clés du dépôt Sury + ansible.builtin.get_url: + url: https://packages.sury.org/debsuryorg-archive-keyring.deb + dest: /tmp/debsuryorg-archive-keyring.deb + mode: "0644" + +- name: installer le trousseau de clés Sury + ansible.builtin.apt: + deb: /tmp/debsuryorg-archive-keyring.deb + state: present + +- name: ajouter le dépôt PHP Sury + ansible.builtin.apt_repository: + repo: "deb [signed-by=/usr/share/keyrings/debsuryorg-archive-keyring.gpg] https://packages.sury.org/php/ {{ ansible_distribution_release }} main" + filename: php + state: present + update_cache: true + +- name: installer PHP {{ portail_php_version }}, Nginx et les extensions nécessaires + ansible.builtin.apt: + name: + - "php{{ portail_php_version }}-fpm" + - "php{{ portail_php_version }}-cli" + - "php{{ portail_php_version }}-mysql" + - "php{{ portail_php_version }}-redis" + - "php{{ portail_php_version }}-mbstring" + - "php{{ portail_php_version }}-xml" + - "php{{ portail_php_version }}-curl" + - "php{{ portail_php_version }}-zip" + - "php{{ portail_php_version }}-bcmath" + - "php{{ portail_php_version }}-gd" + - "php{{ portail_php_version }}-intl" + - "php{{ portail_php_version }}-opcache" + - nginx + state: present + update_cache: true + +- name: installer Composer (avec vérification de la somme d'installation officielle) + block: + - name: télécharger le programme d'installation de Composer + ansible.builtin.get_url: + url: https://getcomposer.org/installer + dest: /tmp/composer-setup.php + mode: "0644" + + - name: récupérer la somme SHA384 officielle + ansible.builtin.uri: + url: https://composer.github.io/installer.sig + return_content: true + register: portail_composer_signature + when: not (ansible_check_mode | default(false)) + + - name: calculer la somme SHA384 du fichier téléchargé + ansible.builtin.stat: + path: /tmp/composer-setup.php + checksum_algorithm: sha384 + register: portail_composer_setup_stat + + - name: vérifier que la somme correspond (sinon, installateur corrompu) + ansible.builtin.fail: + msg: "La somme de l'installateur Composer ne correspond pas à la signature officielle : installation interrompue." + when: + - portail_composer_signature is defined + - portail_composer_setup_stat.stat.checksum != portail_composer_signature.content.strip() + + - name: installer Composer (dernière version stable) + ansible.builtin.command: + cmd: php /tmp/composer-setup.php --install-dir=/usr/local/bin --filename=composer + args: + creates: /usr/local/bin/composer + +- name: installer Node.js 22 LTS (dépôt NodeSource) + block: + - name: ajouter le dépôt NodeSource + ansible.builtin.shell: + cmd: curl -fsSL https://deb.nodesource.com/setup_22.x | bash - + creates: /etc/apt/sources.list.d/nodesource.list + + - name: installer nodejs + ansible.builtin.apt: + name: nodejs + state: present + update_cache: true diff --git a/ansible/roles/portail/tasks/02-bases-donnees.yml b/ansible/roles/portail/tasks/02-bases-donnees.yml new file mode 100644 index 0000000..5c97bbb --- /dev/null +++ b/ansible/roles/portail/tasks/02-bases-donnees.yml @@ -0,0 +1,63 @@ +--- +- name: créer le dossier de configuration Docker du portail + ansible.builtin.file: + path: "{{ portail_dossier_docker }}" + state: directory + owner: root + group: root + mode: "0750" + +# Un seul fichier de secrets, nommé .env : docker compose le charge +# automatiquement pour les substitutions ${...} de docker-compose.yml, et une +# tâche plus loin le relit pour remplir le .env de Laravel avec les mêmes +# valeurs (une seule source de vérité, jamais dans Git). +- name: générer les secrets MariaDB/Redis de l'app (première fois seulement) + ansible.builtin.copy: + dest: "{{ portail_dossier_docker }}/.env" + content: | + MYSQL_ROOT_PASSWORD={{ lookup('community.general.random_string', length=32, special=false) }} + MYSQL_PASSWORD={{ lookup('community.general.random_string', length=32, special=false) }} + REDIS_PASSWORD={{ lookup('community.general.random_string', length=32, special=false) }} + owner: root + group: root + mode: "0600" + force: false + register: portail_secrets_generes + +- name: lire les secrets générés + ansible.builtin.slurp: + src: "{{ portail_dossier_docker }}/.env" + register: portail_secrets_brut + +- name: "exposer les secrets comme variables (jamais affichées, no_log)" + ansible.builtin.set_fact: + portail_secrets: "{{ dict(portail_secrets_lignes | map('split', '=', 1)) }}" + vars: + portail_secrets_lignes: "{{ (portail_secrets_brut.content | b64decode).splitlines() | select('string') | list }}" + no_log: true + +- name: déployer docker-compose.yml (MariaDB + Redis de l'app) + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ portail_dossier_docker }}/docker-compose.yml" + owner: root + group: root + mode: "0640" + vars: + portail_mysql_root_password: "${MYSQL_ROOT_PASSWORD}" + portail_mysql_password: "${MYSQL_PASSWORD}" + portail_redis_password: "${REDIS_PASSWORD}" + register: portail_compose_deploye + +- name: démarrer MariaDB et Redis (app) + community.docker.docker_compose_v2: + project_src: "{{ portail_dossier_docker }}" + state: present + register: portail_compose_resultat + +- name: attendre que MariaDB (app) accepte les connexions + ansible.builtin.wait_for: + host: 127.0.0.1 + port: "{{ portail_mysql_port }}" + delay: 2 + timeout: 60 diff --git a/ansible/roles/portail/tasks/03-code.yml b/ansible/roles/portail/tasks/03-code.yml new file mode 100644 index 0000000..edd7c80 --- /dev/null +++ b/ansible/roles/portail/tasks/03-code.yml @@ -0,0 +1,113 @@ +--- +- name: créer le dossier de l'application + ansible.builtin.file: + path: "{{ portail_dossier_app }}" + state: directory + owner: "{{ ansible_user }}" + group: "{{ ansible_user }}" + mode: "0755" + +# storage/ et bootstrap/cache/ sont de l'état et du cache runtime, jamais du +# code : on ne les synchronise jamais depuis le Mac (un rsync --delete les +# effacerait sinon, y compris storage/app/ où atterriront plus tard les +# travaux d'élèves). On crée leur structure une fois, puis on laisse +# www-data (PHP-FPM) les posséder en continu. +- name: créer la structure storage/ et bootstrap/cache/ (une seule fois) + ansible.builtin.file: + path: "{{ portail_dossier_app }}/{{ item }}" + state: directory + owner: www-data + group: www-data + mode: "0775" + loop: + - storage/app/public + - storage/framework/cache/data + - storage/framework/sessions + - storage/framework/views + - storage/logs + - bootstrap/cache + +- name: synchroniser le code du portail (sans vendor/node_modules/.env/storage) + become: false + ansible.posix.synchronize: + src: "{{ playbook_dir }}/../portail/" + dest: "{{ portail_dossier_app }}/" + delete: true + rsync_opts: + - "--exclude=/vendor" + - "--exclude=/node_modules" + - "--exclude=/.env" + - "--exclude=/database/database.sqlite" + - "--exclude=/storage" + - "--exclude=/bootstrap/cache" + - "--exclude=/public/build" + - "--exclude=/public/hot" + - "--exclude=/.git" + register: portail_code_synchronise + +- name: installer les dépendances PHP (composer) + ansible.builtin.command: + cmd: composer install --no-dev --optimize-autoloader --no-interaction + args: + chdir: "{{ portail_dossier_app }}" + environment: + COMPOSER_ALLOW_SUPERUSER: "1" + register: portail_composer_install_app + changed_when: "'Nothing to install' not in portail_composer_install_app.stdout" + +- name: vérifier si .env existe déjà + ansible.builtin.stat: + path: "{{ portail_dossier_app }}/.env" + register: portail_env_stat + +- name: générer une clé d'application (une seule fois) + ansible.builtin.command: + cmd: php artisan key:generate --show + args: + chdir: "{{ portail_dossier_app }}" + register: portail_app_key_generee + when: not portail_env_stat.stat.exists + changed_when: true + +- name: déployer .env de production (une seule fois, jamais réécrit ensuite) + ansible.builtin.template: + src: env.production.j2 + dest: "{{ portail_dossier_app }}/.env" + owner: "{{ ansible_user }}" + group: www-data + mode: "0640" + vars: + portail_app_key: "{{ portail_app_key_generee.stdout }}" + portail_app_url: "http://{{ ansible_host }}" + when: not portail_env_stat.stat.exists + +- name: installer les dépendances JS (npm) + ansible.builtin.command: + cmd: npm ci + args: + chdir: "{{ portail_dossier_app }}" + register: portail_npm_ci + changed_when: true + +- name: compiler les assets (vite build) + ansible.builtin.command: + cmd: npm run build + args: + chdir: "{{ portail_dossier_app }}" + register: portail_npm_build + changed_when: true + +- name: appliquer les migrations + ansible.builtin.command: + cmd: php artisan migrate --force + args: + chdir: "{{ portail_dossier_app }}" + register: portail_migrate + changed_when: "'Nothing to migrate' not in portail_migrate.stdout" + +- name: mettre en cache la configuration et les routes + ansible.builtin.shell: + cmd: php artisan config:cache && php artisan route:cache && php artisan view:cache + args: + chdir: "{{ portail_dossier_app }}" + changed_when: true diff --git a/ansible/roles/portail/tasks/04-nginx.yml b/ansible/roles/portail/tasks/04-nginx.yml new file mode 100644 index 0000000..531a8c2 --- /dev/null +++ b/ansible/roles/portail/tasks/04-nginx.yml @@ -0,0 +1,29 @@ +--- +- name: déployer le vhost Nginx du portail + ansible.builtin.template: + src: nginx-portail.conf.j2 + dest: /etc/nginx/sites-available/portail.conf + mode: "0644" + notify: recharger nginx + +- name: activer le vhost + ansible.builtin.file: + src: /etc/nginx/sites-available/portail.conf + dest: /etc/nginx/sites-enabled/portail.conf + state: link + notify: recharger nginx + +- name: désactiver le vhost par défaut de Nginx + ansible.builtin.file: + path: /etc/nginx/sites-enabled/default + state: absent + notify: recharger nginx + +- name: activer et démarrer php-fpm et nginx + ansible.builtin.systemd: + name: "{{ item }}" + enabled: true + state: started + loop: + - "php{{ portail_php_version }}-fpm" + - nginx diff --git a/ansible/roles/portail/tasks/main.yml b/ansible/roles/portail/tasks/main.yml new file mode 100644 index 0000000..c5a8168 --- /dev/null +++ b/ansible/roles/portail/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: système (PHP, Nginx, Composer, Node) + ansible.builtin.import_tasks: 01-systeme.yml + +- name: bases de données de l'app (MariaDB, Redis) + ansible.builtin.import_tasks: 02-bases-donnees.yml + +- name: code de l'application + ansible.builtin.import_tasks: 03-code.yml + +- name: Nginx + ansible.builtin.import_tasks: 04-nginx.yml diff --git a/ansible/roles/portail/templates/docker-compose.yml.j2 b/ansible/roles/portail/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..3c7309d --- /dev/null +++ b/ansible/roles/portail/templates/docker-compose.yml.j2 @@ -0,0 +1,28 @@ +services: + mariadb: + image: mariadb:11 + container_name: portail_mariadb + restart: unless-stopped + ports: + - "127.0.0.1:{{ portail_mysql_port }}:3306" + environment: + MARIADB_ROOT_PASSWORD: "{{ portail_mysql_root_password }}" + MARIADB_DATABASE: "{{ portail_mysql_database }}" + MARIADB_USER: "{{ portail_mysql_user }}" + MARIADB_PASSWORD: "{{ portail_mysql_password }}" + volumes: + - portail_mariadb_data:/var/lib/mysql + + redis: + image: redis:7 + container_name: portail_redis + restart: unless-stopped + command: ["redis-server", "--requirepass", "{{ portail_redis_password }}"] + ports: + - "127.0.0.1:{{ portail_redis_port }}:6379" + volumes: + - portail_redis_data:/data + +volumes: + portail_mariadb_data: + portail_redis_data: diff --git a/ansible/roles/portail/templates/env.production.j2 b/ansible/roles/portail/templates/env.production.j2 new file mode 100644 index 0000000..2335cd8 --- /dev/null +++ b/ansible/roles/portail/templates/env.production.j2 @@ -0,0 +1,29 @@ +APP_NAME=Syllabus +APP_ENV=production +APP_KEY={{ portail_app_key }} +APP_DEBUG=false +APP_URL={{ portail_app_url }} + +APP_LOCALE=fr +APP_FALLBACK_LOCALE=fr + +LOG_CHANNEL=stack +LOG_LEVEL=warning + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT={{ portail_mysql_port }} +DB_DATABASE={{ portail_mysql_database }} +DB_USERNAME={{ portail_mysql_user }} +DB_PASSWORD={{ portail_secrets.MYSQL_PASSWORD }} + +SESSION_DRIVER=redis +SESSION_LIFETIME=120 +CACHE_STORE=redis +QUEUE_CONNECTION=redis + +REDIS_HOST=127.0.0.1 +REDIS_PORT={{ portail_redis_port }} +REDIS_PASSWORD={{ portail_secrets.REDIS_PASSWORD }} + +MAIL_MAILER=log diff --git a/ansible/roles/portail/templates/nginx-portail.conf.j2 b/ansible/roles/portail/templates/nginx-portail.conf.j2 new file mode 100644 index 0000000..cd2501e --- /dev/null +++ b/ansible/roles/portail/templates/nginx-portail.conf.j2 @@ -0,0 +1,28 @@ +server { + listen 80; + listen [::]:80; + server_name _; + + root {{ portail_dossier_app }}/public; + index index.php; + + client_max_body_size 20m; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass unix:/run/php/php{{ portail_php_version }}-fpm.sock; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + location ~ /\.(?!well-known) { + deny all; + } + + access_log /var/log/nginx/portail-access.log; + error_log /var/log/nginx/portail-error.log; +} diff --git a/docs/journal.md b/docs/journal.md index d4394d7..e4c3e76 100644 --- a/docs/journal.md +++ b/docs/journal.md @@ -272,3 +272,45 @@ La bonne nouvelle de cette étape, c'est que Kolla avait déjà fait le plus gro **Suite** Étape 8 : portail Laravel (Blade, Vite, MariaDB/Redis pour l'app, modèles, connexion prof/élève, salle d'attente). Le dossier `design/` doit exister : à vérifier avant de commencer. + +## 2026-10-07 — Étape 8 : portail Laravel + +**Fait** + +- Projet Laravel 13 créé dans `portail/` (développé en local sur le Mac : PHP 8.4/Composer/Node 22 y étaient déjà installés), plateforme Composer forcée à PHP 8.3 pour que les dépendances restent installables sur le serveur (PHP 8.3, via Sury). Pest 4 (Pest 5 exige PHP 8.4). `concurrently` retiré du starter (2 vulnérabilités critiques via `shell-quote`, et en réalité inutilisé : `php artisan dev` de Laravel 13 gère déjà le lancement parallèle nativement). +- Vues, routes, JS, CSS, images de `design/` repris tels quels dans `portail/resources` et `portail/public`. Toutes les pages vitrine (une cinquantaine) fonctionnent sans rien construire de plus. +- Modèles + migrations : `Etablissement`, `User` (+ `etablissement_id`, `jeton_session_actif`), `Classe` (code unique auto-généré), `Eleve` (identifiant unique, `code_secret` haché, `jeton_qr` à usage unique, `jeton_session_actif`), `Lab`, `Seance`, `LabSession`. +- Deux guards d'authentification (`config/auth.php`) : `web` (profs, email/mot de passe) et `eleve` (identifiant/code secret, sans e-mail — minimisation des données mineurs). `App\Models\Eleve` authentifiable via le trait `Illuminate\Auth\Authenticatable` et `getAuthPasswordName()`. +- **Session unique par compte** : trait `GereSessionUnique` (un jeton en base, recréé à chaque connexion) + middleware `VerifieSessionUnique` (`session.unique:web`/`session.unique:eleve`), qui déconnecte et prévient si le jeton de la session en cours ne correspond plus à celui, plus récent, enregistré en base. +- Connexion (`/connexion`, un seul écran, deux formulaires), inscription (`/inscription` : crée établissement + prof + première classe en une transaction), gestion de classe (`/mes-classes/...` : ajout rapide, import CSV, génération identifiant + code à 6 caractères + QR à usage unique via `bacon/bacon-qr-code` en SVG, fiches imprimables), salle d'attente de séance (`/seances/...`, rafraîchissement automatique, le prof admet). +- Service `GenerateurCompteEleve` : identifiant `prenom.initiale` (suffixe numérique si homonyme), code secret à 6 caractères sans caractères ambigus (`0/O`, `1/I/L`). +- 11 tests Pest (connexion prof, connexion élève, session unique × 2, création de classe × 3) : tous passent, base SQLite en mémoire. +- Déploiement : rôle Ansible `portail` (PHP 8.3 via Sury, Nginx, Composer avec vérification de signature officielle, Node 22 LTS via NodeSource, MariaDB+Redis de l'app en Docker Compose sur `127.0.0.1:3307`/`127.0.0.1:6380` — jamais sur `10.10.0.1`, réservé à Kolla), synchronisation du code par `rsync`, `.env` de production généré une seule fois (secrets jamais dans Git). + +**Deux décisions de design tranchées par Adam** + +1. Champ mot de passe ajouté au formulaire d'inscription (absent de la maquette) : sans lui, la connexion prof décrite dans le prompt n'aurait pas pu fonctionner sans serveur mail. +2. Écrans sans maquette (gestion des élèves, salle d'attente) : design minimal sobre, mêmes jetons visuels que le reste du site, pas de recherche d'effet « wow ». + +**Problèmes rencontrés et solution** + +1. `@auth` (sans argument) dans le layout partagé retournait `true` pour un **élève** connecté, affichant le menu du prof. Cause : le middleware `auth:eleve` appelle `Auth::shouldUse('eleve')`, ce qui change le *guard par défaut* pour le reste de la requête — `@auth` nu teste alors ce guard-là, pas forcément `web`. Corrigé en précisant partout `@auth('web')` / `auth('eleve')`. +2. Déploiement : j'ai modifié `kolla/globals.yml` et le script de sauvegarde dans le dépôt sans les repousser sur le serveur avant de relancer les outils correspondants (déjà rencontré à l'étape 7 — même piège, maintenant documenté deux fois). +3. `php artisan key:generate` échouait : lancé avant `composer install`, donc avant que `vendor/autoload.php` existe. Réordonné : code → composer install → clé/`.env` → npm → migrations. +4. `php artisan view:cache` échouait (« View path not found ») : `storage/framework/views` avait été exclu du `rsync` mais n'était créé par rien d'autre. Plus largement, **`storage/` et `bootstrap/cache/` ne doivent jamais être synchronisés depuis le poste de dev** (ce sont de l'état et du cache runtime, pas du code — un `rsync --delete` y effacerait un jour les travaux déposés par les élèves dans `storage/app/`). Corrigé : leur structure est créée une fois sur le serveur, propriété `www-data` dès le départ, et ils sont exclus en bloc du `rsync` (pas juste leurs sous-dossiers). +5. **Un handler Ansible qui notifie un autre handler ne s'est pas propagé avant la fin du play** : Nginx a continué de servir sa page par défaut malgré un déploiement sans erreur apparente. Corrigé en fusionnant les deux handlers (`nginx -t` puis `systemctl reload nginx`) en un seul. Détecté uniquement parce que le critère de fin a été vérifié sur le serveur réel, pas seulement sur le code local. +6. Vérification Playwright visuelle (demandée par le prompt) non faite : l'extension Chrome n'était pas connectée dans cette session. Vérification faite autrement : les mêmes fichiers Blade que `design/` sont repris sans changement de structure, et le flux complet a été vérifié par capture HTML (`curl`) côté connexion, tableau de bord et gestion de classe. + +**Critère vérifié (sur le serveur réel, pas seulement en local)** + +- 11/11 tests Pest passent (en local, avant déploiement). +- Un élève fictif (`lea.m`) s'est connecté sur `http://163.172.4.195/connexion` et a vu son tableau de bord (« Salut Léa », nom de sa classe) — données de test supprimées après coup (accord d'Adam). +- `nft list tables` après déploiement : toujours seulement `inet filter` + `ip nat` (+ `f2b-table`) — Nginx/PHP-FPM n'ont rien ajouté au pare-feu. + +**Comprendre** + +Cette étape a un piège intéressant pour la suite : Laravel a deux systèmes de comptes (profs et élèves), et un mécanisme Laravel conçu pour un seul système (« quel est l'utilisateur connecté ? ») suppose souvent qu'il n'y en a qu'un — le bug du menu qui affichait « Mes classes » à un élève connecté vient exactement de là. Chaque fois qu'on ajoutera un rôle (établissement, formateur, opérateur), il faudra se souvenir d'être explicite sur le guard, partout. L'autre leçon, plus générale : un déploiement qui se termine « sans erreur » ne veut pas dire que le résultat est correct — c'est en testant le vrai critère de fin sur le serveur réel (pas juste en relisant les logs Ansible) qu'on a trouvé le bug du handler Nginx. + +**Suite** + +Étape 9 : proxy console Go et xterm.js (jeton Redis à usage unique, relais WebSocket vers `nova-serialproxy`, lecture seule pour le prof). diff --git a/docs/runbooks/portail.md b/docs/runbooks/portail.md new file mode 100644 index 0000000..57118bb --- /dev/null +++ b/docs/runbooks/portail.md @@ -0,0 +1,97 @@ +# Runbook — Portail Laravel + +## Où sont les choses + +| Élément | Emplacement | +|---|---| +| Code de l'application | `/var/www/portail` (propriétaire `adam`, sauf `storage/` et `bootstrap/cache/` → `www-data`) | +| Secrets Laravel (`.env`) | `/var/www/portail/.env`, `640 adam:www-data`, généré une seule fois, jamais dans Git | +| Secrets MariaDB/Redis de l'app | `/etc/portail/.env`, `600 root:root`, générés une seule fois | +| Config Docker Compose (MariaDB + Redis app) | `/etc/portail/docker-compose.yml` | +| Vhost Nginx | `/etc/nginx/sites-available/portail.conf` | +| PHP | 8.3 (dépôt Sury), FPM sur `/run/php/php8.3-fpm.sock` | + +Déployé par `ansible/portail.yml` (rôle `portail`) : +``` +cd ansible && script -q /dev/null ansible-playbook portail.yml -i inventory.ini +``` + +## Architecture de l'app + +- Laravel 13, Blade + Vite (Tailwind v4 via le plugin Vite officiel). +- Deux comptes distincts, deux guards (`config/auth.php`) : + - `web` → `User` (profs), email + mot de passe. + - `eleve` → `Eleve` (élèves), identifiant (`prenom.initiale`, suffixe numérique si homonyme) + code secret à 6 caractères (haché). +- **Une seule session active par compte** (`App\Models\Concerns\GereSessionUnique` + middleware `session.unique`) : un jeton est stocké en session ET en base à chaque connexion ; s'ils ne correspondent plus, la session est coupée à la requête suivante avec un message. +- Vues reprises telles quelles depuis `design/` (voir le journal du 2026-10-07, étape 8, pour les deux écarts assumés : mot de passe ajouté au formulaire d'inscription, sélecteur d'établissement retiré du formulaire de connexion). +- Écrans sans maquette, construits sobres avec les jetons visuels existants : `resources/views/mes-classes/*` (gestion des élèves, import CSV, fiches QR) et `resources/views/seances/*` (salle d'attente). + +## Base de données et cache de l'app + +MariaDB et Redis **dédiés à l'app**, en Docker Compose, liés à `127.0.0.1` uniquement (jamais sur `10.10.0.1`, qui est le réseau de Kolla) : + +- MariaDB : port `3307` (celui de Kolla est sur `10.10.0.1:3306`). +- Redis : port `6380`. + +``` +cd /etc/portail && docker compose ps +``` + +## Déployer une mise à jour du code + +``` +cd ansible && script -q /dev/null ansible-playbook portail.yml -i inventory.ini +``` + +Le rôle synchronise le code (`rsync`, sans `vendor/`, `node_modules/`, `.env`, +ni `storage/`/`bootstrap/cache/` — ce sont de l'état runtime, jamais du code), +puis `composer install --no-dev`, `npm ci && npm run build`, +`php artisan migrate --force`, et recrée les caches +(`config:cache`, `route:cache`, `view:cache`). + +**Après toute modification de code qui touche les routes ou la config**, les +caches doivent être régénérés (le rôle le fait automatiquement). Si on modifie +une vue sans repasser par Ansible, penser à `php artisan view:clear` à la main. + +## Lancer les tests (en local, avant tout déploiement) + +``` +cd portail && ./vendor/bin/pest +``` + +11 tests : connexion prof, connexion élève, session unique (prof et élève), +création de classe + établissement. Base de test : SQLite en mémoire +(`phpunit.xml`), aucun rapport avec MariaDB de prod. + +## Écarts au design assumés le 2026-10-07 + +- `auth/inscription.blade.php` : champ mot de passe ajouté (absent de la + maquette), confirmé par Adam — sans lui, la connexion prof décrite dans le + prompt de déploiement n'aurait pas pu fonctionner sans serveur mail. +- `auth/connexion.blade.php` : le sélecteur d'établissement du formulaire + enseignant a été retiré (un prof qui se connecte a déjà un établissement en + base ; ce sélecteur n'avait de sens que pour l'inscription). +- `paillasse-contenus/README.md` corrigé : `tree` n'est pas préinstallé dans + l'image (le lab 02 demande à l'élève de l'installer lui-même). +- Gestion des élèves (ajout, import CSV, fiches QR) et salle d'attente : + aucune maquette fournie, design minimal sobre construit avec les jetons + visuels existants (confirmé par Adam). + +## Piège vécu le 2026-10-07 + +Un handler Ansible qui notifie un autre handler (`recharger nginx` → +`redémarrer nginx`) ne s'est **pas** propagé avant la fin du play : Nginx a +continué de servir sa page par défaut malgré un déploiement « réussi ». +Corrigé en fusionnant les deux handlers en un seul +(`nginx -t && systemctl reload nginx`). Si un jour un changement Nginx semble +sans effet, vérifier d'abord `sudo nginx -T` sur le serveur, puis forcer +`sudo systemctl reload nginx` à la main. + +## Vérifié le 2026-10-07 + +- `curl http://163.172.4.195/connexion` : les deux formulaires s'affichent. +- Un élève fictif (`lea.m` / code à 6 caractères) s'est connecté et a vu son + tableau de bord (« Salut Léa », nom de sa classe) — données de test + supprimées juste après (accord d'Adam). +- `nft list tables` après déploiement : toujours seulement `inet filter` et + `ip nat` (+ `f2b-table` de fail2ban) — Nginx n'a rien ajouté au pare-feu. diff --git a/portail/.editorconfig b/portail/.editorconfig new file mode 100644 index 0000000..6df8428 --- /dev/null +++ b/portail/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[{compose,docker-compose}.{yml,yaml}] +indent_size = 4 diff --git a/portail/.env.example b/portail/.env.example new file mode 100644 index 0000000..40bb13b --- /dev/null +++ b/portail/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost:8000 + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/portail/.gitattributes b/portail/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/portail/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/portail/.gitignore b/portail/.gitignore new file mode 100644 index 0000000..f2e8cf6 --- /dev/null +++ b/portail/.gitignore @@ -0,0 +1,28 @@ +*.log +.DS_Store +.env +/database/database.sqlite +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.codex +/.cursor/ +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/fonts-manifest.dev.json +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +_ide_helper.php +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/portail/.npmrc b/portail/.npmrc new file mode 100644 index 0000000..495a6af --- /dev/null +++ b/portail/.npmrc @@ -0,0 +1,2 @@ +ignore-scripts=true +audit=true diff --git a/portail/README.md b/portail/README.md new file mode 100644 index 0000000..5ad1377 --- /dev/null +++ b/portail/README.md @@ -0,0 +1,58 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +In addition, [Laracasts](https://laracasts.com) contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +You can also watch bite-sized lessons with real-world projects on [Laravel Learn](https://laravel.com/learn), where you will be guided through building a Laravel application from scratch while learning PHP fundamentals. + +## Agentic Development + +Laravel's predictable structure and conventions make it ideal for AI coding agents like Claude Code, Cursor, and GitHub Copilot. Install [Laravel Boost](https://laravel.com/docs/ai) to supercharge your AI workflow: + +```bash +composer require laravel/boost --dev + +php artisan boost:install +``` + +Boost provides your agent 15+ tools and skills that help agents build Laravel applications while following best practices. + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/portail/app/Http/Controllers/Auth/ConnexionController.php b/portail/app/Http/Controllers/Auth/ConnexionController.php new file mode 100644 index 0000000..58ea1aa --- /dev/null +++ b/portail/app/Http/Controllers/Auth/ConnexionController.php @@ -0,0 +1,132 @@ +validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'string'], + ]); + + $this->appliquerLimiteTentatives($request, 'prof:'.$donnees['email']); + + if (! Auth::guard('web')->attempt($donnees)) { + RateLimiter::hit($this->cleLimite($request, 'prof:'.$donnees['email'])); + + throw ValidationException::withMessages([ + 'email' => 'Identifiants incorrects.', + ]); + } + + RateLimiter::clear($this->cleLimite($request, 'prof:'.$donnees['email'])); + $request->session()->regenerate(); + + /** @var User $prof */ + $prof = Auth::guard('web')->user(); + $avaitDejaUneSession = $prof->jeton_session_actif !== null; + $request->session()->put('jeton_session_web', $prof->demarreNouvelleSession()); + + return redirect()->route('mes-classes.index') + ->with('session_precedente_fermee', $avaitDejaUneSession); + } + + public function connexionEleve(Request $request): RedirectResponse + { + $donnees = $request->validate([ + 'identifiant' => ['required', 'string'], + 'code' => ['required', 'string'], + ]); + + $this->appliquerLimiteTentatives($request, 'eleve:'.$donnees['identifiant']); + + $succes = Auth::guard('eleve')->attempt([ + 'identifiant' => $donnees['identifiant'], + 'password' => $donnees['code'], + ]); + + if (! $succes) { + RateLimiter::hit($this->cleLimite($request, 'eleve:'.$donnees['identifiant'])); + + throw ValidationException::withMessages([ + 'identifiant' => 'Identifiant ou code secret incorrect.', + ]); + } + + RateLimiter::clear($this->cleLimite($request, 'eleve:'.$donnees['identifiant'])); + $request->session()->regenerate(); + + /** @var Eleve $eleve */ + $eleve = Auth::guard('eleve')->user(); + $eleve->forceFill(['derniere_connexion_a' => now()])->save(); + $request->session()->put('jeton_session_eleve', $eleve->demarreNouvelleSession()); + + return redirect()->route('eleve'); + } + + /** + * Connexion directe par QR (à usage unique, voir App\Services\GenerateurCompteEleve). + */ + public function connexionParQr(Request $request, string $jeton): RedirectResponse + { + $eleve = Eleve::where('jeton_qr', $jeton)->first(); + + if ($eleve === null) { + abort(404); + } + + $eleve->forceFill(['jeton_qr' => null, 'derniere_connexion_a' => now()])->save(); + Auth::guard('eleve')->login($eleve); + $request->session()->regenerate(); + $request->session()->put('jeton_session_eleve', $eleve->demarreNouvelleSession()); + + return redirect()->route('eleve'); + } + + public function deconnexion(Request $request): RedirectResponse + { + if (Auth::guard('web')->check()) { + Auth::guard('web')->user()->terminerSession(); + Auth::guard('web')->logout(); + } + + if (Auth::guard('eleve')->check()) { + Auth::guard('eleve')->user()->terminerSession(); + Auth::guard('eleve')->logout(); + } + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('accueil'); + } + + private function cleLimite(Request $request, string $identifiant): string + { + return 'connexion:'.$identifiant.'|'.$request->ip(); + } + + private function appliquerLimiteTentatives(Request $request, string $identifiant): void + { + $cle = $this->cleLimite($request, $identifiant); + + if (RateLimiter::tooManyAttempts($cle, 5)) { + $secondes = RateLimiter::availableIn($cle); + + throw ValidationException::withMessages([ + 'identifiant' => "Trop de tentatives. Réessaie dans {$secondes} secondes.", + ]); + } + } +} diff --git a/portail/app/Http/Controllers/ClasseController.php b/portail/app/Http/Controllers/ClasseController.php new file mode 100644 index 0000000..2b51030 --- /dev/null +++ b/portail/app/Http/Controllers/ClasseController.php @@ -0,0 +1,131 @@ +user()->classes()->withCount('eleves')->latest()->get(); + + return view('mes-classes.index', ['classes' => $classes]); + } + + public function show(Classe $classe): View + { + $this->verifierProprietaire($classe); + + return view('mes-classes.show', [ + 'classe' => $classe->load('eleves'), + ]); + } + + public function storeEleve(Request $request, Classe $classe): RedirectResponse + { + $this->verifierProprietaire($classe); + + $donnees = $request->validate([ + 'prenom' => ['required', 'string', 'max:100'], + 'nom' => ['nullable', 'string', 'max:100'], + ]); + + $fiche = $this->creerEleve($classe, $donnees['prenom'], $donnees['nom'] ?? null); + + return redirect()->route('mes-classes.show', $classe) + ->with('fiches', [$fiche]); + } + + public function importerCsv(Request $request, Classe $classe): RedirectResponse + { + $this->verifierProprietaire($classe); + + $request->validate([ + 'fichier' => ['required', 'file', 'mimes:csv,txt'], + ]); + + $lignes = file($request->file('fichier')->getRealPath(), FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $fiches = []; + + foreach ($lignes as $ligne) { + $colonnes = str_getcsv($ligne); + $prenom = trim($colonnes[0] ?? ''); + + if ($prenom === '' || Str::lower($prenom) === 'prenom') { + continue; // ligne vide ou en-tête + } + + $nom = trim($colonnes[1] ?? '') ?: null; + $fiches[] = $this->creerEleve($classe, $prenom, $nom); + } + + return redirect()->route('mes-classes.show', $classe) + ->with('fiches', $fiches) + ->with('nombre_importes', count($fiches)); + } + + public function regenererCode(Classe $classe, Eleve $eleve): RedirectResponse + { + $this->verifierProprietaire($classe); + abort_if($eleve->classe_id !== $classe->id, 404); + + $codeSecret = $this->generateur->genererCodeSecret(); + $jetonQr = Str::random(32); + + $eleve->forceFill([ + 'code_secret' => Hash::make($codeSecret), + 'jeton_qr' => $jetonQr, + ])->save(); + + return redirect()->route('mes-classes.show', $classe) + ->with('fiches', [[ + 'identifiant' => $eleve->identifiant, + 'prenom' => $eleve->prenom, + 'nom' => $eleve->nom, + 'code_secret' => $codeSecret, + 'jeton_qr' => $jetonQr, + ]]); + } + + /** + * @return array{identifiant: string, prenom: string, nom: ?string, code_secret: string, jeton_qr: string} + */ + private function creerEleve(Classe $classe, string $prenom, ?string $nom): array + { + $identifiant = $this->generateur->genererIdentifiant($prenom, $nom); + $codeSecret = $this->generateur->genererCodeSecret(); + $jetonQr = Str::random(32); + + $classe->eleves()->create([ + 'prenom' => $prenom, + 'nom' => $nom, + 'identifiant' => $identifiant, + 'code_secret' => Hash::make($codeSecret), + 'jeton_qr' => $jetonQr, + ]); + + return [ + 'identifiant' => $identifiant, + 'prenom' => $prenom, + 'nom' => $nom, + 'code_secret' => $codeSecret, + 'jeton_qr' => $jetonQr, + ]; + } + + private function verifierProprietaire(Classe $classe): void + { + abort_if($classe->user_id !== Auth::guard('web')->id(), 403); + } +} diff --git a/portail/app/Http/Controllers/Controller.php b/portail/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/portail/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +user()->load('classe'); + + return view('eleve', ['eleve' => $eleve]); + } +} diff --git a/portail/app/Http/Controllers/InscriptionController.php b/portail/app/Http/Controllers/InscriptionController.php new file mode 100644 index 0000000..5793e7c --- /dev/null +++ b/portail/app/Http/Controllers/InscriptionController.php @@ -0,0 +1,63 @@ +validate([ + 'prenom' => ['required', 'string', 'max:100'], + 'nom' => ['required', 'string', 'max:100'], + 'email' => ['required', 'email', 'unique:users,email'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + 'etablissement_nom_liste' => ['required', 'string'], + 'etablissement_nom' => ['required_if:etablissement_nom_liste,autre', 'nullable', 'string', 'max:150'], + 'etablissement_ville' => ['required_if:etablissement_nom_liste,autre', 'nullable', 'string', 'max:100'], + 'etablissement_uai' => ['nullable', 'string', 'max:20'], + 'formation' => ['nullable', 'string', 'max:100'], + 'classe' => ['required', 'string', 'max:100'], + ]); + + [$prof, $classe] = DB::transaction(function () use ($donnees) { + $etablissement = $donnees['etablissement_nom_liste'] === 'autre' + ? Etablissement::firstOrCreate( + ['nom' => $donnees['etablissement_nom']], + ['ville' => $donnees['etablissement_ville'], 'uai' => $donnees['etablissement_uai'] ?? null], + ) + : Etablissement::firstOrCreate(['nom' => $donnees['etablissement_nom_liste']]); + + $prof = User::create([ + 'name' => trim($donnees['prenom'].' '.$donnees['nom']), + 'email' => $donnees['email'], + 'password' => Hash::make($donnees['password']), + 'etablissement_id' => $etablissement->id, + ]); + + $classe = Classe::create([ + 'etablissement_id' => $etablissement->id, + 'user_id' => $prof->id, + 'nom' => $donnees['classe'], + 'formation' => $donnees['formation'] ?? null, + ]); + + return [$prof, $classe]; + }); + + Auth::guard('web')->login($prof); + $request->session()->regenerate(); + $request->session()->put('jeton_session_web', $prof->demarreNouvelleSession()); + + return redirect()->route('mes-classes.show', $classe) + ->with('classe_creee', true); + } +} diff --git a/portail/app/Http/Controllers/SeanceController.php b/portail/app/Http/Controllers/SeanceController.php new file mode 100644 index 0000000..3c7ebfa --- /dev/null +++ b/portail/app/Http/Controllers/SeanceController.php @@ -0,0 +1,85 @@ +user_id !== Auth::guard('web')->id(), 403); + + $donnees = $request->validate([ + 'lab_id' => ['required', 'exists:labs,id'], + ]); + + $seance = $classe->seances()->create([ + 'lab_id' => $donnees['lab_id'], + 'statut' => Seance::SALLE_ATTENTE, + 'demarree_a' => now(), + ]); + + return redirect()->route('seances.attente', $seance); + } + + public function attente(Seance $seance): View + { + abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403); + + return view('seances.attente', [ + 'seance' => $seance->load(['classe', 'lab', 'labSessions.eleve']), + ]); + } + + public function admettre(Seance $seance, LabSession $labSession): RedirectResponse + { + abort_if($seance->classe->user_id !== Auth::guard('web')->id(), 403); + abort_if($labSession->seance_id !== $seance->id, 404); + + $labSession->update(['etat' => LabSession::ADMIS]); + + return redirect()->route('seances.attente', $seance); + } + + /** + * Un élève rejoint la séance : il patiente jusqu'à ce que le prof l'admette. + */ + public function rejoindre(Request $request, Seance $seance): RedirectResponse + { + $eleve = Auth::guard('eleve')->user(); + abort_if($eleve->classe_id !== $seance->classe_id, 403); + + LabSession::firstOrCreate( + ['seance_id' => $seance->id, 'eleve_id' => $eleve->id], + ['lab_id' => $seance->lab_id, 'etat' => LabSession::EN_ATTENTE], + ); + + return redirect()->route('seances.eleve.attente', $seance); + } + + public function attenteEleve(Seance $seance): View|RedirectResponse + { + $eleve = Auth::guard('eleve')->user(); + abort_if($eleve->classe_id !== $seance->classe_id, 403); + + $labSession = LabSession::where('seance_id', $seance->id) + ->where('eleve_id', $eleve->id) + ->first(); + + if ($labSession === null) { + return redirect()->route('eleve'); + } + + return view('seances.attente-eleve', [ + 'seance' => $seance->load('lab'), + 'labSession' => $labSession, + ]); + } +} diff --git a/portail/app/Http/Middleware/VerifieSessionUnique.php b/portail/app/Http/Middleware/VerifieSessionUnique.php new file mode 100644 index 0000000..13e336a --- /dev/null +++ b/portail/app/Http/Middleware/VerifieSessionUnique.php @@ -0,0 +1,36 @@ +user(); + + if ($utilisateur !== null) { + $cleSession = "jeton_session_{$garde}"; + + if (! $utilisateur->sessionEstEncoreValide($request->session()->get($cleSession))) { + Auth::guard($garde)->logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login') + ->with('session_coupee', 'Vous avez été déconnecté : une nouvelle connexion a eu lieu sur votre compte.'); + } + } + + return $next($request); + } +} diff --git a/portail/app/Models/Classe.php b/portail/app/Models/Classe.php new file mode 100644 index 0000000..3c08788 --- /dev/null +++ b/portail/app/Models/Classe.php @@ -0,0 +1,61 @@ + 'datetime', + ]; + + protected static function booted(): void + { + static::creating(function (Classe $classe) { + $classe->code_classe ??= static::genererCodeUnique(); + }); + } + + public static function genererCodeUnique(): string + { + do { + $code = Str::upper(Str::random(6)); + } while (static::where('code_classe', $code)->exists()); + + return $code; + } + + public function etablissement(): BelongsTo + { + return $this->belongsTo(Etablissement::class); + } + + public function prof(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function eleves(): HasMany + { + return $this->hasMany(Eleve::class); + } + + public function seances(): HasMany + { + return $this->hasMany(Seance::class); + } + + public function estArchivee(): bool + { + return $this->archivee_a !== null; + } +} diff --git a/portail/app/Models/Concerns/GereSessionUnique.php b/portail/app/Models/Concerns/GereSessionUnique.php new file mode 100644 index 0000000..a41949c --- /dev/null +++ b/portail/app/Models/Concerns/GereSessionUnique.php @@ -0,0 +1,31 @@ +forceFill(['jeton_session_actif' => $jeton])->save(); + + return $jeton; + } + + public function sessionEstEncoreValide(?string $jeton): bool + { + return $jeton !== null && $jeton === $this->jeton_session_actif; + } + + public function terminerSession(): void + { + $this->forceFill(['jeton_session_actif' => null])->save(); + } +} diff --git a/portail/app/Models/Eleve.php b/portail/app/Models/Eleve.php new file mode 100644 index 0000000..a1f0b98 --- /dev/null +++ b/portail/app/Models/Eleve.php @@ -0,0 +1,46 @@ + 'datetime', + ]; + + public function getAuthPasswordName(): string + { + return 'code_secret'; + } + + public function classe(): BelongsTo + { + return $this->belongsTo(Classe::class); + } + + public function labSessions(): HasMany + { + return $this->hasMany(LabSession::class); + } + + public function nomAffiche(): string + { + return $this->nom + ? $this->prenom.' '.mb_substr($this->nom, 0, 1).'.' + : $this->prenom; + } +} diff --git a/portail/app/Models/Etablissement.php b/portail/app/Models/Etablissement.php new file mode 100644 index 0000000..e8229b2 --- /dev/null +++ b/portail/app/Models/Etablissement.php @@ -0,0 +1,24 @@ +hasMany(User::class); + } + + public function classes(): HasMany + { + return $this->hasMany(Classe::class); + } +} diff --git a/portail/app/Models/Lab.php b/portail/app/Models/Lab.php new file mode 100644 index 0000000..b305066 --- /dev/null +++ b/portail/app/Models/Lab.php @@ -0,0 +1,19 @@ +hasMany(Seance::class); + } +} diff --git a/portail/app/Models/LabSession.php b/portail/app/Models/LabSession.php new file mode 100644 index 0000000..bbc2f69 --- /dev/null +++ b/portail/app/Models/LabSession.php @@ -0,0 +1,43 @@ + 'datetime', + ]; + + public function seance(): BelongsTo + { + return $this->belongsTo(Seance::class); + } + + public function eleve(): BelongsTo + { + return $this->belongsTo(Eleve::class); + } + + public function lab(): BelongsTo + { + return $this->belongsTo(Lab::class); + } +} diff --git a/portail/app/Models/Seance.php b/portail/app/Models/Seance.php new file mode 100644 index 0000000..a333d18 --- /dev/null +++ b/portail/app/Models/Seance.php @@ -0,0 +1,43 @@ + 'datetime', + 'terminee_a' => 'datetime', + ]; + + public function classe(): BelongsTo + { + return $this->belongsTo(Classe::class); + } + + public function lab(): BelongsTo + { + return $this->belongsTo(Lab::class); + } + + public function labSessions(): HasMany + { + return $this->hasMany(LabSession::class); + } +} diff --git a/portail/app/Models/User.php b/portail/app/Models/User.php new file mode 100644 index 0000000..1e9abe1 --- /dev/null +++ b/portail/app/Models/User.php @@ -0,0 +1,45 @@ + */ + use GereSessionUnique, HasFactory, Notifiable; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + public function etablissement(): BelongsTo + { + return $this->belongsTo(Etablissement::class); + } + + public function classes(): HasMany + { + return $this->hasMany(Classe::class); + } +} diff --git a/portail/app/Providers/AppServiceProvider.php b/portail/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..452e6b6 --- /dev/null +++ b/portail/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ +exists()) { + $identifiant = $base.$suffixe; + $suffixe++; + } + + return $identifiant; + } + + public function genererCodeSecret(): string + { + $code = ''; + for ($i = 0; $i < 6; $i++) { + $code .= self::ALPHABET_CODE[random_int(0, strlen(self::ALPHABET_CODE) - 1)]; + } + + return $code; + } +} diff --git a/portail/app/Support/QrCode.php b/portail/app/Support/QrCode.php new file mode 100644 index 0000000..196452f --- /dev/null +++ b/portail/app/Support/QrCode.php @@ -0,0 +1,22 @@ +writeString($contenu); + } +} diff --git a/portail/artisan b/portail/artisan new file mode 100755 index 0000000..c35e31d --- /dev/null +++ b/portail/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/portail/bootstrap/app.php b/portail/bootstrap/app.php new file mode 100644 index 0000000..3c8ca9d --- /dev/null +++ b/portail/bootstrap/app.php @@ -0,0 +1,23 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->alias([ + 'session.unique' => \App\Http\Middleware\VerifieSessionUnique::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->shouldRenderJsonWhen( + fn (Request $request) => $request->is('api/*') || $request->expectsJson(), + ); + })->create(); diff --git a/portail/bootstrap/cache/.gitignore b/portail/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/bootstrap/providers.php b/portail/bootstrap/providers.php new file mode 100644 index 0000000..fc94ae6 --- /dev/null +++ b/portail/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2026-09-06T14:11:38+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "reference": "71d305eaa6480d3d3460c7d053d1da7749fa9cca", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.58 || ^12.5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.3" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2026-10-01T06:59:29+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2026-08-24T09:06:52+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "8.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "93939470950a9b11e2e84204166ef5e048c55fe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/93939470950a9b11e2e84204166ef5e048c55fe4", + "reference": "93939470950a9b11e2e84204166ef5e048c55fe4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^3.0.2", + "guzzlehttp/psr7": "^3.1", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "4.0.1", + "guzzlehttp/test-server": "^1.0.1", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/8.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-09-06T13:55:09+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "42118e66a53c492effaf92bc357e931985d5c6f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/42118e66a53c492effaf92bc357e931985d5c6f9", + "reference": "42118e66a53c492effaf92bc357e931985d5c6f9", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-08-24T10:00:26+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a3059ba1a84c9139c4ae03cf0f45bea276c97c74", + "reference": "a3059ba1a84c9139c4ae03cf0f45bea276c97c74", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^2.0", + "symfony/polyfill-php80": "^1.25", + "symfony/polyfill-php82": "^1.27" + }, + "provide": { + "psr/http-factory-implementation": "1.1", + "psr/http-message-implementation": "2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "php-http/psr7-integration-tests": "^1.5.1", + "phpunit/phpunit": "^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/3.1.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-08-24T11:02:13+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7a466ad606491eb6528c717482f7cca77f1851f3", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "symfony/polyfill-php80": "^1.25" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^9.6.34", + "uri-template/tests": "1.0.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-08-24T17:13:02+00:00" + }, + { + "name": "laravel/framework", + "version": "v13.35.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "a413a9da94884e5e9d58f4e6612884f712827804" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/a413a9da94884e5e9d58f4e6612884f712827804", + "reference": "a413a9da94884e5e9d58f4e6612884f712827804", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.9 || ^3.0", + "guzzlehttp/uri-template": "^1.0 || ^2.0", + "laravel/prompts": "^0.3.11", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.10", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/http-message": "^1.0 || ^2.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/image": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "intervention/image": "^4.0", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^11.5.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "2.2.15", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "2.6.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/mercure": "^0.8.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0", + "web-token/jwt-library": "^4.1" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "intervention/image": "Required to use the image processing features (^4.0).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/mercure": "Required to use the Mercure broadcast driver (^0.8).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0).", + "web-token/jwt-library": "Required to sign Mercure JWTs and use end-to-end encrypted channels (^4.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-10-06T13:52:37+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.25", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "21b7b49d60fcb700016dd27b582ae739c75de816" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/21b7b49d60fcb700016dd27b582ae739c75de816", + "reference": "21b7b49d60fcb700016dd27b582ae739c75de816", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "source": "https://github.com/laravel/prompts/tree/v0.3.25" + }, + "time": "2026-10-06T02:25:49+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "reference": "2d5869a838bbcf37e0d8b0568fc41914e81374b5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-09-22T14:32:34+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" + }, + "time": "2026-03-17T14:54:13+00:00" + }, + { + "name": "league/commonmark", + "version": "2.10.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.11-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-09-21T13:07:34+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.36.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" + }, + "time": "2026-09-02T08:00:27+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.35.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" + }, + "time": "2026-08-12T13:29:21+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.17.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2026-07-09T11:49:27+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/d7059e4c691d05ea1222b5124bed0f961be46e15", + "reference": "d7059e4c691d05ea1222b5124bed0f961be46e15", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-09-29T19:21:12+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.14.2", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/94cfc34887f07ab5c0efa07914c8f527544e3224", + "reference": "94cfc34887f07ab5c0efa07914c8f527544e3224", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^3.0@dev", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-10-02T07:26:52+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.6", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "c54350438cd6914616f790a49cb424605f421562" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/c54350438cd6914616f790a49cb424605f421562", + "reference": "c54350438cd6914616f790a49cb424605f421562", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.6" + }, + "time": "2026-08-16T21:58:41+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.5", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.5" + }, + "time": "2026-07-17T23:02:45+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.9.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" + }, + "time": "2026-09-13T18:51:52+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2026-08-24T00:54:40+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.24", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" + }, + "time": "2026-06-29T15:41:09+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.4", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/75d73f48d02797c2c285a7e9f348fadc0102ffe2", + "reference": "75d73f48d02797c2c285a7e9f348fadc0102ffe2", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14 || ^0.15 || ^0.16 || ^0.17 || ^0.18 || ^0.19 || ^0.20 || ^1.0", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.4" + }, + "time": "2026-09-16T11:39:30+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-18T15:55:00+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "reference": "fecf40067fc8d8880ea87b8ac227600b9aadd1d0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-23T10:03:40+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-22T18:56:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d269974ee93c61d03620ffee358355bfdb471d66", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-26T06:31:10+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9d0761c0da6bd5b801b401666834901a9a1ffc33", + "reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T07:12:44+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "392784b5d7b959b8d872542a2530c50c540a788f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/392784b5d7b959b8d872542a2530c50c540a788f", + "reference": "392784b5d7b959b8d872542a2530c50c540a788f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-29T14:05:16+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "reference": "6f6f2441bef07a42d2617b9b3aadf04a321aa514", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-15T06:01:15+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "reference": "f8ac7d0a800bcabbcd1f7de56594cf05e6cd5693", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-04T10:45:44+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5cac8f4db97208823aa353f59a01faad2da52c75", + "reference": "5cac8f4db97208823aa353f59a01faad2da52c75", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "reference": "533d68cf99c8f8ee2e8893c59ed59192e92cfd74", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T14:40:04+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "reference": "ebd57ccc7edb93d632e184efbe7693d2517bab3b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:02:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "reference": "4d5d331bf731e0aedbc6b2f5e61135b7b7996a72", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T15:19:04+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php82", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/843bbe8b2a7934307b749751eede0f111f5d1cfc", + "reference": "843bbe8b2a7934307b749751eede0f111f5d1cfc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "reference": "80ccff923a8d61f73ebe9da0aa94e25232f004ce", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:50:05+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/22c6548499cbe5c5bb072fc0c42312dff062a015", + "reference": "22c6548499cbe5c5bb072fc0c42312dff062a015", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T22:01:09+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/86a42cbcc07adc331481a5ab310a4024bfc227f6", + "reference": "86a42cbcc07adc331481a5ab310a4024bfc227f6", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-php86", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/8840df45b24f33a49dc419fa18dc0057231ae1d8", + "reference": "8840df45b24f33a49dc419fa18dc0057231ae1d8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php86\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-25T15:46:47+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.43.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/b23ff8245cc5c4479ea2be3d0132d2e6dd433993", + "reference": "b23ff8245cc5c4479ea2be3d0132d2e6dd433993", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.43.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-24T05:20:52+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/ed0ae095b86994d370d5791612e55984f15aa30e", + "reference": "ed0ae095b86994d370d5791612e55984f15aa30e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-02T12:38:34+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/2bacb72be5eadeaf9874d5accb75bfb33d21c74b", + "reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-28T19:09:50+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-27T15:39:01+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/02731ce2bd26514bec9efc0baef2cd0b0cf67f46", + "reference": "02731ce2bd26514bec9efc0baef2cd0b0cf67f46", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-11T14:46:58+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:40:08+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.20", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/a96bbdb02a04ff3b5592cee6741b03edc78b56aa", + "reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.20" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-09-23T16:18:28+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.18", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/e088da50b813f32473a76871616cbb8fa54653a8", + "reference": "e088da50b813f32473a76871616cbb8fa54653a8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12|^4.0" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.18" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-30T20:10:52+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.10", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2026-08-24T18:07:49+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.20.0", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.7 || ^8.0.7", + "symfony/process": "^7.4.5 || ^8.0.5" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2026-03-29T15:46:14+00:00" + }, + { + "name": "composer/pcre", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<2.2.2" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2026-06-07T11:47:49+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/d384a7d0d6e67aa497327260ca95023e92dc2032", + "reference": "d384a7d0d6e67aa497327260ca95023e92dc2032", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0 || ^1.1.2", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "symfony/polyfill-mbstring": "^1.33", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.4.1" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2026-09-29T22:11:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.6", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "7d0a2cde892089bb09372f4366e9b960eb3001a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/7d0a2cde892089bb09372f4366e9b960eb3001a9", + "reference": "7d0a2cde892089bb09372f4366e9b960eb3001a9", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3 || ^10.5.58", + "symfony/var-dumper": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.6" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2026-10-03T17:03:57+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b61cd040da1a4925bc90a51c074f5297e7c0fa52", + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-dom": "*", + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v3.0.0" + }, + "time": "2026-03-17T11:56:53+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "laravel/agent-detector", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/agent-detector.git", + "reference": "2e4f1e00f75afa82ce2ade999c6e72b3c81d03c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/agent-detector/zipball/2e4f1e00f75afa82ce2ade999c6e72b3c81d03c5", + "reference": "2e4f1e00f75afa82ce2ade999c6e72b3c81d03c5", + "shasum": "" + }, + "require": { + "php": "^8.2.0" + }, + "require-dev": { + "laravel/pint": "^1.24.0", + "pestphp/pest": "^3.8.5|^4.1.0", + "pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2", + "phpstan/phpstan": "^2.1.26", + "rector/rector": "^2.1.7", + "symfony/var-dumper": "^7.3.3" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Laravel\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Detect if code is running in an AI agent or automated development environment", + "homepage": "https://github.com/laravel/agent-detector", + "keywords": [ + "Agent", + "ai", + "automation", + "claude", + "cursor", + "detection", + "devin", + "php" + ], + "support": { + "issues": "https://github.com/laravel/agent-detector/issues", + "source": "https://github.com/laravel/agent-detector" + }, + "time": "2026-10-05T14:40:01+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-05-20T22:24:57+00:00" + }, + { + "name": "laravel/pao", + "version": "v1.1.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/pao.git", + "reference": "620424f1ce744d96937fb6be60a94793e79b67ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pao/zipball/620424f1ce744d96937fb6be60a94793e79b67ad", + "reference": "620424f1ce744d96937fb6be60a94793e79b67ad", + "shasum": "" + }, + "require": { + "laravel/agent-detector": "^2.0.2", + "php": "^8.3" + }, + "conflict": { + "laravel/framework": "<12.0.0", + "nunomaduro/collision": "<8.9.3", + "pestphp/pest": "<4.6.3 || >=6.0.0", + "phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.20.0", + "laravel/pint": "^1.30.5", + "orchestra/testbench": "^10.11.0 || ^11.2.0", + "pestphp/pest": "^4.7.8 || ^5.1.4", + "pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.2", + "phpstan/phpstan": "^2.2.13", + "rector/rector": "^2.6.6", + "symfony/process": "^7.4.13 || ^8.1.6", + "symfony/var-dumper": "^7.4.15 || ^8.1.6" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Laravel\\Pao\\Drivers\\Pest\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Laravel\\Pao\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Laravel\\Pao\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Agent-optimized output for PHP testing tools", + "keywords": [ + "Agent", + "PHPStan", + "ai", + "dev", + "paratest", + "pest", + "php", + "phpunit", + "rector", + "testing" + ], + "support": { + "issues": "https://github.com/laravel/pao/issues", + "source": "https://github.com/laravel/pao" + }, + "time": "2026-09-07T14:48:06+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.32.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "006bd7825eb01daf92959d060b89cd9514fe4253" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/006bd7825eb01daf92959d060b89cd9514fe4253", + "reference": "006bd7825eb01daf92959d060b89cd9514fe4253", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.3.0" + }, + "require-dev": { + "composer/semver": "^3.4.4", + "friendsofphp/php-cs-fixer": "^3.95.25", + "illuminate/view": "^13.31.0", + "larastan/larastan": "^3.11.0", + "laravel-zero/framework": "^13.0.0", + "laravel/agent-detector": "^2.0.2", + "laravel/prompts": "^0.3.24", + "mockery/mockery": "^1.6.15", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^4.7.8" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-09-10T10:47:55+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.15", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/967a801bd188989a5669bd280f252d51c0fdc9ee", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0 || ^3.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6.36", + "symplify/easy-coding-standard": "^13.2.17" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2026-08-19T19:37:52+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.5", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.14 || ^8.1.1" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.10.0", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0", + "laravel/pint": "^1.29.3", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5", + "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-07-15T19:09:14+00:00" + }, + { + "name": "pestphp/pest", + "version": "v4.7.8", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/5b2293f67adcf1b2320b33f521b94a692d18f360", + "reference": "5b2293f67adcf1b2320b33f521b94a692d18f360", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.20.0", + "composer/xdebug-handler": "^3.0.5", + "nunomaduro/collision": "^8.9.5", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.2", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.13|^8.1.0" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.33", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "mrpunyapal/peststan": "^0.2.12", + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.1", + "pestphp/pest-plugin-type-coverage": "^4.0.4", + "psy/psysh": "^0.12.24" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.7.8" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-08-03T20:49:44+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" + }, + "require-dev": { + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.7" + }, + "require-dev": { + "pestphp/pest": "^4.4.6", + "pestphp/pest-dev-tools": "^4.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-04-10T17:20:19+00:00" + }, + { + "name": "pestphp/pest-plugin-laravel", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-laravel.git", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", + "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", + "shasum": "" + }, + "require": { + "laravel/framework": "^11.45.2|^12.52.0|^13.0", + "pestphp/pest": "^4.4.1", + "php": "^8.3.0" + }, + "require-dev": { + "laravel/dusk": "^8.3.6", + "orchestra/testbench": "^9.13.0|^10.9.0|^11.0", + "pestphp/pest-dev-tools": "^4.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Laravel\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Pest\\Laravel\\PestServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Laravel Plugin", + "keywords": [ + "framework", + "laravel", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-02-21T00:29:45+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/ee15a5a2022c8383906b225055e9496a6a5e6e3b", + "reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "https://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.2.1" + }, + "time": "2025-11-10T21:21:55+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "dbe2133bb23e2e33a6834f4ed417f24a1b167769" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/dbe2133bb23e2e33a6834f4ed417f24a1b167769", + "reference": "dbe2133bb23e2e33a6834f4ed417f24a1b167769", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.1.0" + }, + "time": "2026-09-27T19:32:54+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.6", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1427af4647235b4a73b13940b1f1258d3584f36e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1427af4647235b4a73b13940b1f1258d3584f36e", + "reference": "1427af4647235b4a73b13940b1f1258d3584f36e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.6" + }, + "time": "2026-09-27T20:07:45+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7440368d94738d54f2c1de24dbd3871793bbb0dd", + "reference": "7440368d94738d54f2c1de24dbd3871793bbb0dd", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.9.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0.1", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.37" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-10-03T14:03:50+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.33" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:40:53+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/babab73b7ef9fae406f2eb3ee11323a61caa63d5", + "reference": "babab73b7ef9fae406f2eb3ee11323a61caa63d5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.37" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-10-03T04:51:29+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.33", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "reference": "b98e028a26c5c5ba7e4a54be96ccf35f2914d184", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.7", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.33" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-07-28T13:58:09+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:35:54+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/9e935a16187e14f39b0e79910dabd614972cdb26", + "reference": "9e935a16187e14f39b0e79910dabd614972cdb26", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-10-04T05:50:08+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.7", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" + }, + "time": "2026-02-17T17:25:14+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "8a163706bbcac613733dba03f51e13b5eb1bb819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/8a163706bbcac613733dba03f51e13b5eb1bb819", + "reference": "8a163706bbcac613733dba03f51e13b5eb1bb819", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, + "branch-alias": { + "dev-master": "2.0-dev", + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.5.0" + }, + "time": "2026-10-05T23:14:19+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.3.0" + }, + "plugin-api-version": "2.9.0" +} diff --git a/portail/config/app.php b/portail/config/app.php new file mode 100644 index 0000000..dfa47ba --- /dev/null +++ b/portail/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache", "array" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/portail/config/auth.php b/portail/config/auth.php new file mode 100644 index 0000000..20d140b --- /dev/null +++ b/portail/config/auth.php @@ -0,0 +1,130 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + // Les élèves ne sont pas des "users" : identifiant + code secret, + // jamais d'e-mail (voir CLAUDE.md, minimisation des données mineurs). + 'eleve' => [ + 'driver' => 'session', + 'provider' => 'eleves', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + + 'eleves' => [ + 'driver' => 'eloquent', + 'model' => Eleve::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/portail/config/cache.php b/portail/config/cache.php new file mode 100644 index 0000000..d88a596 --- /dev/null +++ b/portail/config/cache.php @@ -0,0 +1,136 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "storage", "octane", + | "session", "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/locks'), + ], + + 'storage' => [ + 'driver' => 'storage', + 'disk' => env('CACHE_STORAGE_DISK'), + 'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This value determines the classes that can be unserialized from cache + | storage. By default, no PHP classes will be unserialized from your + | cache to prevent gadget chain attacks if your APP_KEY is leaked. + | + */ + + 'serializable_classes' => false, + +]; diff --git a/portail/config/database.php b/portail/config/database.php new file mode 100644 index 0000000..abbb88e --- /dev/null +++ b/portail/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/portail/config/filesystems.php b/portail/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/portail/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/portail/config/logging.php b/portail/config/logging.php new file mode 100644 index 0000000..97bdc03 --- /dev/null +++ b/portail/config/logging.php @@ -0,0 +1,140 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "monthly", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'max_files' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'monthly' => [ + 'driver' => 'monthly', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'max_files' => 3, + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/portail/config/mail.php b/portail/config/mail.php new file mode 100644 index 0000000..e32e88d --- /dev/null +++ b/portail/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/portail/config/queue.php b/portail/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/portail/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/portail/config/services.php b/portail/config/services.php new file mode 100644 index 0000000..ec226a4 --- /dev/null +++ b/portail/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/portail/config/session.php b/portail/config/session.php new file mode 100644 index 0000000..f574482 --- /dev/null +++ b/portail/config/session.php @@ -0,0 +1,233 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + + /* + |-------------------------------------------------------------------------- + | Session Serialization + |-------------------------------------------------------------------------- + | + | This value controls the serialization strategy for session data, which + | is JSON by default. Setting this to "php" allows the storage of PHP + | objects in the session but can make an application vulnerable to + | "gadget chain" serialization attacks if the APP_KEY is leaked. + | + | Supported: "json", "php" + | + */ + + 'serialization' => 'json', + +]; diff --git a/portail/database/.gitignore b/portail/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/portail/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/portail/database/factories/ClasseFactory.php b/portail/database/factories/ClasseFactory.php new file mode 100644 index 0000000..7dfdadf --- /dev/null +++ b/portail/database/factories/ClasseFactory.php @@ -0,0 +1,20 @@ + Etablissement::factory(), + 'user_id' => User::factory(), + 'nom' => 'BTS SIO 1 · Groupe '.fake()->randomLetter(), + 'formation' => 'BTS SIO SISR', + ]; + } +} diff --git a/portail/database/factories/EleveFactory.php b/portail/database/factories/EleveFactory.php new file mode 100644 index 0000000..a599be1 --- /dev/null +++ b/portail/database/factories/EleveFactory.php @@ -0,0 +1,25 @@ +firstName(); + + return [ + 'classe_id' => Classe::factory(), + 'prenom' => $prenom, + 'nom' => fake()->lastName(), + 'identifiant' => Str::slug($prenom, '').'.'.Str::random(4), + 'code_secret' => Hash::make('123456'), + 'jeton_qr' => Str::random(32), + ]; + } +} diff --git a/portail/database/factories/EtablissementFactory.php b/portail/database/factories/EtablissementFactory.php new file mode 100644 index 0000000..999be6d --- /dev/null +++ b/portail/database/factories/EtablissementFactory.php @@ -0,0 +1,17 @@ + 'Lycée '.fake()->lastName(), + 'ville' => fake()->city(), + 'uai' => fake()->bothify('0###??#'), + ]; + } +} diff --git a/portail/database/factories/LabFactory.php b/portail/database/factories/LabFactory.php new file mode 100644 index 0000000..6ceb3a0 --- /dev/null +++ b/portail/database/factories/LabFactory.php @@ -0,0 +1,21 @@ +unique()->sentence(3); + + return [ + 'identifiant' => Str::slug($titre), + 'titre' => $titre, + 'niveau' => 'decouverte', + 'duree_min' => 45, + ]; + } +} diff --git a/portail/database/factories/UserFactory.php b/portail/database/factories/UserFactory.php new file mode 100644 index 0000000..0cc90b0 --- /dev/null +++ b/portail/database/factories/UserFactory.php @@ -0,0 +1,47 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + 'etablissement_id' => Etablissement::factory(), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/portail/database/migrations/0001_01_01_000000_create_users_table.php b/portail/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/portail/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/portail/database/migrations/0001_01_01_000001_create_cache_table.php b/portail/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..06dc7a5 --- /dev/null +++ b/portail/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->bigInteger('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->bigInteger('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/portail/database/migrations/0001_01_01_000002_create_jobs_table.php b/portail/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..edac6fe --- /dev/null +++ b/portail/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,59 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedSmallInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->string('connection'); + $table->string('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/portail/database/migrations/2026_10_07_195113_create_etablissements_table.php b/portail/database/migrations/2026_10_07_195113_create_etablissements_table.php new file mode 100644 index 0000000..1b650f6 --- /dev/null +++ b/portail/database/migrations/2026_10_07_195113_create_etablissements_table.php @@ -0,0 +1,24 @@ +id(); + $table->string('nom'); + $table->string('ville')->nullable(); + $table->string('uai')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('etablissements'); + } +}; diff --git a/portail/database/migrations/2026_10_07_195114_create_classes_table.php b/portail/database/migrations/2026_10_07_195114_create_classes_table.php new file mode 100644 index 0000000..3ae5636 --- /dev/null +++ b/portail/database/migrations/2026_10_07_195114_create_classes_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('etablissement_id')->constrained(); + $table->foreignId('user_id')->constrained()->comment('le prof qui a créé la classe'); + $table->string('nom'); + $table->string('formation')->nullable(); + $table->string('code_classe', 8)->unique()->comment('code que les élèves peuvent utiliser pour rejoindre (V2)'); + $table->timestamp('archivee_a')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('classes'); + } +}; diff --git a/portail/database/migrations/2026_10_07_195115_create_eleves_table.php b/portail/database/migrations/2026_10_07_195115_create_eleves_table.php new file mode 100644 index 0000000..f021327 --- /dev/null +++ b/portail/database/migrations/2026_10_07_195115_create_eleves_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('classe_id')->constrained()->cascadeOnDelete(); + $table->string('prenom'); + $table->string('nom')->nullable()->comment('facultatif, visible uniquement du prof'); + $table->string('identifiant')->unique()->comment('format prenom.initiale, suffixe numérique si homonyme'); + $table->string('code_secret')->comment('haché, 6 caractères en clair à la génération'); + $table->string('jeton_qr')->nullable()->unique()->comment('QR à usage unique de la fiche de connexion, vidé après le premier usage'); + $table->string('jeton_session_actif')->nullable(); + $table->timestamp('derniere_connexion_a')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('eleves'); + } +}; diff --git a/portail/database/migrations/2026_10_07_195116_create_labs_table.php b/portail/database/migrations/2026_10_07_195116_create_labs_table.php new file mode 100644 index 0000000..2b774b0 --- /dev/null +++ b/portail/database/migrations/2026_10_07_195116_create_labs_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('identifiant')->unique()->comment('ex: 08-deux-lan-routeur, nom du dossier paillasse-contenus'); + $table->string('titre'); + $table->string('niveau')->nullable(); + $table->unsignedSmallInteger('duree_min')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('labs'); + } +}; diff --git a/portail/database/migrations/2026_10_07_195117_create_seances_table.php b/portail/database/migrations/2026_10_07_195117_create_seances_table.php new file mode 100644 index 0000000..85b68cf --- /dev/null +++ b/portail/database/migrations/2026_10_07_195117_create_seances_table.php @@ -0,0 +1,26 @@ +id(); + $table->foreignId('classe_id')->constrained()->cascadeOnDelete(); + $table->foreignId('lab_id')->constrained(); + $table->string('statut')->default('planifiee')->comment('planifiee, salle_attente, en_cours, terminee'); + $table->timestamp('demarree_a')->nullable(); + $table->timestamp('terminee_a')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('seances'); + } +}; diff --git a/portail/database/migrations/2026_10_07_195118_create_lab_sessions_table.php b/portail/database/migrations/2026_10_07_195118_create_lab_sessions_table.php new file mode 100644 index 0000000..47021e1 --- /dev/null +++ b/portail/database/migrations/2026_10_07_195118_create_lab_sessions_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('seance_id')->nullable()->constrained()->cascadeOnDelete(); + $table->foreignId('eleve_id')->constrained()->cascadeOnDelete(); + $table->foreignId('lab_id')->constrained(); + $table->string('stack_id')->nullable()->comment('id de la stack Heat / des VM, rempli à l’étape 10'); + $table->string('etat')->default('en_attente')->comment('en_attente, admis, actif, termine, expire'); + $table->timestamp('expire_a')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('lab_sessions'); + } +}; diff --git a/portail/database/migrations/2026_10_07_195119_add_etablissement_and_session_fields_to_users_table.php b/portail/database/migrations/2026_10_07_195119_add_etablissement_and_session_fields_to_users_table.php new file mode 100644 index 0000000..83f8a6d --- /dev/null +++ b/portail/database/migrations/2026_10_07_195119_add_etablissement_and_session_fields_to_users_table.php @@ -0,0 +1,24 @@ +foreignId('etablissement_id')->nullable()->after('id')->constrained(); + $table->string('jeton_session_actif')->nullable()->after('remember_token'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropConstrainedForeignId('etablissement_id'); + $table->dropColumn('jeton_session_actif'); + }); + } +}; diff --git a/portail/database/seeders/DatabaseSeeder.php b/portail/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..6b901f8 --- /dev/null +++ b/portail/database/seeders/DatabaseSeeder.php @@ -0,0 +1,25 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/portail/package-lock.json b/portail/package-lock.json new file mode 100644 index 0000000..86450e3 --- /dev/null +++ b/portail/package-lock.json @@ -0,0 +1,2147 @@ +{ + "name": "portail", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "laravel-vite-plugin": "^3.1", + "tailwindcss": "^4.0.0", + "vite": "^8.0.0" + }, + "optionalDependencies": { + "@laravel/multiplex": "^0.4.1" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.1.tgz", + "integrity": "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@laravel/multiplex": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@laravel/multiplex/-/multiplex-0.4.5.tgz", + "integrity": "sha512-NhDhj4b1/+6j1xYnonzx3vZa39CAUUM7iCAN0xZ5jjcYw3GKORtiXXb0XwXW74wyeVJ25vAvXzfIhy4MXmmzCw==", + "license": "MIT", + "optional": true, + "os": [ + "darwin", + "linux" + ], + "dependencies": { + "ansi-styles": "^7.0.0", + "commander": "^15.0.0", + "ink": "^7.1.0", + "react": "^19.2.7", + "string-width": "^8.2.2", + "wrap-ansi": "^10.0.0" + }, + "bin": { + "multiplex": "dist/cli.js" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "optional": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-7.0.0.tgz", + "integrity": "sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-boxes": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-4.0.1.tgz", + "integrity": "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.20 <19 || >=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "optional": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-6.1.1.tgz", + "integrity": "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^9.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "optional": true, + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ink": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/ink/-/ink-7.1.1.tgz", + "integrity": "sha512-Y43xxa1ZSPvpmfLHcN5o+OdP8Rf8ykkNJEuKYOUNZKT8wXVNLFTtEm1nSDMQkfBH+YANF4Xuu0hhZ4ejqAtN2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.3.0", + "ansi-escapes": "^7.3.0", + "ansi-styles": "^6.2.3", + "auto-bind": "^5.0.1", + "chalk": "^5.6.2", + "cli-boxes": "^4.0.1", + "cli-cursor": "^4.0.0", + "cli-truncate": "^6.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.45.1", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.33.0", + "scheduler": "^0.27.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^9.0.0", + "stack-utils": "^2.0.6", + "string-width": "^8.2.0", + "terminal-size": "^4.0.1", + "type-fest": "^5.5.0", + "widest-line": "^6.0.0", + "wrap-ansi": "^10.0.0", + "ws": "^8.20.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@types/react": ">=19.2.0", + "react": ">=19.2.0", + "react-devtools-core": ">=6.1.2" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "optional": true, + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.2.0.tgz", + "integrity": "sha512-xSxY9Gzeb/eancd8WeK09piAFP+a6i5QIBqNCKNv9L0Eq6wziwzSem7F1GvMSrtjMh5F/QVKFxn8t9naGOA66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "tinyglobby": "^0.2.12", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "fontaine": "^0.8.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "fontaine": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-reconciler": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "license": "MIT", + "optional": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT", + "optional": true + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/slice-ansi": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.1.tgz", + "integrity": "sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terminal-size": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/terminal-size/-/terminal-size-4.0.1.tgz", + "integrity": "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-fest": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz", + "integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", + "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vite/node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/vite/node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/vite/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/widest-line": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-6.0.0.tgz", + "integrity": "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.2.tgz", + "integrity": "sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT", + "optional": true + } + } +} diff --git a/portail/package.json b/portail/package.json new file mode 100644 index 0000000..f20dc02 --- /dev/null +++ b/portail/package.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "laravel-vite-plugin": "^3.1", + "tailwindcss": "^4.0.0", + "vite": "^8.0.0" + }, + "optionalDependencies": { + "@laravel/multiplex": "^0.4.1" + } +} diff --git a/portail/phpunit.xml b/portail/phpunit.xml new file mode 100644 index 0000000..e7f0a48 --- /dev/null +++ b/portail/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/portail/public/.htaccess b/portail/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/portail/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/portail/public/css/syllabus-theme.css b/portail/public/css/syllabus-theme.css new file mode 100644 index 0000000..793b0c9 --- /dev/null +++ b/portail/public/css/syllabus-theme.css @@ -0,0 +1,151 @@ +/* Syllabus — thème global. Surcharge les anciennes classes néo-brutalistes pour appliquer la DA "SaaS doux" de la bibliothèque v2. */ +@import url('https://fonts.googleapis.com/css2?family=Mulish:wght@400;500;600;700;800&display=swap'); +:root { --sy-bg:#f5f6f8; --sy-surface:#fff; --sy-line:#b8c1cf; --sy-ink:#111827; --sy-muted:#6b7280; --sy-blue:#1d5cf5; --sy-blue-dark:#174bd0; --sy-navy:#1e3a8a; } +html, body { background: var(--sy-bg) !important; color: var(--sy-ink) !important; font-family: Mulish, system-ui, sans-serif !important; } +body * { font-family: inherit; } +code, pre, kbd, .font-mono, [class*="font-mono"] { font-family: ui-monospace, "IBM Plex Mono", Menlo, monospace !important; } +a { color: inherit; } a:hover { color: var(--sy-blue); } + +/* Bordures : fines et grises, coins arrondis */ +[class~="border-4"]:not([class~="border-transparent"]), [class~="border-2"]:not([class~="border-transparent"]), [class~="border"] { border-width: 1px !important; border-color: var(--sy-line) !important; } +[class~="border-b-4"], [class~="border-b-2"] { border-bottom-width: 1px !important; border-bottom-color: var(--sy-line) !important; } +[class~="border-t-4"], [class~="border-t-2"] { border-top-width: 1px !important; border-top-color: var(--sy-line) !important; } +[class~="border-r-2"], [class~="border-l-2"] { border-color: var(--sy-line) !important; } +[class~="divide-black"] > * + * { border-color: var(--sy-line) !important; } +section[class*="border"], article[class*="border"], aside[class*="border"], div[class~="border-4"], div[class~="border-2"], li[class~="border-2"], figure[class*="border"], details[class*="border"], table { border-radius: 12px; } +a[class~="border-2"], button[class~="border-2"], summary[class~="border-2"], span[class~="border-2"], input, select, textarea { border-radius: 6px !important; } +[class*="border-dashed"] { border-style: dashed !important; } + +/* Ombres dures → ombres douces ; plus de rotations ni de décors flottants */ +[class*="shadow-["] { box-shadow: 0 1px 3px rgba(17,24,39,.06) !important; } +[class*="hover:shadow-["]:hover { box-shadow: 0 6px 18px rgba(17,24,39,.08) !important; } +[class*="rotate-"], [class*="-rotate-"] { transform: none !important; } +[aria-hidden="true"][class*="absolute"][class*="pointer-events-none"] { display: none !important; } + +/* Palette : jaune/vert/rose/lavande fluo → pastels ; rouge/noir d'action → bleu */ +[class~="bg-[#FFD23F]"] { background-color: #dbe6ff !important; color: var(--sy-navy) !important; } +[class~="bg-[#7CF2C0]"] { background-color: #dcfce7 !important; color: #166534 !important; } +[class~="bg-[#FF6FB5]"] { background-color: #fce7f3 !important; color: #9d174d !important; } +[class~="bg-[#C4B5FD]"] { background-color: #ede4ff !important; color: #5b21b6 !important; } +[class~="bg-[#FFFBEA]"], [class~="bg-[#F6F5F1]"], [class*="hover:bg-[#FFFBEA]"]:hover { background-color: #f1f2f5 !important; } +[class~="bg-[#ec3013]"] { background-color: var(--sy-blue) !important; color: #fff !important; } +[class~="bg-black"]:not(pre):not([class*="h-2"]):not([class*="h-3"]):not([class*="h-full"]) { background-color: var(--sy-blue) !important; color: #fff !important; } +[class*="hover:bg-black"]:hover { background-color: var(--sy-blue-dark) !important; color: #fff !important; } +[class~="bg-[#13233A]"] { background-color: #0f172a !important; border-radius: 12px; } +[class*="text-[#b3001b]"], [class*="text-[#ec3013]"] { color: #b91c1c !important; } +[class~="bg-[#F6F5F1]"] { background-color: #f8f9fb !important; } +body > header, header[class*="border-b"] { background: #fff !important; box-shadow: 0 1px 0 var(--sy-line) !important; } + +/* Titres un peu moins massifs */ +h1[class*="font-black"] { letter-spacing: -0.015em !important; } +[class*="font-black"] { font-weight: 800 !important; } +[class*="uppercase"][class*="tracking-wider"] { color: var(--sy-muted); } + +/* Focus */ +:focus-visible { outline: 2px solid var(--sy-blue) !important; outline-offset: 2px; } + +/* États : page courante, menu ouvert, survol */ +[aria-current="page"][class*="aria-[current=page]:bg-[#FFD23F]"], details[open] > [class*="group-open:bg-[#FFD23F]"] { background-color: #dbe6ff !important; color: var(--sy-blue) !important; border-color: transparent !important; border-radius: 6px; } +[aria-current="page"][class*="aria-[current=page]:border-black"] { border-color: transparent !important; } +[class*="hover:bg-[#FFD23F]"]:hover { background-color: #dbe6ff !important; } +[class*="hover:border-black"]:hover { border-color: var(--sy-line) !important; } + +/* Ton posé : titres plus mesurés, moins de couleur, pas d'emoji ni d'effets */ +h1:not([style*="font-size"]) { font-size: clamp(28px, 3.2vw, 40px) !important; line-height: 1.15 !important; font-weight: 800 !important; } +h2:not([style*="font-size"]) { font-size: clamp(22px, 2.2vw, 28px) !important; line-height: 1.2 !important; } +h3:not([style*="font-size"]) { font-size: 18px !important; line-height: 1.3 !important; } +[class*="text-7xl"], [class*="text-8xl"], [class*="text-6xl"] { font-size: clamp(30px, 3.4vw, 42px) !important; line-height: 1.15 !important; } +[class*="text-5xl"] { font-size: clamp(26px, 2.6vw, 32px) !important; } +[class*="leading-[0.9"], [class*="leading-[0.8"] { line-height: 1.15 !important; } +[class*="tracking-tighter"] { letter-spacing: -0.01em !important; } +[class*="hover:-translate-y"]:hover, [class*="hover:translate"]:hover { transform: none !important; } +[class*="animate-"] { animation: none !important; } +section[class~="bg-[#ec3013]"], section[class~="bg-black"], div[class~="bg-black"][class*="p-8"], div[class~="bg-black"][class*="p-6"] { background: #fff !important; color: var(--sy-ink) !important; border: 1px solid var(--sy-line) !important; } +section[class~="bg-[#ec3013]"] *, div[class~="bg-black"][class*="p-8"] *, div[class~="bg-black"][class*="p-6"] * { color: inherit; } +[class*="bg-[#FFD23F]"][class*="px-3"][class*="uppercase"] { background: transparent !important; color: var(--sy-blue) !important; padding-left: 0 !important; box-shadow: none !important; border: 0 !important; } + +[class*="E6FF3F"],[class*="E8FF3C"]{background-color:#eef2ff!important;color:#1e3a8a!important} + +/* Contraste : fond de page plus sombre, contours visibles partout, traits fins */ +html, body { background: #eef1f5 !important; } +main section.bg-white, main section[class*="bg-white"] { background: #eef1f5 !important; } +[class*="border-4"], [class*="border-2"], [class*="border-black"], [class*="border-y-4"], [class*="border-t-4"], [class*="border-b-4"] { border-color: #b8c1cf !important; } +[class*="border-4"] { border-width: 1px !important; } +[class*="border-2"] { border-width: 1px !important; } +[class*="border-y-4"] { border-top-width: 1px !important; border-bottom-width: 1px !important; } +[class*="border-t-4"] { border-top-width: 1px !important; } +[class*="border-b-4"] { border-bottom-width: 1px !important; } +[class*="border-r-4"], [class*="border-l-4"] { border-width: 0 1px 0 0 !important; } +[class*="divide-black"] > * + * { border-color: #b8c1cf !important; } +main article, main figure, main li[class*="border"], main div[class*="border"][class*="bg-"] { box-shadow: 0 1px 2px rgba(17,24,39,.06), 0 2px 8px rgba(17,24,39,.05) !important; } +[class*="bg-white"][class*="border"], figure[style*="border"] { background: #fff !important; } +/* Badges collés aux cartes : dans le flux, alignés en haut */ +main article > span[class*="absolute"], main li > span[class*="absolute"], main div[class*="relative"] > span[class*="absolute"][class*="border"] { position: static !important; display: inline-block !important; margin: 0 0 12px 0 !important; transform: none !important; } + +header a, header summary, nav a, header [class*="border-transparent"], nav [class*="border-transparent"] { border-color: transparent !important; } +header a[aria-current="page"], header details[open] > summary { border-color: #bfd0f5 !important; } +header ul[class*="absolute"] { border: 1px solid #b8c1cf !important; } +main [class*="border-4"], main [class*="border-2"]:not([class*="border-transparent"]) { border-color: #b8c1cf !important; } +main li[class*="min-h-[340px]"] { min-height: 0 !important; } +main li[class*="min-h-[340px]"] > *:first-child + * { margin-top: 12px !important; } +h1 span[class*="bg-"] { background: transparent !important; border: 0 !important; padding: 0 !important; color: #1d4ed8 !important; } +[class*="border-transparent"] { border-color: transparent !important; } +[class*="border-transparent"][aria-current="page"], [class*="border-transparent"]:hover { border-color: transparent !important; background: #e8eefc !important; } +aside a[class*="border-black"][class*="bg-black"] { border-color: transparent !important; } + +/* === SOBRIÉTÉ (inspiration Google Workspace) : l'espace et la typographie structurent, un seul appel à l'action fort === */ +:root { --sy-ink:#0b1220; --sy-text:#3c4043; --sy-hair:#e3e5e8; --sy-soft:#f8f9fa; --sy-blue:#0b57d0; --sy-blue-dark:#0842a0; } +html, body { background:#fff !important; } +main p { color: var(--sy-text); } +main h1, main h2, main h3 { color: var(--sy-ink) !important; text-wrap: balance; } +main h1:not([style*="line-height"]) { font-weight:800 !important; line-height:1.06 !important; } +main p, main li { text-wrap: pretty; } + +/* Fonds : blanc, et un gris très léger pour alterner les sections */ +main section[class*="bg-"], main div[class~="bg-[#F6F5F1]"], main div[class~="bg-[#FFFBEA]"] { background-color:#fff !important; } +main section[class~="bg-[#F6F5F1]"], main section[class~="bg-[#FFFBEA]"] { background-color: var(--sy-soft) !important; } +main section { border-color: transparent !important; box-shadow:none !important; } + +/* Pages vitrine : beaucoup plus d'air */ +main > section > div[class*="py-"] { padding-top: clamp(64px, 9vw, 112px) !important; padding-bottom: clamp(64px, 9vw, 112px) !important; } +main > section > div[class*="max-w-"] > p[class*="text-xl"], main > section > div[class*="max-w-"] p[class*="text-lg"] { line-height:1.65 !important; } + +/* Cartes et encadrés : trait très fin, grands rayons, aucune ombre */ +[class~="border-4"]:not([class~="border-transparent"]), [class~="border-2"]:not([class~="border-transparent"]), [class~="border"] { border-color: var(--sy-hair) !important; } +main article[class*="border"], main li[class~="border-2"], main div[class~="border-2"], main div[class~="border-4"], main figure[class*="border"], main details[class*="border"] { border-radius:16px !important; } +[class*="shadow-["], [class*="shadow-md"], [class*="shadow-lg"] { box-shadow:none !important; } +[class*="rotate-"], [class*="-rotate-"], [class*="md:rotate"], [class*="md:-rotate"], [class*="translate-y-8"] { transform:none !important; } +main [aria-hidden="true"][class*="absolute"] { display:none !important; } + +/* Badges et pastilles : neutres, texte foncé */ +main span[class~="border-2"][class*="text-xs"], main span[class~="border-2"][class*="text-sm"][class*="px-2"], main span[class*="rounded-full"][class*="px-"] { background:#f1f3f4 !important; color:#3c4043 !important; border-color:transparent !important; border-radius:999px !important; } +[class~="bg-[#FFD23F]"], [class~="bg-[#7CF2C0]"], [class~="bg-[#FF6FB5]"], [class~="bg-[#C4B5FD]"] { background-color:#f1f3f4 !important; color:#202124 !important; } +main div[class*="bg-[#FFD23F]"][class*="p-"], main div[class*="bg-[#7CF2C0]"][class*="p-"], main div[class*="bg-[#C4B5FD]"][class*="p-"], main div[class*="bg-[#FF6FB5]"][class*="p-"] { background: var(--sy-soft) !important; } + +/* Boutons : pilules ; l'action principale en bleu plein, le reste en contour discret */ +a[class*="inline-flex"][class*="h-11"], a[class*="inline-flex"][class*="h-12"], a[class*="inline-flex"][class*="h-10"], button[class*="h-11"], button[class*="h-12"], button[class*="h-10"] { border-radius:999px !important; padding-left:24px !important; padding-right:24px !important; font-weight:700 !important; } +a[class~="bg-[#ec3013]"], button[class~="bg-[#ec3013]"], a[class~="bg-black"], button[class~="bg-black"] { background-color: var(--sy-blue) !important; border-color: var(--sy-blue) !important; color:#fff !important; } +a[class~="bg-[#ec3013]"]:hover, button[class~="bg-[#ec3013]"]:hover, a[class~="bg-black"]:hover, button[class~="bg-black"]:hover { background-color: var(--sy-blue-dark) !important; } +main a[class~="bg-white"][class*="inline-flex"], main button[class~="bg-white"][class*="h-"] { background:#fff !important; color: var(--sy-blue) !important; border:1px solid #dadce0 !important; } +main a[class~="bg-white"][class*="inline-flex"]:hover, main button[class~="bg-white"][class*="h-"]:hover { background:#f6f9fe !important; } + +/* En-tête : conservé, épuré */ +body > header { background:#fff !important; box-shadow:none !important; border-bottom:1px solid var(--sy-hair) !important; } +body > header nav a, body > header nav summary { font-weight:600 !important; color:#3c4043 !important; text-decoration:none !important; } +body > header nav a:hover, body > header nav summary:hover { color: var(--sy-ink) !important; background:transparent !important; } +body > header nav ul a[aria-current="page"] { color: var(--sy-blue) !important; background:transparent !important; } +body > header a[href*="inscription"] { background: var(--sy-blue) !important; color:#fff !important; border:0 !important; border-radius:999px !important; padding:0 22px !important; } +body > header a[href*="inscription"] > span[aria-hidden="true"] { display:none !important; } +body > header a[href*="inscription"]:hover { background: var(--sy-blue-dark) !important; color:#fff !important; } +body > header a[href*="connexion"] { border:0 !important; background:transparent !important; } + +[class*="text-[#FFD23F]"], [class*="text-[#7CF2C0]"], [class*="text-[#FF6FB5]"], [class*="text-[#C4B5FD]"], [class*="text-[#E6FF3F]"], [class*="text-[#E8FF3C]"] { color: var(--sy-blue) !important; } +main li[class*="min-h-[340px]"] { min-height:0 !important; aspect-ratio:auto !important; } +main li[class*="min-h-[340px]"] > div[class*="justify-between"] { justify-content:flex-start !important; gap:16px !important; padding-bottom:0 !important; } +main li[class*="min-h-[340px]"] > div > span[class*="self-end"] { align-self:flex-start !important; font-size:40px !important; color: var(--sy-blue) !important; } +body > header a[href*="connexion"] { border:0 !important; background:transparent !important; box-shadow:none !important; padding-left:8px !important; padding-right:8px !important; } + +main h1:not([style*="letter-spacing"]), main h2:not([style*="letter-spacing"]) { letter-spacing:-0.02em !important; } + +header a[style*="background:#0b57d0"], header a[style*="background: #0b57d0"], nav a[style*="background:#0b57d0"] { color: #fff !important; } +header a[style*="background:#0b57d0"]:hover, nav a[style*="background:#0b57d0"]:hover { color: #fff !important; background: #0842a0 !important; } diff --git a/portail/public/favicon.ico b/portail/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/portail/public/images/adam-belkacemi.jpeg b/portail/public/images/adam-belkacemi.jpeg new file mode 100644 index 0000000..558e31b Binary files /dev/null and b/portail/public/images/adam-belkacemi.jpeg differ diff --git a/portail/public/images/couleurs/cockpit.jpg b/portail/public/images/couleurs/cockpit.jpg new file mode 100644 index 0000000..04fa11f Binary files /dev/null and b/portail/public/images/couleurs/cockpit.jpg differ diff --git a/portail/public/images/couleurs/gilet.jpg b/portail/public/images/couleurs/gilet.jpg new file mode 100644 index 0000000..991325c Binary files /dev/null and b/portail/public/images/couleurs/gilet.jpg differ diff --git a/portail/public/images/couleurs/moniteur.webp b/portail/public/images/couleurs/moniteur.webp new file mode 100644 index 0000000..b2bf892 Binary files /dev/null and b/portail/public/images/couleurs/moniteur.webp differ diff --git a/portail/public/images/couleurs/paillasse.jpg b/portail/public/images/couleurs/paillasse.jpg new file mode 100644 index 0000000..04a2724 Binary files /dev/null and b/portail/public/images/couleurs/paillasse.jpg differ diff --git a/portail/public/images/den-bosch.jpg b/portail/public/images/den-bosch.jpg new file mode 100644 index 0000000..0a01803 Binary files /dev/null and b/portail/public/images/den-bosch.jpg differ diff --git a/portail/public/images/logo-paillasse-icone.png b/portail/public/images/logo-paillasse-icone.png new file mode 100644 index 0000000..30f06f9 Binary files /dev/null and b/portail/public/images/logo-paillasse-icone.png differ diff --git a/portail/public/images/logo-paillasse.png b/portail/public/images/logo-paillasse.png new file mode 100644 index 0000000..1cfa372 Binary files /dev/null and b/portail/public/images/logo-paillasse.png differ diff --git a/portail/public/images/logo-syllabus-icone.png b/portail/public/images/logo-syllabus-icone.png new file mode 100644 index 0000000..a1deeca Binary files /dev/null and b/portail/public/images/logo-syllabus-icone.png differ diff --git a/portail/public/images/logo-syllabus.png b/portail/public/images/logo-syllabus.png new file mode 100644 index 0000000..dead7b1 Binary files /dev/null and b/portail/public/images/logo-syllabus.png differ diff --git a/portail/public/images/parcours/coursier.png b/portail/public/images/parcours/coursier.png new file mode 100644 index 0000000..1779550 Binary files /dev/null and b/portail/public/images/parcours/coursier.png differ diff --git a/portail/public/images/telecom-saint-etienne.jpeg b/portail/public/images/telecom-saint-etienne.jpeg new file mode 100644 index 0000000..a64c551 Binary files /dev/null and b/portail/public/images/telecom-saint-etienne.jpeg differ diff --git a/portail/public/index.php b/portail/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/portail/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/portail/public/js/contenus-1.js b/portail/public/js/contenus-1.js new file mode 100644 index 0000000..9292ac5 --- /dev/null +++ b/portail/public/js/contenus-1.js @@ -0,0 +1,95 @@ +// Cours complets · lot 1 : représentation des données, terminal, droits Unix +window.CONTENUS_PRO = (window.CONTENUS_PRO || []).concat([ +{ id: 'binaire', match: /binaire|hexad|représentation des (données|entiers)/i, + titre: 'Représentation des entiers : binaire et hexadécimal', duree: '4 h (CM 1 h 30 · TD 1 h 30 · TP 1 h)', niveau: 'NSI Première · BTS SIO / CIEL 1re année · BUT R&T / Info S1', + prerequis: ['Puissances de 10 et de 2', 'Division euclidienne'], + competences: ['NSI 1re · Représentation des données : types et valeurs de base', 'BTS SIO · Bloc 1 · Exploiter les éléments d’un réseau (adressage)', 'BUT R&T · Compétence « Administrer » · niveau 1'], + intro: 'Tout ce qu’un ordinateur stocke, du texte aux images en passant par les adresses IP, finit en suites de 0 et de 1. Savoir passer d’une base à l’autre est un geste de base de l’informaticien : il sert à calculer un masque de sous-réseau, lire une adresse MAC, choisir une couleur web ou comprendre un débordement d’entier.', + cm: [ + { t: 'Les systèmes de numération', d: '15 min', html: 'Un système de numération positionnel en base b utilise b chiffres (de 0 à b − 1). La valeur d’un chiffre dépend de sa position : le chiffre de rang k vaut chiffre × bk, les rangs étant comptés à partir de 0, depuis la droite.

En base 10 : 2 047 = 2×10³ + 0×10² + 4×10¹ + 7×10⁰. En base 2, seuls 0 et 1 existent. En base 16, on utilise 0-9 puis A (10), B (11), C (12), D (13), E (14), F (15).', table: [['Base', 'Nom', 'Chiffres', 'Notation'], ['2', 'binaire', '0 1', '0b1011 ou 1011₂'], ['10', 'décimal', '0 à 9', '11'], ['16', 'hexadécimal', '0 à 9, A à F', '0xB ou B₁₆']] }, + { t: 'Bits, octets et ordres de grandeur', d: '10 min', html: 'Un bit (binary digit) prend la valeur 0 ou 1. Avec n bits on code 2n valeurs, de 0 à 2n − 1. Un octet vaut 8 bits, donc 256 valeurs (0 à 255). Les préfixes normalisés sont décimaux (1 ko = 1 000 octets) ; les préfixes binaires s’écrivent Kio, Mio, Gio (1 Kio = 1 024 octets).', attention: 'Un disque de « 1 To » affiche environ 931 Gio dans le système : ce n’est pas une arnaque, c’est la différence entre 10¹² et 2⁴⁰.' }, + { t: 'Du binaire au décimal', d: '15 min', html: 'On additionne les poids des bits à 1. Les poids d’un octet sont 128, 64, 32, 16, 8, 4, 2, 1.', exemple: '1011 0010₂ = 128 + 32 + 16 + 2 = 178\n0100 0001₂ = 64 + 1 = 65 (code ASCII de « A »)' }, + { t: 'Du décimal au binaire', d: '20 min', html: 'Méthode 1, soustractions : on retire la plus grande puissance de 2 possible, on écrit 1 à sa place, 0 sinon, et on recommence avec le reste.
Méthode 2, divisions successives : on divise par 2 jusqu’à obtenir 0 ; les restes lus de bas en haut donnent le nombre binaire.', exemple: '45 ÷ 2 = 22 reste 1\n22 ÷ 2 = 11 reste 0\n11 ÷ 2 = 5 reste 1\n 5 ÷ 2 = 2 reste 1\n 2 ÷ 2 = 1 reste 0\n 1 ÷ 2 = 0 reste 1 → lecture de bas en haut : 101101₂', attention: 'On lit les restes de bas en haut. L’erreur la plus fréquente en devoir est de les lire dans l’ordre du calcul.' }, + { t: 'L’hexadécimal', d: '15 min', html: 'Comme 16 = 2⁴, un chiffre hexadécimal correspond exactement à un groupe de 4 bits (un quartet). Pour convertir, on découpe le binaire en groupes de 4 à partir de la droite.', table: [['Binaire', '0000', '0101', '1001', '1010', '1100', '1111'], ['Hexa', '0', '5', '9', 'A', 'C', 'F']], exemple: '1011 0010₂ → B 2 → 0xB2\n#FF6FB5 → rouge FF = 255, vert 6F = 111, bleu B5 = 181' }, + { t: 'Où les retrouve-t-on ?', d: '15 min', html: 'Adresses IPv4 : 4 octets écrits en décimal pointé (192.168.10.65). Masques : /26 = 26 bits à 1 = 255.255.255.192. Adresses MAC : 6 octets en hexadécimal (3C:52:82:1A:0F:9B). IPv6 : 8 groupes de 16 bits en hexadécimal. Couleurs web : #RRVVBB. Droits Unix : notation octale, base 8 (chmod 755).' }], + synthese: ['n bits codent 2ⁿ valeurs, de 0 à 2ⁿ − 1', 'Poids d’un octet : 128 64 32 16 8 4 2 1', 'Décimal → binaire : divisions par 2, restes lus de bas en haut', '1 chiffre hexadécimal = 4 bits : on regroupe par 4 depuis la droite'], + qcm: [ + ['Combien de valeurs différentes peut-on coder sur 10 bits ?', ['10', '100', '1 000', '1 024'], 3, '2¹⁰ = 1 024.'], + ['Que vaut 1100 1000₂ en décimal ?', ['200', '192', '208', '136'], 0, '128 + 64 + 8 = 200.'], + ['Comment s’écrit 172 en binaire ?', ['1010 1100', '1011 0100', '1100 1010', '1010 1010'], 0, '128 + 32 + 8 + 4 = 172.'], + ['Que vaut 0x3F en décimal ?', ['48', '63', '53', '36'], 1, '3 × 16 + 15 = 63.'], + ['Quel est le plus grand nombre codable sur 16 bits non signés ?', ['32 767', '65 535', '65 536', '16 383'], 1, '2¹⁶ − 1 = 65 535.'], + ['255.255.255.224 correspond à quel préfixe CIDR ?', ['/24', '/26', '/27', '/28'], 2, '224 = 1110 0000 : 24 + 3 = 27 bits à 1.'], + ['Combien d’octets compte une adresse MAC ?', ['4', '6', '8', '16'], 1, '48 bits = 6 octets.'], + ['Un disque vendu 500 Go contient environ :', ['500 Gio', '465 Gio', '512 Gio', '536 Gio'], 1, '500 × 10⁹ / 2³⁰ ≈ 465.']], + td: [ + { n: 'Échauffement', c: 'Convertir en binaire sur 8 bits : 7, 64, 99, 128, 250.', r: '0000 0111 · 0100 0000 · 0110 0011 · 1000 0000 · 1111 1010' }, + { n: 'Échauffement', c: 'Convertir en décimal : 1010 1010₂, 0x7F, 0xC0, 1111 0000₂.', r: '170 · 127 · 192 · 240' }, + { n: 'Application', c: 'Écrire l’adresse 172.16.5.130 en binaire, octet par octet.', r: '10101100.00010000.00000101.10000010' }, + { n: 'Application', c: 'Écrire le masque /20 en décimal pointé, puis en hexadécimal.', r: '255.255.240.0 · 0xFFFFF000' }, + { n: 'Approfondissement', c: 'Un capteur code une température sur 12 bits. Combien de valeurs distinctes peut-il transmettre ? Si la plage mesurée va de −40 °C à +85 °C, quelle est la résolution ?', r: '2¹² = 4 096 valeurs. Résolution = 125 / 4 095 ≈ 0,031 °C.' }, + { n: 'Approfondissement', c: 'Écrire en Python une fonction en_binaire(n) sans utiliser bin(), par divisions successives.', r: 'def en_binaire(n):\n s = ""\n while n > 0:\n s = str(n % 2) + s\n n //= 2\n return s or "0"' }], + tp: { type: 'Générateur', url: 'apercu-generateur.html', desc: 'Conversions aléatoires à l’infini, corrigées au fil de l’eau.', etapes: ['Série de 10 conversions décimal → binaire', 'Série de 10 conversions binaire → hexadécimal', 'Défi chronométré : 20 conversions en 5 minutes'] }, + plusLoin: [['Complément à deux et entiers signés', ''], ['Norme IEEE 754 des flottants', ''], ['Défis « Bit Manipulation » sur HackerRank', 'https://www.hackerrank.com/domains/algorithms?filters%5Bsubdomains%5D%5B%5D=bit-manipulation']] }, + +{ id: 'terminal', match: /terminal|commandes de base|premiers pas|arborescence|FHS|shell/i, + titre: 'Le terminal Linux : se repérer et manipuler des fichiers', duree: '4 h (CM 1 h · TD 1 h · TP 2 h)', niveau: 'SNT · NSI · BTS SIO / CIEL · BUT R&T · Titre pro TSSR', + prerequis: ['Notion de fichier et de dossier'], + competences: ['BTS SIO · Bloc 1 · Gérer le patrimoine informatique', 'BTS CIEL · C04 · Installer un système', 'TSSR · Exploiter des serveurs Linux'], + intro: 'Les serveurs du monde entier se pilotent en ligne de commande. Le terminal est plus rapide que la souris, scriptable et accessible à distance par SSH. Ce cours pose les gestes que vous utiliserez dans tous les labs suivants.', + cm: [ + { t: 'Shell, terminal et invite', d: '10 min', html: 'Le terminal est la fenêtre ; le shell (bash, zsh) est le programme qui lit vos commandes et les exécute. L’invite eleve@debian:~$ indique l’utilisateur, la machine, le dossier courant (~ = dossier personnel) et le type de compte ($ utilisateur, # administrateur).' }, + { t: 'L’arborescence Linux (FHS)', d: '15 min', html: 'Tout part de la racine /. Il n’y a pas de lettres de lecteur.', table: [['Dossier', 'Contenu'], ['/home', 'dossiers personnels des utilisateurs'], ['/etc', 'fichiers de configuration'], ['/var/log', 'journaux du système'], ['/bin, /usr/bin', 'commandes'], ['/tmp', 'fichiers temporaires, vidés au redémarrage'], ['/root', 'dossier personnel de l’administrateur']] }, + { t: 'Chemins absolus et relatifs', d: '10 min', html: 'Un chemin absolu commence par / et part de la racine : /home/eleve/notes.txt. Un chemin relatif part du dossier courant : projets/web. . désigne le dossier courant, .. le dossier parent.', exemple: 'eleve@debian:~$ pwd\n/home/eleve\neleve@debian:~$ cd projets/web\neleve@debian:~/projets/web$ cd ../..\neleve@debian:~$' }, + { t: 'Lister et se déplacer', d: '10 min', html: 'pwd affiche le dossier courant. ls liste ; ls -l détaille (droits, taille, date) ; ls -a montre les fichiers cachés (qui commencent par un point) ; ls -lh affiche les tailles lisibles. cd change de dossier.' }, + { t: 'Créer, copier, déplacer, supprimer', d: '15 min', html: 'mkdir -p a/b/c crée une arborescence, touch f un fichier vide, cp src dest copie (-r pour un dossier), mv déplace ou renomme, rm supprime (-r pour un dossier).', attention: 'Il n’y a pas de corbeille. rm -rf supprime sans confirmation. Relisez toujours la commande avant Entrée, surtout avec sudo.' }, + { t: 'Aide et productivité', d: '10 min', html: 'man commande ouvre le manuel (/ pour chercher, q pour quitter). Tab complète les noms, ↑ rappelle l’historique, Ctrl+R recherche dans l’historique, Ctrl+C interrompt une commande, history liste les commandes passées.' }], + synthese: ['/ est la racine, ~ le dossier personnel', 'Chemin absolu = commence par /', 'pwd, ls, cd pour se repérer ; mkdir, cp, mv, rm pour agir', 'Pas de corbeille : relire avant rm'], + qcm: [ + ['Où se trouvent les fichiers de configuration du système ?', ['/home', '/etc', '/var', '/bin'], 1, '/etc regroupe la configuration.'], + ['Lequel est un chemin absolu ?', ['projets/web', '../notes', '/var/log/syslog', '~notes'], 2, 'Il commence par /.'], + ['Que fait mkdir -p a/b/c ?', ['Erreur si a n’existe pas', 'Crée toute l’arborescence', 'Crée un fichier', 'Supprime a/b/c'], 1, '-p crée les dossiers parents manquants.'], + ['Que signifie # à la fin de l’invite ?', ['Commentaire', 'Compte administrateur', 'Erreur', 'Dossier racine'], 1, '# indique root.'], + ['Quelle commande copie le dossier site et son contenu ?', ['cp site copie', 'cp -r site copie', 'mv site copie', 'mkdir site copie'], 1, '-r = récursif.'], + ['Quel raccourci recherche dans l’historique ?', ['Ctrl+C', 'Ctrl+R', 'Ctrl+Z', 'Tab'], 1, 'Ctrl+R lance la recherche inversée.'], + ['Où sont les journaux du système ?', ['/var/log', '/etc/log', '/tmp', '/home/log'], 0, '/var/log contient syslog, auth.log…'], + ['Depuis /home/eleve/projets, que désigne ../.. ?', ['/home', '/home/eleve', '/', '/projets'], 0, 'On remonte deux fois.']], + td: [ + { n: 'Échauffement', c: 'Depuis /home/eleve, donner le chemin absolu puis relatif du fichier /home/eleve/projets/web/index.html.', r: 'Absolu : /home/eleve/projets/web/index.html · relatif : projets/web/index.html' }, + { n: 'Application', c: 'Créer en une commande : tp/{reseau,systeme,web}.', r: 'mkdir -p tp/reseau tp/systeme tp/web (ou mkdir -p tp/{reseau,systeme,web})' }, + { n: 'Application', c: 'Copier /etc/hosts dans tp/reseau sous le nom hosts.bak, puis vérifier.', r: 'cp /etc/hosts tp/reseau/hosts.bak && ls -l tp/reseau' }, + { n: 'Approfondissement', c: 'Trouver tous les fichiers .conf de /etc de plus de 10 Ko.', r: 'find /etc -name "*.conf" -size +10k 2>/dev/null' }], + tp: { type: 'Lab VM', url: 'apercu-lab-essai.html', desc: 'Un vrai terminal Debian, étapes vérifiées automatiquement dans la VM.', etapes: ['Se repérer : pwd, ls -la', 'Créer l’arborescence du projet', 'Copier et renommer des fichiers', 'Retrouver un fichier avec find', 'Nettoyer sans rien casser'] }, + plusLoin: [['Le jeu OverTheWire Bandit (niveaux 0 à 10)', 'https://overthewire.org/wargames/bandit/'], ['Linux Journey', 'https://linuxjourney.com/']] }, + +{ id: 'droits', match: /droits|chmod|chown|permissions|utilisateurs et groupes/i, + titre: 'Droits, utilisateurs et groupes sous Linux', duree: '5 h (CM 1 h 30 · TD 1 h 30 · TP 2 h)', niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR', + prerequis: ['Le terminal Linux', 'Binaire : poids des bits'], + competences: ['BTS SIO · Bloc 3 · Sécuriser les équipements et les usages', 'BTS CIEL · C08 · Sécuriser un système', 'BUT R&T · Compétence « Administrer »'], + intro: 'Sur un serveur partagé, chaque fichier appartient à quelqu’un et chacun n’a que les droits dont il a besoin : c’est le principe du moindre privilège. Une erreur de droits, c’est soit un service qui ne démarre pas, soit une fuite de données.', + cm: [ + { t: 'Utilisateurs et groupes', d: '15 min', html: 'Chaque utilisateur a un identifiant (UID) et un groupe principal (GID), déclarés dans /etc/passwd et /etc/group. Les mots de passe hachés sont dans /etc/shadow, lisible par root seulement. id affiche vos identifiants.', exemple: 'root@srv:~# adduser alice\nroot@srv:~# addgroup profs\nroot@srv:~# usermod -aG profs alice\nroot@srv:~# id alice\nuid=1001(alice) gid=1001(alice) groupes=1001(alice),1002(profs)' }, + { t: 'Lire les droits', d: '15 min', html: 'ls -l affiche 10 caractères : le type puis trois triplets pour le propriétaire (u), le groupe (g) et les autres (o).', table: [['Droit', 'Sur un fichier', 'Sur un dossier'], ['r (4)', 'lire le contenu', 'lister les noms'], ['w (2)', 'modifier', 'créer, renommer, supprimer dedans'], ['x (1)', 'exécuter', 'traverser (cd)']], exemple: '-rwxr-x--- 1 alice profs 812 sept. 29 10:14 sauvegarde.sh\n│└┬┘└┬┘└┬┘\n│ u g o' }, + { t: 'chmod : octal et symbolique', d: '20 min', html: 'En octal, on additionne r = 4, w = 2, x = 1 pour chaque triplet : rwx = 7, r-x = 5, r-- = 4. En symbolique, on ajoute (+), retire (−) ou fixe (=) un droit pour u, g, o ou a.', exemple: 'chmod 750 sauvegarde.sh # rwxr-x---\nchmod u+x,g-w rapport.txt\nchmod -R o= /srv/partage # retire tout aux autres, récursivement' }, + { t: 'chown et umask', d: '15 min', html: 'chown alice:profs fichier change propriétaire et groupe (réservé à root). umask fixe les droits retirés à la création : avec umask 022, un fichier est créé en 644 et un dossier en 755 ; avec 027, en 640 et 750.' }, + { t: 'Droits spéciaux', d: '15 min', html: 'SUID (4000) : le programme s’exécute avec les droits de son propriétaire (ex. passwd). SGID (2000) sur un dossier : les fichiers créés héritent du groupe du dossier, idéal pour un partage. Sticky bit (1000) sur un dossier : seul le propriétaire d’un fichier peut le supprimer (ex. /tmp).', attention: 'Un script SUID root mal écrit est une porte d’entrée classique pour une élévation de privilèges. En audit, on les recherche avec find / -perm -4000.' }, + { t: 'Au-delà : les ACL', d: '10 min', html: 'Les ACL permettent de donner des droits à un utilisateur précis sans changer le groupe : setfacl -m u:bob:rw rapport.txt, et getfacl pour les lire.' }], + synthese: ['u g o × r w x, avec r = 4, w = 2, x = 1', 'x sur un dossier = pouvoir y entrer', 'SGID sur un dossier partagé, sticky bit sur /tmp', 'Moindre privilège : ne donner que le nécessaire'], + qcm: [ + ['rwxr-x--- en octal ?', ['750', '755', '740', '770'], 0, '7, 5, 0.'], + ['Quel fichier contient les hachages des mots de passe ?', ['/etc/passwd', '/etc/shadow', '/etc/group', '/etc/login'], 1, 'shadow, lisible par root uniquement.'], + ['Que fait chmod g+w rapport.txt ?', ['Donne l’écriture aux autres', 'Donne l’écriture au groupe', 'Retire l’écriture', 'Change le groupe'], 1, 'g = groupe, + = ajouter.'], + ['Avec umask 027, un nouveau dossier a les droits :', ['755', '750', '770', '700'], 1, '777 − 027 = 750.'], + ['Quel droit permet à un utilisateur de faire cd dans un dossier ?', ['r', 'w', 'x', 's'], 2, 'x sur un dossier = traverser.'], + ['Le sticky bit sur /tmp sert à :', ['Accélérer', 'Empêcher de supprimer les fichiers des autres', 'Chiffrer', 'Hériter du groupe'], 1, 'Chacun ne supprime que ses fichiers.'], + ['Comment ajouter alice au groupe profs sans retirer ses autres groupes ?', ['usermod -G profs alice', 'usermod -aG profs alice', 'chown profs alice', 'addgroup alice'], 1, '-a = append.'], + ['Quelle commande trouve les fichiers SUID ?', ['find / -perm -4000', 'ls -suid', 'chmod -find', 'grep suid /'], 0, '-perm -4000 cherche le bit SUID.']], + td: [ + { n: 'Échauffement', c: 'Traduire en octal : rw-r--r--, rwx------, rwxrwxr-x, r--------.', r: '644 · 700 · 775 · 400' }, + { n: 'Échauffement', c: 'Traduire en symbolique : 640, 755, 600.', r: 'rw-r----- · rwxr-xr-x · rw-------' }, + { n: 'Application', c: 'Créer un dossier /srv/projet partagé par le groupe devs : les membres lisent et écrivent, les fichiers créés restent dans le groupe devs, les autres n’ont aucun accès.', r: 'mkdir /srv/projet\nchown root:devs /srv/projet\nchmod 2770 /srv/projet' }, + { n: 'Application', c: 'Le script sauvegarde.sh refuse de se lancer (« Permission non accordée »). Diagnostiquer et corriger.', r: 'ls -l sauvegarde.sh → pas de x. chmod u+x sauvegarde.sh' }, + { n: 'Approfondissement', c: 'Expliquer pourquoi un fichier en 777 dans un dossier en 700 appartenant à alice reste inaccessible à bob.', r: 'bob ne peut pas traverser le dossier (pas de x pour les autres) : il n’atteint jamais le fichier.' }], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Créer des comptes, un dossier partagé en SGID et tester les accès avec su.', etapes: ['Créer alice, bob et le groupe devs', 'Préparer /srv/projet en 2770', 'Tester l’écriture en tant qu’alice puis bob', 'Poser une ACL pour un stagiaire', 'Audit : lister les fichiers SUID'] }, + plusLoin: [['ANSSI · Recommandations de sécurité relatives à un système GNU/Linux', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-un-systeme-gnulinux']] } +]); diff --git a/portail/public/js/contenus-2.js b/portail/public/js/contenus-2.js new file mode 100644 index 0000000..4f3b2cb --- /dev/null +++ b/portail/public/js/contenus-2.js @@ -0,0 +1,90 @@ +// Cours complets · lot 2 : tubes, réseaux (OSI), SQL +window.CONTENUS_PRO = (window.CONTENUS_PRO || []).concat([ +{ id: 'tubes', match: /redirection|tubes|grep|filtres|sed|awk/i, + titre: 'Redirections, tubes et filtres de texte', duree: '4 h (CM 1 h · TD 1 h 30 · TP 1 h 30)', niveau: 'BTS SIO · BTS CIEL · BUT R&T · TSSR', + prerequis: ['Le terminal Linux'], + competences: ['BTS SIO · Bloc 1 · Exploiter des journaux', 'BUT R&T · Compétence « Administrer » · automatiser'], + intro: 'Un administrateur passe beaucoup de temps à lire des journaux de plusieurs milliers de lignes. Plutôt que de les lire à l’œil, on enchaîne de petites commandes spécialisées. C’est la philosophie Unix : chaque outil fait une chose, et bien.', + cm: [ + { t: 'Les trois flux standard', d: '10 min', html: 'Chaque processus dispose de trois flux : stdin (0) l’entrée, stdout (1) la sortie normale, stderr (2) les erreurs. Les séparer permet de garder les résultats propres et d’isoler les erreurs.' }, + { t: 'Rediriger', d: '15 min', html: 'On envoie un flux vers un fichier ou on lit un fichier comme entrée.', table: [['Opérateur', 'Effet'], ['>', 'stdout vers un fichier (écrase)'], ['>>', 'stdout ajouté à la fin'], ['2>', 'stderr vers un fichier'], ['&> ou > f 2>&1', 'les deux flux dans le même fichier'], ['<', 'le fichier devient stdin'], ['> /dev/null', 'jeter la sortie']], exemple: 'find / -name "*.conf" > resultats.txt 2> /dev/null' }, + { t: 'Le tube', d: '10 min', html: 'Le tube | branche la sortie d’une commande sur l’entrée de la suivante, sans fichier intermédiaire. On construit une chaîne de traitement de gauche à droite.', exemple: 'cat /var/log/auth.log | grep "Failed password" | wc -l' }, + { t: 'La boîte à outils', d: '20 min', html: '', table: [['Commande', 'Rôle', 'Exemple'], ['grep', 'garder les lignes qui correspondent', 'grep -i erreur syslog'], ['cut', 'extraire des colonnes', 'cut -d: -f1,7 /etc/passwd'], ['sort', 'trier', 'sort -n, sort -rn'], ['uniq -c', 'compter les lignes identiques consécutives', 'sort | uniq -c'], ['wc -l', 'compter les lignes', ''], ['head / tail', 'début / fin', 'tail -f pour suivre en direct'], ['awk', 'traiter par colonnes', "awk '{print $1}'"], ['sed', 'remplacer', "sed 's/http/https/g'"]], attention: 'uniq ne regroupe que les lignes consécutives : il faut presque toujours trier avant avec sort.' }, + { t: 'Un cas réel : repérer une attaque SSH', d: '15 min', html: 'Objectif : trouver les adresses IP qui ont le plus échoué à se connecter.', exemple: "grep 'Failed password' /var/log/auth.log \\\n | awk '{print $(NF-3)}' \\\n | sort | uniq -c | sort -rn | head -5\n\n 412 203.0.113.42\n 87 198.51.100.7\n 12 192.0.2.19" }], + synthese: ['> écrase, >> ajoute, 2> capture les erreurs', '| enchaîne les traitements', 'sort | uniq -c | sort -rn : le classement le plus utile de l’administrateur', 'tail -f pour suivre un journal en direct'], + qcm: [ + ['Où va l’erreur de ls /inexistant > sortie.txt ?', ['Dans sortie.txt', 'À l’écran', 'Nulle part', 'Dans /dev/null'], 1, '> ne redirige que stdout ; stderr s’affiche.'], + ['Comment jeter à la fois la sortie et les erreurs ?', ['> /dev/null', '2> /dev/null', '&> /dev/null', '| null'], 2, '&> redirige les deux flux.'], + ['Que fait cut -d: -f1 /etc/passwd ?', ['Supprime la ligne 1', 'Affiche la 1re colonne séparée par :', 'Compte les lignes', 'Trie'], 1, '-d = séparateur, -f = champ.'], + ['Pourquoi trier avant uniq -c ?', ['Pour aller plus vite', 'uniq ne regroupe que les lignes consécutives', 'Ce n’est pas utile', 'Pour supprimer les doublons'], 1, 'Sans tri, les doublons dispersés ne sont pas regroupés.'], + ['Quelle commande suit un journal en temps réel ?', ['head -f', 'tail -f', 'cat -f', 'less -r'], 1, 'tail -f affiche les nouvelles lignes au fil de l’eau.'], + ['Que fait sed "s/a/b/g" f.txt ?', ['Supprime a', 'Remplace tous les a par b à l’affichage', 'Modifie le fichier sur disque', 'Compte les a'], 1, 'Sans -i, sed ne modifie pas le fichier.']], + td: [ + { n: 'Échauffement', c: 'Enregistrer la liste de /etc dans liste.txt, puis y ajouter celle de /var.', r: 'ls /etc > liste.txt && ls /var >> liste.txt' }, + { n: 'Application', c: 'Afficher les comptes qui utilisent /bin/bash, avec seulement leur nom.', r: 'grep "/bin/bash$" /etc/passwd | cut -d: -f1' }, + { n: 'Application', c: 'Dans acces.log (format Apache), classer les 10 pages les plus demandées.', r: "awk '{print $7}' acces.log | sort | uniq -c | sort -rn | head -10" }, + { n: 'Approfondissement', c: 'Compter les erreurs 404 par heure dans acces.log.', r: "awk '$9 == 404 {print substr($4, 14, 2)}' acces.log | sort | uniq -c" }], + tp: { type: 'IDE Bash', url: 'apercu-ide.html?ex=bash-tp', desc: 'Scripts Bash sur de vrais journaux, vérifiés par des tests.', etapes: ['Compter les connexions échouées', 'Classer les IP suspectes', 'Écrire un rapport dans un fichier', 'Bonus : alerte si plus de 100 échecs'] }, + plusLoin: [['Le jeu OverTheWire Bandit (niveaux 10 à 20)', 'https://overthewire.org/wargames/bandit/'], ['Défis « Linux Shell » sur HackerRank', 'https://www.hackerrank.com/domains/shell']] }, + +{ id: 'osi', match: /OSI|TCP\/IP|modèle en couches|encapsulation|Internet : IP/i, + titre: 'Modèles OSI et TCP/IP, encapsulation', duree: '4 h (CM 1 h 30 · TD 1 h · TP 1 h 30)', niveau: 'SNT · BTS SIO · BTS CIEL · BUT R&T S1', + prerequis: ['Binaire et hexadécimal'], + competences: ['BTS SIO · Bloc 1 · Exploiter les éléments d’un réseau', 'BTS CIEL · C03 · Analyser un réseau', 'BUT R&T · Compétence « Connecter »'], + intro: 'Quand un réseau tombe en panne, l’administrateur raisonne couche par couche : le câble est-il branché ? le switch voit-il la carte ? l’adresse IP est-elle bonne ? le service écoute-t-il ? Le modèle en couches est d’abord une méthode de diagnostic.', + cm: [ + { t: 'Pourquoi un modèle en couches', d: '10 min', html: 'Communiquer suppose de régler beaucoup de problèmes : transmettre des bits, les adresser, les acheminer, les remettre en ordre, les présenter à l’application. Chaque couche règle un problème et offre un service à la couche du dessus, sans connaître le détail des autres.' }, + { t: 'Les 7 couches OSI', d: '20 min', html: '', table: [['N°', 'Couche', 'Rôle', 'Unité', 'Exemples'], ['7', 'Application', 'service rendu à l’utilisateur', 'données', 'HTTP, DNS, SSH, SMTP'], ['6', 'Présentation', 'format, chiffrement', 'données', 'TLS, UTF-8'], ['5', 'Session', 'dialogue', 'données', 'RPC'], ['4', 'Transport', 'bout en bout, ports', 'segment', 'TCP, UDP'], ['3', 'Réseau', 'adressage logique, routage', 'paquet', 'IP, ICMP'], ['2', 'Liaison', 'accès au support, MAC', 'trame', 'Ethernet, Wi-Fi'], ['1', 'Physique', 'signal', 'bit', 'RJ45, fibre, radio']] }, + { t: 'Le modèle TCP/IP', d: '10 min', html: 'Le modèle réellement implémenté compte 4 couches : Accès réseau (OSI 1-2), Internet (3), Transport (4), Application (5-7).' }, + { t: 'L’encapsulation', d: '20 min', html: 'À l’émission, chaque couche ajoute son en-tête aux données reçues de la couche supérieure. À la réception, chaque couche lit et retire le sien.', exemple: '[ HTTP : GET /index.html ]\n[ TCP : port src 51234 → dst 443 ][ HTTP … ]\n[ IP : 192.168.10.65 → 93.184.216.34 ][ TCP … ][ HTTP … ]\n[ Eth : MAC src → MAC passerelle ][ IP … ][ TCP … ][ HTTP … ][ FCS ]' }, + { t: 'Les équipements par couche', d: '10 min', html: 'Hub : couche 1, répète le signal. Switch : couche 2, commute selon les adresses MAC. Routeur : couche 3, achemine selon les adresses IP. Pare-feu : couches 3-4, filtre IP et ports (voire 7 pour un pare-feu applicatif).' }, + { t: 'Diagnostiquer couche par couche', d: '15 min', html: 'On part du bas : ip link (lien actif ?), ip addr (adresse ?), ping la passerelle (couche 3), ss -tlnp (le service écoute-t-il ?), curl (l’application répond-elle ?).', attention: 'Un ping qui échoue ne prouve pas que la machine est éteinte : ICMP est souvent filtré par les pare-feux.' }], + synthese: ['OSI 7 couches, TCP/IP 4 couches', 'bit → trame → paquet → segment → données', 'MAC couche 2, IP couche 3, port couche 4', 'On diagnostique toujours de bas en haut'], + qcm: [ + ['À quelle couche travaille un switch classique ?', ['1', '2', '3', '4'], 1, 'Il commute selon les adresses MAC.'], + ['Quelle est l’unité de la couche Transport ?', ['Trame', 'Paquet', 'Segment', 'Bit'], 2, 'TCP et UDP manipulent des segments (datagrammes pour UDP).'], + ['Le port 443 est une information de couche :', ['2', '3', '4', '7'], 2, 'Les ports sont gérés par la couche Transport.'], + ['Quel protocole n’est PAS de couche application ?', ['DNS', 'SSH', 'ICMP', 'SMTP'], 2, 'ICMP accompagne IP, couche 3.'], + ['Dans une trame Ethernet, l’adresse MAC de destination est celle :', ['du serveur web distant', 'de la passerelle ou du voisin direct', 'du DNS', 'du switch'], 1, 'La MAC change à chaque saut, l’IP de destination reste la même.'], + ['Combien de couches compte le modèle TCP/IP ?', ['3', '4', '5', '7'], 1, 'Accès réseau, Internet, Transport, Application.'], + ['Un poste a une IP mais n’accède pas à Internet. Première vérification ?', ['Réinstaller', 'ping la passerelle', 'Changer le câble', 'Vider le cache DNS'], 1, 'On teste la couche 3 vers la passerelle avant d’aller plus haut.']], + td: [ + { n: 'Échauffement', c: 'Classer par couche OSI : HTTP, IP, switch, TCP, câble RJ45, routeur, TLS, Wi-Fi.', r: '7 · 3 · 2 · 4 · 1 · 3 · 6 · 1-2' }, + { n: 'Application', c: 'Dessiner l’encapsulation d’une requête DNS du poste 192.168.1.20 vers 1.1.1.1.', r: 'DNS (requête) → UDP (port src aléatoire → 53) → IP (192.168.1.20 → 1.1.1.1) → Ethernet (MAC poste → MAC box)' }, + { n: 'Application', c: 'Un utilisateur ne peut pas ouvrir intranet.lycee.fr. Proposer une démarche de diagnostic en 5 commandes, de bas en haut.', r: 'ip link · ip addr · ping passerelle · nslookup intranet.lycee.fr · curl -I https://intranet.lycee.fr' }, + { n: 'Approfondissement', c: 'Expliquer pourquoi l’adresse MAC de destination change à chaque routeur traversé alors que l’adresse IP de destination reste la même.', r: 'La couche 2 ne sert qu’à atteindre le voisin direct ; chaque routeur ré-encapsule dans une nouvelle trame. La couche 3 porte l’adresse finale.' }], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Capturer du trafic avec tcpdump et reconnaître chaque en-tête.', etapes: ['Lancer tcpdump sur eth0', 'Faire un curl vers un site', 'Identifier Ethernet, IP, TCP, HTTP', 'Comparer avec une requête DNS en UDP'] }, + plusLoin: [['Cisco Networking Academy · cours gratuits', 'https://www.netacad.com/'], ['RFC 1122 (Requirements for Internet Hosts)', 'https://www.rfc-editor.org/rfc/rfc1122']] }, + +{ id: 'sql', match: /SQL|SELECT|requête|bases de données relationnelles|jointure/i, + titre: 'SQL : interroger une base de données relationnelle', duree: '6 h (CM 2 h · TD 2 h · TP 2 h)', niveau: 'NSI Terminale · BTS SIO SLAM · BUT Informatique · MIAGE', + prerequis: ['Notion de tableau de données'], + competences: ['NSI Tle · Bases de données : langage SQL', 'BTS SIO SLAM · Bloc 2 · Concevoir et exploiter une base de données', 'BUT Info · Compétence « Gérer des données »'], + intro: 'Presque toutes les applications stockent leurs données dans une base relationnelle : un ENT, un site marchand, Syllabus elle-même. SQL est le langage standard pour les interroger. Il se lit presque comme une phrase.', + cm: [ + { t: 'Le modèle relationnel', d: '20 min', html: 'Une base est un ensemble de tables (relations). Chaque ligne est un enregistrement, chaque colonne un attribut typé. La clé primaire identifie une ligne de façon unique ; une clé étrangère référence la clé primaire d’une autre table.', table: [['eleves', 'id (PK)', 'nom', 'prenom', 'classe_id (FK)'], ['', '1', 'Martin', 'Léa', '3'], ['', '2', 'Kaci', 'Inès', '3']] }, + { t: 'Sélectionner : SELECT … FROM', d: '15 min', html: 'On choisit les colonnes, puis la table. * sélectionne toutes les colonnes, DISTINCT supprime les doublons, AS renomme.', exemple: 'SELECT nom, prenom FROM eleves;\nSELECT DISTINCT ville FROM eleves;\nSELECT nom AS "Nom de famille" FROM eleves;' }, + { t: 'Filtrer et trier', d: '20 min', html: 'WHERE filtre avec =, <>, <, >, BETWEEN, IN, LIKE (% = n’importe quelle suite), IS NULL, combinés par AND, OR, NOT. ORDER BY trie (ASC, DESC), LIMIT restreint.', exemple: "SELECT nom, moyenne FROM eleves\nWHERE classe_id = 3 AND moyenne >= 10\nORDER BY moyenne DESC\nLIMIT 5;" }, + { t: 'Agréger : COUNT, AVG, GROUP BY', d: '20 min', html: 'Les fonctions d’agrégat calculent sur un ensemble de lignes : COUNT, SUM, AVG, MIN, MAX. GROUP BY forme des groupes, HAVING filtre les groupes (WHERE filtre les lignes avant).', exemple: 'SELECT classe_id, COUNT(*) AS effectif, AVG(moyenne) AS moy\nFROM eleves\nGROUP BY classe_id\nHAVING COUNT(*) > 20;', attention: 'Toute colonne du SELECT qui n’est pas agrégée doit figurer dans le GROUP BY.' }, + { t: 'Joindre des tables', d: '25 min', html: 'La jointure relie deux tables par leurs clés. INNER JOIN garde les lignes qui ont une correspondance des deux côtés ; LEFT JOIN garde toutes les lignes de gauche, avec NULL s’il n’y a pas de correspondance.', exemple: 'SELECT e.nom, c.libelle\nFROM eleves e\nJOIN classes c ON e.classe_id = c.id;\n\n-- Classes sans aucun élève :\nSELECT c.libelle FROM classes c\nLEFT JOIN eleves e ON e.classe_id = c.id\nWHERE e.id IS NULL;' }, + { t: 'Modifier les données', d: '15 min', html: 'INSERT INTO ajoute, UPDATE … SET … WHERE modifie, DELETE FROM … WHERE supprime.', attention: 'Un UPDATE ou un DELETE sans WHERE touche toutes les lignes. On teste toujours d’abord le WHERE avec un SELECT.' }, + { t: 'Sécurité : l’injection SQL', d: '10 min', html: 'Construire une requête en collant du texte saisi par l’utilisateur permet de détourner la requête (\' OR 1=1 --). On utilise toujours des requêtes préparées avec paramètres.' }], + synthese: ['SELECT colonnes FROM table WHERE condition ORDER BY tri', 'WHERE filtre les lignes, HAVING filtre les groupes', 'JOIN … ON relie clé étrangère et clé primaire', 'Jamais de DELETE ni d’UPDATE sans WHERE', 'Requêtes préparées contre l’injection'], + qcm: [ + ['Quelle clause filtre les groupes après un GROUP BY ?', ['WHERE', 'HAVING', 'ORDER BY', 'LIMIT'], 1, 'HAVING s’applique aux groupes.'], + ['Que renvoie SELECT COUNT(*) FROM eleves WHERE moyenne IS NULL ?', ['Les élèves sans moyenne', 'Le nombre d’élèves sans moyenne', 'Une erreur', '0 toujours'], 1, 'COUNT compte les lignes retenues.'], + ['Quelle jointure garde les classes sans élève ?', ['INNER JOIN', 'LEFT JOIN depuis classes', 'CROSS JOIN', 'Aucune'], 1, 'LEFT JOIN conserve toutes les lignes de la table de gauche.'], + ['LIKE "Ma%" correspond à :', ['Ma exactement', 'tout texte commençant par Ma', 'tout texte finissant par Ma', 'tout texte contenant %'], 1, '% remplace n’importe quelle suite de caractères.'], + ['Que se passe-t-il avec DELETE FROM eleves; ?', ['Rien', 'Toutes les lignes sont supprimées', 'La table est supprimée', 'Erreur de syntaxe'], 1, 'Sans WHERE, toutes les lignes partent. La table reste.'], + ['Quel est le rôle d’une clé étrangère ?', ['Chiffrer', 'Référencer une ligne d’une autre table', 'Accélérer les tris', 'Rendre une colonne unique'], 1, 'Elle garantit l’intégrité référentielle.'], + ['Comment se protéger de l’injection SQL ?', ['Masquer les erreurs', 'Requêtes préparées', 'Mettre des guillemets', 'Utiliser HTTPS'], 1, 'Les paramètres ne sont jamais interprétés comme du SQL.'], + ['Classer les élèves du meilleur au moins bon :', ['ORDER BY moyenne', 'ORDER BY moyenne DESC', 'SORT BY moyenne', 'GROUP BY moyenne'], 1, 'DESC = décroissant.']], + td: [ + { n: 'Échauffement', c: 'Afficher nom et prénom des élèves de la classe 3, triés par nom.', r: 'SELECT nom, prenom FROM eleves WHERE classe_id = 3 ORDER BY nom;' }, + { n: 'Application', c: 'Afficher l’effectif et la moyenne de chaque classe, avec son libellé.', r: 'SELECT c.libelle, COUNT(e.id), AVG(e.moyenne)\nFROM classes c LEFT JOIN eleves e ON e.classe_id = c.id\nGROUP BY c.libelle;' }, + { n: 'Application', c: 'Lister les élèves qui n’ont rendu aucun devoir (tables eleves, rendus).', r: 'SELECT e.nom FROM eleves e\nLEFT JOIN rendus r ON r.eleve_id = e.id\nWHERE r.id IS NULL;' }, + { n: 'Approfondissement', c: 'Afficher les classes dont la moyenne est supérieure à la moyenne générale.', r: 'SELECT classe_id, AVG(moyenne) FROM eleves GROUP BY classe_id\nHAVING AVG(moyenne) > (SELECT AVG(moyenne) FROM eleves);' }, + { n: 'Approfondissement', c: 'Réécrire en requête préparée (PHP PDO) : "SELECT * FROM users WHERE login = \'" . $login . "\'"', r: '$st = $pdo->prepare("SELECT * FROM users WHERE login = ?");\n$st->execute([$login]);' }], + tp: { type: 'IDE SQL', url: 'apercu-ide.html?ex=sql-select', desc: 'Une base SQLite dans le navigateur, requêtes vérifiées automatiquement.', etapes: ['Explorer les tables', '8 requêtes SELECT progressives', '3 jointures', '2 requêtes d’agrégat', 'Défi : le palmarès par classe'] }, + plusLoin: [['SQLBolt · exercices interactifs', 'https://sqlbolt.com/'], ['Défis SQL sur HackerRank', 'https://www.hackerrank.com/domains/sql'], ['DataCamp · Introduction to SQL', 'https://www.datacamp.com/courses/introduction-to-sql']] } +]); diff --git a/portail/public/js/contenus-3.js b/portail/public/js/contenus-3.js new file mode 100644 index 0000000..6bbd620 --- /dev/null +++ b/portail/public/js/contenus-3.js @@ -0,0 +1,92 @@ +// Cours complets · lot 3 : Python, Web, cryptographie +window.CONTENUS_PRO = (window.CONTENUS_PRO || []).concat([ +{ id: 'python', match: /Python|programmation|boucle|variable|algorithm/i, + titre: 'Python : variables, conditions, boucles et fonctions', duree: '6 h (CM 2 h · TD 2 h · TP 2 h)', niveau: 'SNT · NSI Première · BTS SIO SLAM · BUT S1', + prerequis: ['Aucun'], + competences: ['NSI 1re · Langages et programmation', 'BTS SIO SLAM · Bloc 2 · Développer une solution applicative', 'BUT Info · Compétence « Réaliser »'], + intro: 'Python est le langage d’enseignement du lycée à l’université, mais aussi celui des administrateurs, des data engineers et de l’IA. Sa syntaxe courte permet de se concentrer sur l’essentiel : décomposer un problème en instructions.', + cm: [ + { t: 'Variables et types', d: '20 min', html: 'Une variable associe un nom à une valeur. Le type est déduit de la valeur.', table: [['Type', 'Exemple', 'Usage'], ['int', '17', 'entiers'], ['float', '3.5', 'nombres à virgule'], ['str', '"Léa"', 'texte'], ['bool', 'True', 'vrai / faux'], ['list', '[12, 15, 9]', 'suite modifiable'], ['dict', '{"nom": "Léa"}', 'associations clé → valeur']], exemple: 'age = 17\nnom = "Léa"\nprint(f"{nom} a {age} ans") # Léa a 17 ans\nprint(type(age)) # ' }, + { t: 'Opérateurs et entrées', d: '15 min', html: '+ − * / (division réelle), // (division entière), % (reste), ** (puissance). input() lit une saisie, toujours sous forme de texte : on la convertit avec int() ou float().', attention: 'input() renvoie une chaîne : "3" + "4" donne "34", pas 7.' }, + { t: 'Conditions', d: '20 min', html: 'Le bloc exécuté dépend d’une condition. Python délimite les blocs par l’indentation (4 espaces).', exemple: 'note = float(input("Note : "))\nif note >= 16:\n print("Très bien")\nelif note >= 10:\n print("Admis")\nelse:\n print("À retravailler")' }, + { t: 'Boucle for', d: '20 min', html: 'for parcourt une séquence. range(n) produit 0, 1, …, n − 1 ; range(a, b, pas) va de a à b − 1.', exemple: 'notes = [12, 15, 9, 18]\ntotal = 0\nfor n in notes:\n total += n\nprint(total / len(notes)) # 13.5' }, + { t: 'Boucle while', d: '15 min', html: 'while répète tant que la condition est vraie. On l’utilise quand on ne connaît pas le nombre de tours à l’avance.', exemple: 'essais = 0\nmdp = ""\nwhile mdp != "paillasse" and essais < 3:\n mdp = input("Mot de passe : ")\n essais += 1', attention: 'Si la condition ne devient jamais fausse, la boucle est infinie. Ctrl+C l’interrompt.' }, + { t: 'Fonctions', d: '20 min', html: 'Une fonction regroupe des instructions réutilisables. Elle reçoit des paramètres et renvoie un résultat avec return. On la documente avec une docstring et on la teste avec assert.', exemple: 'def moyenne(valeurs):\n """Renvoie la moyenne d\'une liste non vide."""\n return sum(valeurs) / len(valeurs)\n\nassert moyenne([10, 20]) == 15' }, + { t: 'Listes et dictionnaires', d: '10 min', html: 'Une liste s’indexe à partir de 0 : notes[0], notes[-1] (dernier), notes.append(14). Un dictionnaire associe des clés à des valeurs : eleve["nom"].' }], + synthese: ['L’indentation délimite les blocs', 'input() renvoie du texte : convertir avec int() ou float()', 'for quand on connaît la séquence, while sinon', 'Une fonction = paramètres + return + tests'], + qcm: [ + ['Que vaut 7 // 2 ?', ['3.5', '3', '4', '1'], 1, '// est la division entière.'], + ['Que vaut 7 % 2 ?', ['1', '3', '3.5', '0'], 0, '% donne le reste.'], + ['Combien de tours fait for i in range(2, 10, 3) ?', ['2', '3', '4', '8'], 1, 'i prend 2, 5, 8.'], + ['Que renvoie input() ?', ['Un int', 'Un str', 'Un float', 'Un bool'], 1, 'Toujours une chaîne.'], + ['Que vaut notes[-1] pour notes = [12, 15, 9] ?', ['12', '15', '9', 'Erreur'], 2, '-1 désigne le dernier élément.'], + ['Que fait return dans une fonction ?', ['Affiche une valeur', 'Renvoie une valeur et sort de la fonction', 'Recommence la fonction', 'Rien'], 1, 'return termine la fonction avec un résultat.'], + ['Quelle boucle convient pour « redemander tant que la saisie est invalide » ?', ['for', 'while', 'range', 'if'], 1, 'On ne connaît pas le nombre de tours.'], + ['Que produit "3" + "4" ?', ['7', '"34"', 'Erreur', '"7"'], 1, 'Concaténation de chaînes.']], + td: [ + { n: 'Échauffement', c: 'Écrire un programme qui demande l’année de naissance et affiche l’âge en 2026.', r: 'annee = int(input("Année : "))\nprint(2026 - annee)' }, + { n: 'Application', c: 'Écrire est_pair(n) qui renvoie True si n est pair, puis afficher les nombres pairs de 1 à 30.', r: 'def est_pair(n):\n return n % 2 == 0\n\nfor i in range(1, 31):\n if est_pair(i):\n print(i)' }, + { n: 'Application', c: 'Écrire mention(note) qui renvoie « TB », « B », « AB », « Passable » ou « Insuffisant ».', r: 'def mention(n):\n if n >= 16: return "TB"\n if n >= 14: return "B"\n if n >= 12: return "AB"\n if n >= 10: return "Passable"\n return "Insuffisant"' }, + { n: 'Approfondissement', c: 'Écrire maximum(liste) sans utiliser max(), avec ses tests.', r: 'def maximum(l):\n m = l[0]\n for x in l:\n if x > m:\n m = x\n return m\n\nassert maximum([3, 9, 2]) == 9' }, + { n: 'Approfondissement', c: 'Compter le nombre d’occurrences de chaque mot d’une phrase avec un dictionnaire.', r: 'compte = {}\nfor mot in phrase.split():\n compte[mot] = compte.get(mot, 0) + 1' }], + tp: { type: 'IDE Python', url: 'apercu-ide.html?ex=py-pair', desc: 'Exercices corrigés par des tests automatiques, directement dans le navigateur.', etapes: ['Parité et multiples', 'Somme et moyenne d’une liste', 'Compter les voyelles', 'Jeu du nombre mystère', 'Défi : FizzBuzz'] }, + plusLoin: [['France-IOI · parcours Python', 'https://www.france-ioi.org/'], ['HackerRank · Python', 'https://www.hackerrank.com/domains/python'], ['DataCamp · Introduction to Python', 'https://www.datacamp.com/courses/intro-to-python-for-data-science']] }, + +{ id: 'web', match: /HTML|CSS|Le Web|HTTP|serveur web|Apache/i, + titre: 'Le Web : HTTP, HTML, CSS et serveur web', duree: '5 h (CM 1 h 30 · TD 1 h 30 · TP 2 h)', niveau: 'SNT · NSI Première · BTS SIO · BTS CIEL', + prerequis: ['Modèles OSI et TCP/IP (conseillé)'], + competences: ['SNT · Le Web', 'NSI 1re · Interactions entre l’homme et la machine sur le Web', 'BTS SIO · Bloc 1 · Mettre à disposition un service'], + intro: 'Chaque fois que vous ouvrez une page, votre navigateur échange des messages HTTP avec un serveur, puis affiche du HTML mis en forme par du CSS. Comprendre cet échange, c’est pouvoir créer un site, mais aussi le dépanner et le sécuriser.', + cm: [ + { t: 'URL et architecture client-serveur', d: '15 min', html: 'Le client (navigateur) envoie une requête, le serveur (Apache, Nginx) répond. L’URL précise tout : https://www.lycee.fr:443/cours/index.html?id=3 = protocole, nom d’hôte, port, chemin, paramètres.' }, + { t: 'Le protocole HTTP', d: '20 min', html: 'HTTP est un protocole texte. La requête contient une méthode, un chemin et des en-têtes ; la réponse contient un code d’état, des en-têtes et un corps.', exemple: 'GET /index.html HTTP/1.1\nHost: www.lycee.fr\n\nHTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n\n…', table: [['Code', 'Sens'], ['200', 'OK'], ['301 / 302', 'redirection'], ['403', 'accès interdit'], ['404', 'ressource introuvable'], ['500', 'erreur du serveur']] }, + { t: 'HTML : la structure', d: '20 min', html: 'HTML décrit le sens du contenu avec des balises : titres <h1> à <h6>, paragraphes, listes, liens, images, formulaires. Une page bien structurée est lisible par les moteurs de recherche et les lecteurs d’écran.', exemple: '\n\nMon TP\n\n

Mon premier site

\n

Hébergé sur Debian.

\n\n' }, + { t: 'CSS : la présentation', d: '20 min', html: 'Une règle CSS associe un sélecteur à des déclarations. Sélecteurs : balise (p), classe (.carte), identifiant (#menu). La mise en page moderne repose sur display: flex et display: grid.', exemple: '.carte {\n border: 2px solid #000;\n padding: 16px;\n}\n.grille { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }' }, + { t: 'Formulaires : GET et POST', d: '10 min', html: 'Un formulaire envoie des données au serveur. GET les place dans l’URL (recherche, filtres). POST les place dans le corps de la requête (connexion, envoi de fichier).', attention: 'Un mot de passe ne doit jamais passer en GET : il finirait dans l’historique et les journaux du serveur.' }, + { t: 'Héberger : le serveur web', d: '15 min', html: 'Sous Debian, apt install apache2 installe le serveur ; les pages sont dans /var/www/html, la configuration dans /etc/apache2, les journaux dans /var/log/apache2. HTTPS ajoute TLS et un certificat (Let’s Encrypt).' }], + synthese: ['Client → requête HTTP → serveur → réponse avec code', 'HTML = structure, CSS = présentation', 'GET dans l’URL, POST dans le corps', 'Apache : /var/www/html et /var/log/apache2'], + qcm: [ + ['Que signifie le code 403 ?', ['Introuvable', 'Accès interdit', 'Erreur serveur', 'Redirection'], 1, '403 Forbidden.'], + ['Quelle partie de l’URL est ?id=3 ?', ['Le chemin', 'Les paramètres', 'Le port', 'Le protocole'], 1, 'La chaîne de requête.'], + ['Quelle balise pour le titre principal ?', ['', '<h1>', '<head>', '<header>'], 1, '<title> est le titre de l’onglet, <h1> celui de la page.'], + ['Quel sélecteur CSS cible class="carte" ?', ['#carte', '.carte', 'carte', '*carte'], 1, 'Le point cible une classe.'], + ['Pour un formulaire de connexion, on utilise :', ['GET', 'POST', 'PUT', 'HEAD'], 1, 'Les identifiants ne doivent pas apparaître dans l’URL.'], + ['Où Apache sert-il les pages par défaut sous Debian ?', ['/etc/apache2', '/var/www/html', '/home/www', '/srv/http'], 1, 'DocumentRoot par défaut.'], + ['Quel port utilise HTTPS ?', ['80', '443', '8080', '22'], 1, 'HTTP 80, HTTPS 443.']], + td: [ + { n: 'Échauffement', c: 'Décomposer https://ent.lycee.fr:8443/notes?eleve=12 : protocole, hôte, port, chemin, paramètres.', r: 'https · ent.lycee.fr · 8443 · /notes · eleve=12' }, + { n: 'Application', c: 'Écrire une page HTML avec un titre, une liste de 3 TP et un lien vers le lycée.', r: '<h1>Mes TP</h1>\n<ul><li>Terminal</li><li>Droits</li><li>Apache</li></ul>\n<a href="https://…">Mon lycée</a>' }, + { n: 'Application', c: 'Écrire le CSS pour afficher 3 cartes côte à côte, avec une bordure noire.', r: '.grille { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }\n.carte { border: 2px solid #000; padding: 16px; }' }, + { n: 'Approfondissement', c: 'Avec curl, afficher uniquement les en-têtes de réponse de https://example.org et relever le code et le type de contenu.', r: 'curl -I https://example.org → HTTP/2 200, content-type: text/html' }], + tp: { type: 'Lab web', url: 'apercu-lab-web.html', desc: 'Terminal et navigateur reliés : installer Apache, publier sa page, la voir en direct.', etapes: ['apt install apache2', 'Vérifier le service', 'Remplacer index.html', 'Actualiser le navigateur relié', 'Lire le journal d’accès'] }, + plusLoin: [['MDN Web Docs (Mozilla)', 'https://developer.mozilla.org/fr/'], ['freeCodeCamp · Responsive Web Design', 'https://www.freecodecamp.org/']] }, + +{ id: 'crypto', match: /mot de passe|hachage|chiffrement|crypto|cybers|sécurité des accès|SSH par clé/i, + titre: 'Cryptographie appliquée : hachage, chiffrement, clés', duree: '5 h (CM 2 h · TD 1 h 30 · TP 1 h 30)', niveau: 'BTS SIO · BTS CIEL · BUT R&T Cyber · Master cybersécurité', + prerequis: ['Binaire et hexadécimal', 'Le terminal Linux'], + competences: ['BTS SIO · Bloc 3 · Cybersécurité des services informatiques', 'BTS CIEL · C08 · Sécuriser', 'BUT R&T parcours Cyber · Compétence « Protéger »'], + intro: 'Mots de passe stockés, sites en HTTPS, connexions SSH, signatures de mises à jour : la cryptographie protège presque tout ce que nous faisons en ligne. L’objectif n’est pas de devenir cryptographe, mais de savoir choisir et configurer les bons outils.', + cm: [ + { t: 'Les trois objectifs', d: '10 min', html: '<b>Confidentialité</b> : seul le destinataire lit le message (chiffrement). <b>Intégrité</b> : le message n’a pas été modifié (hachage, MAC). <b>Authenticité</b> : on sait qui l’a envoyé (signature, certificat).' }, + { t: 'Les fonctions de hachage', d: '20 min', html: 'Une fonction de hachage produit une empreinte de taille fixe. Elle est à sens unique, et un seul bit changé en entrée change complètement l’empreinte. Usages : vérifier un fichier téléchargé, stocker un mot de passe, signer.', exemple: '$ echo -n "bonjour" | sha256sum\n2cb4b1431b84ec15d35ed83bb927e27e8967d75f4bcd9cc4b25c8d879ae23e18\n$ echo -n "Bonjour" | sha256sum\n9172e8eec99f144f72eca9a568759580edadb2cfd154857f07e657569493bc44', attention: 'MD5 et SHA-1 sont cassés (collisions) : ne plus les utiliser pour la sécurité.' }, + { t: 'Stocker un mot de passe', d: '20 min', html: 'On ne stocke jamais un mot de passe en clair ni chiffré, mais un <b>hachage lent et salé</b>. Le <b>sel</b>, aléatoire et unique par compte, empêche les tables précalculées ; la lenteur (bcrypt, scrypt, Argon2) freine la force brute.', table: [['Méthode', 'Verdict'], ['En clair', 'interdit'], ['MD5 / SHA-1', 'cassé'], ['SHA-256 sans sel', 'insuffisant'], ['bcrypt, Argon2id', 'recommandé']] }, + { t: 'Chiffrement symétrique', d: '15 min', html: 'Une même clé chiffre et déchiffre. <b>AES</b> (128 ou 256 bits) est le standard : très rapide, il chiffre les disques, les fichiers, le trafic une fois la session établie. Le problème est d’échanger la clé de façon sûre.' }, + { t: 'Chiffrement asymétrique', d: '20 min', html: 'Chaque personne possède une paire de clés : la <b>publique</b>, diffusée, et la <b>privée</b>, gardée secrète. On chiffre avec la clé publique du destinataire ; lui seul déchiffre avec sa clé privée. À l’inverse, on <b>signe</b> avec sa clé privée et chacun vérifie avec la clé publique. Algorithmes : RSA, courbes elliptiques (Ed25519).' }, + { t: 'En pratique : HTTPS et SSH', d: '15 min', html: '<b>HTTPS (TLS)</b> : le serveur présente un certificat signé par une autorité ; un échange asymétrique établit une clé de session, puis AES chiffre le trafic. <b>SSH</b> : l’authentification par clé remplace le mot de passe.', exemple: '$ ssh-keygen -t ed25519 -C "eleve@syllabus"\n$ ssh-copy-id eleve@192.168.10.10\n$ ssh eleve@192.168.10.10 # plus de mot de passe demandé' }], + synthese: ['Hachage = sens unique, pour l’intégrité', 'Mots de passe : bcrypt ou Argon2, avec sel', 'Symétrique (AES) rapide, asymétrique (RSA, Ed25519) pour échanger et signer', 'Chiffrer avec la clé publique, signer avec la clé privée'], + qcm: [ + ['Quel objectif assure une fonction de hachage ?', ['Confidentialité', 'Intégrité', 'Disponibilité', 'Anonymat'], 1, 'Elle détecte toute modification.'], + ['Pourquoi ajouter un sel ?', ['Accélérer', 'Rendre chaque hachage unique et empêcher les tables précalculées', 'Chiffrer le mot de passe', 'Compresser'], 1, 'Deux mots de passe identiques n’ont plus la même empreinte.'], + ['Quel algorithme est recommandé pour stocker un mot de passe ?', ['MD5', 'SHA-1', 'Argon2id', 'AES'], 2, 'Lent, salé, conçu pour ça.'], + ['Pour envoyer un message confidentiel à Alice, on chiffre avec :', ['sa clé privée', 'sa clé publique', 'notre clé publique', 'un hachage'], 1, 'Seule sa clé privée le déchiffrera.'], + ['Pour signer un document, on utilise :', ['sa propre clé privée', 'la clé publique du destinataire', 'AES', 'SHA-1'], 0, 'Tout le monde vérifie avec notre clé publique.'], + ['Dans HTTPS, les données de la page sont chiffrées avec :', ['RSA uniquement', 'une clé de session symétrique', 'le certificat', 'un hachage'], 1, 'L’asymétrique sert à établir la clé de session.'], + ['Quelle commande génère une paire de clés SSH moderne ?', ['ssh-keygen -t ed25519', 'ssh-copy-id', 'openssl md5', 'gpg --hash'], 0, 'Ed25519 est recommandé.']], + td: [ + { n: 'Échauffement', c: 'Pour chaque besoin, dire s’il faut hacher, chiffrer ou signer : stocker les mots de passe, envoyer un bulletin confidentiel, prouver qu’une mise à jour vient bien de l’éditeur.', r: 'hacher (lent, salé) · chiffrer · signer' }, + { n: 'Application', c: 'Vérifier l’intégrité d’une image ISO téléchargée à partir de son fichier SHA256SUMS.', r: 'sha256sum -c SHA256SUMS --ignore-missing' }, + { n: 'Application', c: 'Une base de 10 000 comptes a fuité, hachée en MD5 sans sel. Expliquer les risques et la correction.', r: 'Les mots de passe courants sont retrouvés instantanément (tables arc-en-ciel). Forcer la réinitialisation et passer à Argon2id avec sel.' }, + { n: 'Approfondissement', c: 'Configurer un serveur SSH pour refuser les connexions par mot de passe et celles de root.', r: 'Dans /etc/ssh/sshd_config :\nPasswordAuthentication no\nPermitRootLogin no\npuis systemctl restart ssh' }], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Générer une paire de clés, se connecter sans mot de passe, durcir sshd.', etapes: ['Comparer des hachages SHA-256', 'Générer une clé Ed25519', 'Déposer la clé sur le serveur', 'Désactiver l’authentification par mot de passe', 'Vérifier dans /var/log/auth.log'] }, + plusLoin: [['ANSSI · Guide de sélection d’algorithmes cryptographiques', 'https://cyber.gouv.fr/publications/mecanismes-cryptographiques'], ['CNIL · Recommandations sur les mots de passe', 'https://www.cnil.fr/fr/mots-de-passe-une-nouvelle-recommandation-pour-maitriser-sa-securite'], ['Root-Me · challenges cryptanalyse', 'https://www.root-me.org/']] } +]); diff --git a/portail/public/js/contenus-alias.js b/portail/public/js/contenus-alias.js new file mode 100644 index 0000000..8e45a76 --- /dev/null +++ b/portail/public/js/contenus-alias.js @@ -0,0 +1,33 @@ +// Élargit la correspondance de cours complets existants aux chapitres qu’ils couvrent réellement +(function () { +var ext = { + terminal: /Fichiers : cp, mv|Lire et éditer|cat, less|Mode examen « Admin Linux »/i, + archi: /Performance : vmstat|Noyau et modules|lsmod|sysctl/i, + diagnostic0: /\bARP\b|ICMP, ping, traceroute|TCP et UDP : ports|Analyse de trames|Dépannage : réseau cassé|Mode examen « Réseau »/i, + sql: /Modèle relationnel|Agrégats : GROUP BY|DDL : CREATE|DML : INSERT|Vues, procédures|Transactions et ACID|Index, EXPLAIN/i, + tubes: /Expressions régulières/i, + binaire: /Codage des entiers/i, + crypto: /PKI, autorité de certification|X\.509|\bGPG\b/i, + pandas: /Nettoyage et qualité des données|Visualisation et tableaux de bord|Mode examen « Data »/i, + ml: /Sur-apprentissage|Biais, éthique|Sécurité des modèles/i, + neurones: /Traitement du langage et de la voix/i, + iot: /Node-RED|Modbus|CoAP/i, + python: /Mode examen « épreuve pratique NSI »|Types construits|Épreuve pratique chronométrée|Projets?$|Grand oral et projets|SAÉ|Projet technique|Projet de fin d’études|Projet data|Réaliser un développement d’application|Développement web et applications/i, + web2: /Photographie numérique|Histoire de l’informatique|Applications web|Développement : web, API/i, + iot2: /Chaîne d’information et chaîne d’énergie|Localisation et cartographie|GPS, NMEA/i, + ipv42: /Préparer une installation|plan d’adressage|Réseaux et infrastructure du SI/i, + dns2: /Présence en ligne/i, + sauvegarde: /Maintenir et dépanner : diagnostic, sauvegarde/i, + diagnostic2: /Épreuves pratiques|Épreuves blanches|Maintenir et sécuriser l’infrastructure|Administrer et sécuriser les infrastructures/i, + systemd: /Systèmes : Linux|Exploiter les serveurs Linux|LPIC-1|LFCS/i, + routagedyn: /réseau fondamental|CCNA/i, + vulns: /Sécurité offensive|pentest|exploitation/i, + ml2: /Éthique, biais et AI Act/i, + bigdata: /Ingénierie des données et big data/i, + modelisation: /Systèmes d’information et urbanisation|Génie logiciel et architecture/i +}; +(window.CONTENUS_PRO || []).forEach(function (c) { + if (!c.intro) return; + Object.keys(ext).forEach(function (k) { if (k.replace(/\d+$/, '') === c.id) c.match = new RegExp(c.match.source + '|' + ext[k].source, 'i'); }); +}); +})(); diff --git a/portail/public/js/contenus-data-4.js b/portail/public/js/contenus-data-4.js new file mode 100644 index 0000000..336e0f3 --- /dev/null +++ b/portail/public/js/contenus-data-4.js @@ -0,0 +1,171 @@ +// Cours complets · Data & IA lot 4 : pandas, apprentissage automatique, réseaux de neurones +(function () { +var reg = function (c) { + (window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours); + (window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus; + (window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples; + (window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] }; +}; + +reg({ +cours: { id: 'pandas', match: /pandas|analyse de données|dataframe|traitement de données|données tabulaires|CSV/i, + titre: 'Analyse de données avec pandas : charger, nettoyer, agréger', duree: '', + niveau: 'NSI Terminale · BTS SIO SLAM · BUT Informatique / SD · Master data', + prerequis: ['Python : listes, dictionnaires, fonctions', 'SQL : SELECT, GROUP BY'], + competences: ['NSI · Traitement de données en tables', 'BTS SIO SLAM · Exploiter des données', 'BUT SD · Traiter des données'], + intro: 'Les données réelles arrivent sales : exports Pronote en point-virgule avec des virgules décimales, cellules vides, doublons, dates au format texte, noms mal orthographiés. Avant toute statistique ou tout modèle d’IA, un data engineer passe l’essentiel de son temps à les charger, les nettoyer et les restructurer. pandas est la bibliothèque Python de référence pour ce travail : elle manipule des tableaux de millions de lignes avec une syntaxe proche de SQL.', + cm: [ + { t: 'Series et DataFrame', d: '', html: 'Une <b>Series</b> est une colonne typée avec un <b>index</b> ; un <b>DataFrame</b> est un tableau de Series partageant le même index. Chaque colonne a un <b>dtype</b> (int64, float64, object pour le texte, datetime64, category). Les opérations sont <b>vectorisées</b> : <code>df["note"] * 2</code> s’applique à toute la colonne en une instruction, bien plus vite qu’une boucle Python.' }, + { t: 'Charger des données réelles', d: '', html: '<code>pd.read_csv</code> accepte de nombreux paramètres indispensables avec les exports français : <code>sep=";"</code>, <code>decimal=","</code>, <code>encoding="utf-8-sig"</code> ou <code>"latin-1"</code>, <code>parse_dates</code>, <code>dtype</code>. On inspecte toujours avant de calculer : <code>df.head()</code>, <code>df.info()</code> (types et valeurs manquantes), <code>df.describe()</code> (statistiques).', exemple: 'import pandas as pd\ndf = pd.read_csv("notes.csv", sep=";", decimal=",", encoding="utf-8-sig", parse_dates=["date"], dayfirst=True)\ndf.info()' }, + { t: 'Sélectionner et filtrer', d: '', html: '<code>df["col"]</code> sélectionne une colonne, <code>df[["a","b"]]</code> plusieurs. <code>df.loc[lignes, colonnes]</code> sélectionne par étiquette, <code>df.iloc</code> par position. Le filtrage utilise des masques booléens combinés par <code>&</code>, <code>|</code> et <code>~</code>, chaque condition entre parenthèses.', exemple: 'df.loc[(df["classe"] == "SIO1") & (df["note"] < 8), ["nom", "matiere", "note"]]', attention: 'Modifier le résultat d’un filtre (df[df.note < 8]["note"] = 8) ne modifie pas df et produit un avertissement SettingWithCopy. On écrit df.loc[df.note < 8, "note"] = 8.' }, + { t: 'Nettoyer', d: '', html: 'Les valeurs manquantes sont <code>NaN</code>. <code>isna().sum()</code> les compte, <code>dropna</code> retire les lignes, <code>fillna</code> les remplace. Le choix est métier : une note absente n’est pas un zéro. <code>drop_duplicates</code> retire les doublons, <code>str.strip().str.upper()</code> normalise le texte, <code>astype</code> et <code>pd.to_numeric(errors="coerce")</code> convertissent les types en transformant les valeurs invalides en NaN.' }, + { t: 'Agréger et joindre', d: '', html: '<code>groupby</code> découpe, applique, combine, exactement comme GROUP BY en SQL : <code>df.groupby("classe")["note"].agg(["mean","median","count"])</code>. <code>pivot_table</code> produit un tableau croisé. <code>merge</code> joint deux DataFrame comme un JOIN, avec <code>how="left"</code> pour garder toutes les lignes de gauche et <code>validate="m:1"</code> pour vérifier qu’on ne multiplie pas les lignes par erreur.' }, + { t: 'Performances et export', d: '', html: 'Sur de gros volumes : lire seulement les colonnes utiles (<code>usecols</code>), typer le texte répétitif en <code>category</code>, éviter <code>apply</code> ligne à ligne au profit des opérations vectorisées, et stocker les résultats intermédiaires en <b>Parquet</b>, format colonne compressé et typé, bien plus rapide que le CSV. L’export final se fait avec <code>to_csv</code>, <code>to_excel</code> ou directement dans une base avec <code>to_sql</code>.' } + ], + synthese: ['DataFrame = colonnes typées partageant un index ; opérations vectorisées', 'read_csv avec sep, decimal, encoding, parse_dates', 'Inspecter : head, info, describe, isna().sum()', 'loc pour sélectionner et modifier ; masques entre parenthèses', 'Valeurs manquantes : décision métier', 'groupby, pivot_table, merge avec validate', 'Parquet et category pour les gros volumes'], + qcm: [ + ['Quel paramètre pour lire « 12,5 » comme un nombre ?', ['sep=","', 'decimal=","', 'dtype=float', 'encoding'], 1, 'Virgule décimale française.'], + ['Que montre df.info() ?', ['Les 5 premières lignes', 'Types et nombre de valeurs non nulles par colonne', 'La moyenne', 'Le graphique'], 1, 'Premier réflexe.'], + ['Comment combiner deux conditions ?', ['and', '(c1) & (c2)', 'c1 && c2', 'c1 + c2'], 1, 'Opérateurs bit à bit sur des masques.'], + ['Une note absente doit-elle devenir 0 ?', ['Toujours', 'Non, c’est une décision métier (absence ≠ zéro)', 'Oui avec fillna', 'Peu importe'], 1, 'Elle fausserait la moyenne.'], + ['Équivalent pandas de GROUP BY ?', ['merge', 'groupby', 'pivot', 'concat'], 1, 'Découper, appliquer, combiner.'], + ['Pourquoi préférer Parquet au CSV pour les fichiers intermédiaires ?', ['Plus lisible', 'Typé, compressé, lecture par colonne rapide', 'Obligatoire', 'Compatible Excel'], 1, 'Format colonne.'] + ], + td: [ + { n: 'Application', c: 'Charger l’export notes.csv (point-virgule, virgule décimale), afficher le nombre de notes manquantes par matière, puis la moyenne par classe et par matière en tableau croisé.', r: 'df = pd.read_csv("notes.csv", sep=";", decimal=",")\ndf.groupby("matiere")["note"].apply(lambda s: s.isna().sum())\ndf.pivot_table(index="classe", columns="matiere", values="note", aggfunc="mean").round(1)' }, + { n: 'Application', c: 'Joindre notes.csv et eleves.csv sur l’identifiant, en vérifiant qu’aucun élève n’est dupliqué, et lister les élèves sans aucune note.', r: 'm = eleves.merge(notes, on="id", how="left", validate="1:m")\nm[m["note"].isna()].drop_duplicates("id")[["nom","prenom"]]' }, + { n: 'Approfondissement', c: 'Repérer les élèves dont la moyenne a baissé de plus de 3 points entre le 1er et le 2e trimestre.', r: 'p = df.pivot_table(index="id", columns="trimestre", values="note", aggfunc="mean")\np[(p[1] - p[2]) > 3]' } + ], + tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Jeu de données anonymisé d’un établissement, tests automatiques.', etapes: ['Charger et inspecter', 'Nettoyer types et doublons', 'Agrégats par classe', 'Jointure avec les effectifs', 'Exporter un bilan en CSV'] }, + plusLoin: [['Documentation pandas', 'https://pandas.pydata.org/docs/'], ['DataCamp · Data Manipulation with pandas', 'https://www.datacamp.com/courses/data-manipulation-with-pandas'], ['data.gouv.fr · jeux de données ouverts', 'https://www.data.gouv.fr/']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Le tableau de bord du conseil de classe affiche une moyenne générale de 4,2 pour la SIO1, alors que les enseignants constatent des moyennes autour de 12. Trouver et corriger l’erreur du script d’analyse.', etapes: [ + ['Inspecter les types', 'df.info()\n note 1840 non-null object\n→ La colonne note est du texte : read_csv n’a pas reconnu la virgule décimale.'], + ['Voir ce qui a été calculé', 'df["note"].head(3)\n0 12,5\n1 14\n2 abs\n→ Des valeurs « abs » et des virgules. Le script faisait pd.to_numeric(errors="coerce") : « 12,5 » et « abs » devenaient NaN puis étaient remplacés par 0 avec fillna(0).'], + ['Corriger le chargement', 'df = pd.read_csv("notes.csv", sep=";", decimal=",", na_values=["abs", "ABS", "disp"])\ndf["note"].dtype\nfloat64\n→ Les absences deviennent des valeurs manquantes, pas des zéros.'], + ['Recalculer', 'df.groupby("classe")["note"].mean().round(2)\nSIO1 11.87\n→ Moyenne cohérente avec le terrain.'], + ['Prévenir', '→ Ajout de contrôles automatiques dans le script : assert df["note"].between(0, 20).all(), et journalisation du nombre d’absences traitées. Aucun fillna(0) sur des notes.'] + ] }, + erreurs: ['Oublier decimal="," sur un export français', 'Remplacer les absences par zéro', 'Modifier un filtre au lieu d’utiliser loc', 'Boucler ligne par ligne avec iterrows', 'Joindre sans vérifier les doublons', 'Calculer sans avoir regardé info()'], + memo: [['Instruction', 'Usage'], ['pd.read_csv(f, sep=";", decimal=",")', 'lire un export français'], ['df.head() · df.info() · df.describe()', 'inspecter'], ['df.isna().sum()', 'valeurs manquantes par colonne'], ['df.loc[masque, cols]', 'filtrer et sélectionner'], ['df.groupby(k)[c].agg([...])', 'agréger'], ['df.pivot_table(index, columns, values)', 'tableau croisé'], ['a.merge(b, on, how, validate)', 'joindre'], ['df.drop_duplicates()', 'retirer les doublons'], ['pd.to_numeric(s, errors="coerce")', 'convertir en nombre'], ['df.to_parquet() · df.to_csv()', 'exporter']], + examen: { titre: 'Sujet type · 20 points', q: ['Écrire le chargement d’un export fourni. (3 pts)', 'Nettoyer les valeurs manquantes en justifiant. (4 pts)', 'Produire trois agrégats demandés. (6 pts)', 'Joindre deux tables et détecter les incohérences. (4 pts)', 'Expliquer vectorisation et Parquet. (3 pts)'], r: ['read_csv paramétré', 'na_values, pas de 0', 'groupby / pivot_table', 'merge validate, how', 'voir cours'] }, + prof: { deroule: [['Ouverture', 'Un vrai export raté projeté'], ['Cours', 'DataFrame, chargement, inspection'], ['Pratique', 'Nettoyage dans l’IDE'], ['Pratique', 'Agrégats et jointures'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : cellules à trous. Élèves avancés : Parquet, polars, DataCamp.', eval: 'Tests IDE, sujet type.' } }, +exemples: [ + ['Inspecter un export', 'Premier chargement.', 'df.info()', 'RangeIndex: 1840 entries\n # Column Non-Null Count Dtype\n 0 nom 1840 non-null object\n 1 classe 1840 non-null object\n 2 note 1712 non-null float64\n 3 date 1840 non-null datetime64[ns]', '128 notes manquantes (absences), types corrects : on peut calculer.'], + ['Compter par groupe', 'Effectifs par classe.', 'df.groupby("classe")["nom"].nunique()', 'classe\nSIO1 31\nSIO2 27', 'nunique compte les élèves distincts, pas le nombre de lignes de notes.'], + ['Détecter des doublons', 'Un élève apparaît deux fois.', 'eleves[eleves.duplicated("ine", keep=False)]', ' ine nom prenom\n12 070123456AB MARTIN Léa\n88 070123456AB Martin Lea', 'Même identifiant INE, casse et accents différents : on normalise avant de dédoublonner.'], + ['Jointure qui multiplie les lignes', 'Le nombre de lignes explose après merge.', 'notes.merge(eleves, on="nom", validate="m:1")', 'MergeError: Merge keys are not unique in right dataset; not a many-to-one merge', 'Deux élèves portent le même nom : on joint sur l’identifiant, jamais sur le nom.'], + ['Tableau croisé', 'Bilan pour le conseil.', 'df.pivot_table(index="classe", columns="matiere", values="note", aggfunc="mean").round(1)', 'matiere Maths Réseaux Dev\nclasse\nSIO1 11.2 12.8 10.4\nSIO2 12.0 13.1 12.7', 'Le Dev de SIO1 est le point faible, piste pour le conseil.'], + ['Mesurer un gain de performance', 'Script lent sur 2 millions de lignes.', '%timeit df["total"] = df.apply(lambda r: r.note * r.coef, axis=1)\n%timeit df["total"] = df["note"] * df["coef"]', '8.41 s ± 120 ms\n6.2 ms ± 0.3 ms', 'La version vectorisée est plus de mille fois plus rapide.'] +], +termes: ['pandas', 'DataFrame', 'Series', 'index', 'dtype', 'read_csv', 'loc', 'iloc', 'NaN', 'isna', 'fillna', 'dropna', 'groupby', 'agg', 'pivot_table', 'merge', 'validate', 'drop_duplicates', 'Parquet', 'category', 'vectorisé'] +}); + +reg({ +cours: { id: 'ml', match: /apprentissage automatique|machine learning|scikit|régression|classification|intelligence artificielle|\bIA\b/i, + titre: 'Apprentissage automatique : du jeu de données au modèle évalué', duree: '', + niveau: 'NSI Terminale (k plus proches voisins) · BUT SD / Informatique · École d’ingénieurs · Master IA', + prerequis: ['Analyse de données avec pandas', 'Statistiques descriptives'], + competences: ['NSI · Algorithme des k plus proches voisins', 'BUT SD · Modéliser', 'Master · Concevoir un système d’apprentissage'], + intro: 'Un modèle d’apprentissage automatique apprend une règle à partir d’exemples plutôt qu’on ne la lui programme. Il détecte un courriel frauduleux, prédit le risque de décrochage d’un élève ou reconnaît un caractère manuscrit. Mais un modèle mal évalué donne des chiffres flatteurs et des décisions fausses. Ce module suit la démarche complète avec scikit-learn : préparer les données, séparer apprentissage et test, choisir un modèle, mesurer correctement, et comprendre les limites éthiques et réglementaires.', + cm: [ + { t: 'Apprendre à partir d’exemples', d: '', html: 'En <b>apprentissage supervisé</b>, chaque exemple a des <b>variables explicatives</b> (X) et une <b>cible</b> (y) connue : on apprend à prédire y à partir de X. C’est une <b>classification</b> si y est une catégorie (décroche / ne décroche pas), une <b>régression</b> si y est un nombre (note finale). En <b>apprentissage non supervisé</b>, il n’y a pas de cible : on cherche une structure, par exemple des groupes d’élèves aux profils proches (clustering).' }, + { t: 'Séparer apprentissage et test', d: '', html: 'Un modèle évalué sur les données qui ont servi à l’entraîner paraît toujours excellent : il les a apprises par cœur. On sépare donc un <b>jeu d’apprentissage</b> et un <b>jeu de test</b> jamais vu (<code>train_test_split</code>, souvent 80/20, stratifié pour garder la proportion des classes). Pour régler les hyperparamètres sans toucher au test, on utilise la <b>validation croisée</b> : l’apprentissage est découpé en k parts, chacune servant tour à tour de validation.<br><br>Le <b>surapprentissage</b> (overfitting) se voit quand le score d’apprentissage est très bon et celui de test nettement moins bon ; le <b>sous-apprentissage</b> quand les deux sont mauvais.', attention: 'La fuite de données (data leakage) fausse tout : normaliser toutes les données avant la séparation, ou inclure une variable connue seulement après coup (la note finale pour prédire le décrochage), donne un modèle parfait en test et inutile en réalité.' }, + { t: 'Préparer les variables', d: '', html: 'Les modèles attendent des nombres. On <b>encode</b> les catégories (OneHotEncoder), on <b>normalise</b> les échelles (StandardScaler) pour les modèles sensibles aux distances comme les k plus proches voisins, on traite les valeurs manquantes (SimpleImputer). Un <b>Pipeline</b> enchaîne ces étapes et le modèle : il apprend les paramètres de préparation sur l’apprentissage uniquement et les applique au test, ce qui évite la fuite de données.', exemple: 'from sklearn.pipeline import make_pipeline\nfrom sklearn.compose import make_column_transformer\nfrom sklearn.preprocessing import StandardScaler, OneHotEncoder\nfrom sklearn.ensemble import RandomForestClassifier\n\nprep = make_column_transformer(\n (StandardScaler(), ["absences", "moyenne_t1", "retards"]),\n (OneHotEncoder(handle_unknown="ignore"), ["filiere"]))\nmodele = make_pipeline(prep, RandomForestClassifier(n_estimators=300, random_state=0))\nmodele.fit(X_train, y_train)' }, + { t: 'Quelques modèles de référence', d: '', html: 'Les <b>k plus proches voisins</b> (au programme de NSI) classent un exemple selon la classe majoritaire de ses k voisins les plus proches. La <b>régression linéaire</b> et la <b>régression logistique</b> sont simples, rapides et interprétables. Les <b>arbres de décision</b> produisent des règles lisibles ; les <b>forêts aléatoires</b> et le <b>gradient boosting</b> en combinent des centaines et sont souvent les plus performants sur des données tabulaires. On commence toujours par une <b>référence simple</b> (prédire la classe majoritaire) pour savoir si le modèle apporte vraiment quelque chose.' }, + { t: 'Mesurer correctement', d: '', html: 'L’<b>exactitude</b> (accuracy) trompe quand les classes sont déséquilibrées : si 5 % des élèves décrochent, prédire « personne ne décroche » donne 95 %. La <b>matrice de confusion</b> distingue vrais et faux positifs et négatifs. La <b>précision</b> mesure la part de vrais positifs parmi les alertes, le <b>rappel</b> la part des vrais cas détectés, le <b>F1</b> leur moyenne harmonique. Le choix dépend du coût des erreurs : manquer un élève en difficulté coûte plus cher qu’un entretien inutile, on privilégie donc le rappel. En régression, on utilise l’erreur absolue moyenne (MAE) ou la racine de l’erreur quadratique (RMSE).' }, + { t: 'Éthique et cadre légal', d: '', html: 'Un modèle reproduit les biais de ses données : entraîné sur des décisions passées discriminantes, il les automatise. Pour des décisions qui touchent des personnes, et a fortiori des mineurs, on vérifie les performances par sous-groupe, on garde un humain dans la décision et on explique les facteurs du modèle. Le <b>RGPD</b> encadre les décisions individuelles automatisées (article 22) et impose la minimisation des données ; le <b>règlement européen sur l’IA</b> (AI Act) classe l’éducation parmi les usages à haut risque, soumis à des obligations de documentation, de qualité des données et de supervision humaine.' } + ], + synthese: ['Supervisé (classification, régression) ou non supervisé', 'Toujours évaluer sur un jeu de test jamais vu ; validation croisée pour régler', 'Pipeline pour préparer sans fuite de données', 'Partir d’une référence simple', 'Accuracy trompeuse si déséquilibre : matrice de confusion, précision, rappel, F1', 'Biais, supervision humaine, RGPD article 22, AI Act'], + qcm: [ + ['Prédire la note finale d’un élève est une :', ['classification', 'régression', 'clustering', 'réduction de dimension'], 1, 'La cible est un nombre.'], + ['Score d’apprentissage 0,99, score de test 0,71 : c’est', ['du sous-apprentissage', 'du surapprentissage', 'normal', 'une fuite de données'], 1, 'Le modèle a appris par cœur.'], + ['5 % de décrocheurs : un modèle qui prédit « aucun » a une accuracy de :', ['5 %', '50 %', '95 %', '0 %'], 2, 'D’où les autres métriques.'], + ['Quelle métrique privilégier pour ne manquer aucun élève en difficulté ?', ['Précision', 'Rappel', 'Accuracy', 'MAE'], 1, 'Part des vrais cas détectés.'], + ['Pourquoi utiliser un Pipeline ?', ['Esthétique', 'Pour appliquer la préparation apprise sur l’apprentissage seulement, sans fuite', 'Plus rapide', 'Obligatoire'], 1, 'Évite la fuite de données.'], + ['Selon l’AI Act, l’usage de l’IA dans l’éducation est classé :', ['interdit', 'à haut risque', 'sans risque', 'non concerné'], 1, 'Obligations renforcées.'] + ], + td: [ + { n: 'Échauffement', c: 'Implémenter les k plus proches voisins en Python pur pour classer un point à partir d’une liste de points étiquetés.', r: 'def knn(pts, x, k):\n d = sorted(pts, key=lambda p: (p[0]-x[0])**2 + (p[1]-x[1])**2)[:k]\n etiq = [p[2] for p in d]\n return max(set(etiq), key=etiq.count)' }, + { n: 'Application', c: 'Entraîner une régression logistique et une forêt aléatoire sur le jeu fourni, comparer leur F1 en validation croisée à 5 plis, puis évaluer la meilleure sur le test.', r: 'from sklearn.model_selection import cross_val_score\nfor m in [logit, foret]:\n print(cross_val_score(m, X_train, y_train, cv=5, scoring="f1").mean())\nmeilleur.fit(X_train, y_train); f1_score(y_test, meilleur.predict(X_test))' }, + { n: 'Approfondissement', c: 'Vérifier si le modèle de détection du décrochage a un rappel différent pour les filles et les garçons, et proposer une conduite à tenir.', r: 'for g in ["F","M"]: m = X_test["sexe"] == g; recall_score(y_test[m], pred[m])\nSi écart important : examiner les données, rééquilibrer, ajuster le seuil par sous-groupe avec prudence, documenter, et garder la décision humaine.' } + ], + tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'scikit-learn dans le navigateur (Pyodide), jeu de données synthétique.', etapes: ['Séparer apprentissage et test', 'Construire un Pipeline', 'Comparer trois modèles en validation croisée', 'Lire la matrice de confusion', 'Analyser les performances par sous-groupe'] }, + plusLoin: [['scikit-learn · guide utilisateur', 'https://scikit-learn.org/stable/user_guide.html'], ['CNIL · IA : dossiers et recommandations', 'https://www.cnil.fr/fr/intelligence-artificielle'], ['DataCamp · Supervised Learning with scikit-learn', 'https://www.datacamp.com/courses/supervised-learning-with-scikit-learn'], ['Kaggle Learn · Intro to Machine Learning', 'https://www.kaggle.com/learn/intro-to-machine-learning']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Un prototype de détection du décrochage affiche 98 % d’exactitude et 0,97 de F1 en test. Mis en service sur la nouvelle année, il ne détecte presque aucun élève. Trouver pourquoi.', etapes: [ + ['Regarder les variables', 'modele[-1].feature_importances_ triées\nnb_absences_annee 0.71\nmoyenne_t1 0.08\n→ Le nombre d’absences sur l’année entière domine. En septembre, cette variable n’est pas encore connue.'], + ['Identifier la fuite', '→ La variable résume toute l’année, y compris la période où l’élève a déjà décroché : le modèle « prédit » à partir d’un effet du décrochage. C’est une fuite de données temporelle.'], + ['Reconstruire le jeu', 'Variables limitées à ce qui est connu au 15 octobre : absences et retards de septembre, moyenne des premières évaluations, filière.\nSéparation temporelle : apprentissage sur 2022-2024, test sur 2025.\n→ On évalue comme on utilisera le modèle.'], + ['Réévaluer', 'F1 test : 0.58, rappel 0.71, précision 0.49\n→ Scores plus modestes mais honnêtes : le modèle repère 71 % des futurs décrocheurs, avec une alerte sur deux justifiée.'], + ['Décider de l’usage', '→ Usage limité à une aide au repérage pour l’équipe de vie scolaire, jamais une décision automatique ; analyse d’impact (AIPD) et information des familles. Documentation du modèle et de ses limites.'] + ] }, + erreurs: ['Évaluer sur les données d’apprentissage', 'Normaliser avant de séparer', 'Utiliser une variable connue après coup', 'Se fier à l’accuracy sur des classes déséquilibrées', 'Pas de référence simple', 'Décision automatique sur des élèves sans supervision humaine'], + memo: [['Instruction', 'Usage'], ['train_test_split(X, y, test_size=0.2, stratify=y)', 'séparer'], ['make_pipeline(prep, modele)', 'préparer sans fuite'], ['cross_val_score(m, X, y, cv=5, scoring="f1")', 'validation croisée'], ['GridSearchCV', 'régler les hyperparamètres'], ['DummyClassifier', 'référence simple'], ['confusion_matrix · classification_report', 'mesurer'], ['precision_score · recall_score · f1_score', 'métriques'], ['mean_absolute_error', 'régression'], ['feature_importances_ · permutation_importance', 'comprendre le modèle']], + examen: { titre: 'Sujet type · 20 points', q: ['Distinguer classification, régression, clustering. (3 pts)', 'Construire et évaluer un pipeline sur un jeu fourni. (8 pts)', 'Interpréter une matrice de confusion. (4 pts)', 'Identifier une fuite de données dans un cas donné. (3 pts)', 'Citer deux obligations RGPD ou AI Act. (2 pts)'], r: ['voir cours', 'split, pipeline, cv, test', 'précision, rappel', 'variable postérieure', 'art. 22, haut risque'] }, + prof: { deroule: [['Ouverture', 'Un modèle à 95 % qui ne sert à rien'], ['Cours', 'Supervisé, séparation, métriques'], ['Pratique', 'kNN à la main'], ['Pratique', 'Pipeline scikit-learn'], ['Débat', 'IA et décisions sur les élèves'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : notebook guidé. Élèves avancés : gradient boosting, interprétabilité (SHAP), Kaggle.', eval: 'Notebook rendu, sujet type.' } }, +exemples: [ + ['Séparer proprement', 'Préparer l’évaluation.', 'X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2, stratify=y, random_state=0)\nprint(y_tr.mean(), y_te.mean())', '0.0512 0.0508', 'stratify garde la même proportion de cas positifs dans les deux jeux.'], + ['Référence simple', 'Savoir si le modèle apporte quelque chose.', 'DummyClassifier(strategy="most_frequent").fit(X_tr, y_tr).score(X_te, y_te)', '0.949', 'Toute accuracy inférieure à 95 % est pire que ne rien faire : il faut regarder rappel et F1.'], + ['Matrice de confusion', 'Évaluer le modèle.', 'confusion_matrix(y_te, pred)', '[[1782 31]\n [ 28 69]]', '69 décrocheurs repérés sur 97 (rappel 0,71), 31 fausses alertes sur 100 alertes (précision 0,69).'], + ['Rapport complet', 'Présenter les résultats.', 'print(classification_report(y_te, pred, target_names=["reste", "décroche"]))', ' precision recall f1-score support\n reste 0.98 0.98 0.98 1813\n décroche 0.69 0.71 0.70 97', 'La classe minoritaire est celle qui compte ; ses scores sont bien plus faibles que ceux de la majorité.'], + ['Détecter le surapprentissage', 'Arbre de décision très profond.', 'arbre.score(X_tr, y_tr), arbre.score(X_te, y_te)', '(1.0, 0.89)', 'Score parfait en apprentissage, bien moindre en test : on limite max_depth ou on passe à une forêt aléatoire.'], + ['Importance par permutation', 'Expliquer le modèle à l’équipe.', 'r = permutation_importance(modele, X_te, y_te, scoring="f1", n_repeats=10)\nsorted(zip(r.importances_mean, X_te.columns), reverse=True)[:3]', '[(0.21, "absences_sept"), (0.09, "moyenne_eval1"), (0.04, "retards_sept")]', 'Les absences de septembre sont le premier signal, cohérent avec l’expérience des CPE.'] +], +termes: ['apprentissage supervisé', 'classification', 'régression', 'clustering', 'jeu d’apprentissage', 'jeu de test', 'validation croisée', 'surapprentissage', 'fuite de données', 'Pipeline', 'StandardScaler', 'OneHotEncoder', 'k plus proches voisins', 'forêt aléatoire', 'matrice de confusion', 'précision', 'rappel', 'F1', 'accuracy', 'scikit-learn', 'AI Act', 'RGPD'] +}); + +reg({ +cours: { id: 'neurones', match: /réseaux? de neurones|deep learning|apprentissage profond|PyTorch|TensorFlow|OCR|reconnaissance vocale/i, + titre: 'Réseaux de neurones : principe, entraînement et applications (OCR, voix)', duree: '', + niveau: 'BUT Informatique / SD · École d’ingénieurs · Master IA', + prerequis: ['Apprentissage automatique', 'Dérivées et matrices'], + competences: ['Master · Concevoir et entraîner un modèle d’apprentissage profond', 'École d’ingénieurs · Intégrer un modèle en production'], + intro: 'Reconnaître un caractère manuscrit, transcrire une voix, décrire une image : ces tâches, longtemps hors de portée des programmes classiques, sont aujourd’hui résolues par des réseaux de neurones. Ce module explique ce qu’est réellement un réseau, comment il apprend par descente de gradient et rétropropagation, quelles architectures conviennent aux images et au son, et comment on met un tel modèle en production.', + cm: [ + { t: 'Le neurone et la couche', d: '', html: 'Un neurone artificiel calcule une somme pondérée de ses entrées, ajoute un biais et applique une <b>fonction d’activation</b> non linéaire : ReLU (max(0, x)) dans les couches cachées, sigmoïde ou softmax en sortie pour obtenir des probabilités. Une <b>couche dense</b> est un produit matriciel suivi d’une activation. Empiler des couches permet de représenter des fonctions très complexes ; sans non-linéarité, plusieurs couches se réduiraient à une seule transformation linéaire.' }, + { t: 'Apprendre : perte, gradient, rétropropagation', d: '', html: 'L’entraînement minimise une <b>fonction de perte</b> (entropie croisée pour la classification, erreur quadratique pour la régression). La <b>descente de gradient</b> ajuste chaque poids dans la direction qui fait baisser la perte, d’un pas fixé par le <b>taux d’apprentissage</b>. La <b>rétropropagation</b> calcule efficacement ces gradients couche par couche grâce à la règle de dérivation des fonctions composées. On traite les données par <b>lots</b> (mini-batches) et on parcourt plusieurs fois le jeu (<b>époques</b>). Un taux trop grand fait diverger, trop petit rend l’apprentissage interminable.' }, + { t: 'Images : réseaux convolutifs', d: '', html: 'Une image de 28×28 pixels a 784 entrées ; une photo en a des millions. Les <b>réseaux convolutifs</b> (CNN) appliquent de petits filtres qui glissent sur l’image et détectent des motifs locaux (bords, courbes), puis des motifs de plus en plus abstraits d’une couche à l’autre. Les poids d’un filtre sont partagés sur toute l’image, ce qui réduit énormément le nombre de paramètres et rend le réseau insensible à la position du motif. C’est le cœur des systèmes d’<b>OCR</b> : détection des zones de texte, puis reconnaissance des caractères.' }, + { t: 'Séquences : son, texte et transformeurs', d: '', html: 'Le son est une séquence : on le convertit d’abord en <b>spectrogramme</b> (énergie par fréquence au cours du temps, souvent en échelle de Mel), que l’on traite comme une image ou comme une séquence. Les réseaux récurrents ont longtemps servi à la reconnaissance vocale ; ils sont aujourd’hui largement remplacés par les <b>transformeurs</b>, fondés sur le mécanisme d’<b>attention</b> qui relie chaque élément de la séquence à tous les autres. Whisper (transcription) et les grands modèles de langage reposent sur cette architecture.' }, + { t: 'Entraîner en pratique', d: '', html: 'On entraîne rarement de zéro : l’<b>apprentissage par transfert</b> part d’un modèle pré-entraîné sur un grand corpus et l’ajuste (fine-tuning) sur ses propres données. Contre le surapprentissage : <b>augmentation de données</b> (rotations, bruit, variations de vitesse pour la voix), <b>dropout</b>, <b>arrêt précoce</b> quand la perte de validation remonte. Les calculs se font sur GPU ; un suivi des courbes de perte d’apprentissage et de validation est indispensable.' }, + { t: 'Mettre en production', d: '', html: 'Un modèle en production doit être rapide et léger : export en format standard (ONNX), <b>quantification</b> (poids en 8 bits au lieu de 32), exécution sur CPU si possible. On surveille la <b>dérive</b> : les données réelles s’éloignent de celles de l’entraînement (nouvelle police de caractères, micro différent), et les performances baissent sans erreur visible. Côté réglementaire, la voix est une donnée biométrique potentielle au sens du RGPD : traitement local de préférence, finalité limitée, conservation minimale.' } + ], + synthese: ['Neurone = somme pondérée + biais + activation non linéaire', 'Perte, descente de gradient, rétropropagation, lots et époques', 'CNN pour les images et l’OCR ; spectrogrammes et transformeurs pour la voix', 'Transfert, augmentation, dropout, arrêt précoce', 'Production : ONNX, quantification, surveillance de la dérive, RGPD'], + qcm: [ + ['Pourquoi une activation non linéaire ?', ['Pour accélérer', 'Sinon les couches se réduisent à une seule transformation linéaire', 'Pour normaliser', 'Obligatoire en Python'], 1, 'La non-linéarité donne la puissance.'], + ['Que calcule la rétropropagation ?', ['Les prédictions', 'Les gradients de la perte par rapport aux poids', 'Le taux d’apprentissage', 'Les lots'], 1, 'Règle de dérivation en chaîne.'], + ['Avantage principal d’une convolution sur une image ?', ['Couleurs', 'Partage des poids et détection de motifs locaux', 'Plus de paramètres', 'Aucun'], 1, 'Moins de paramètres, invariance à la position.'], + ['Comment représente-t-on souvent le son pour un réseau ?', ['En texte', 'En spectrogramme', 'En binaire brut', 'En CSV'], 1, 'Temps × fréquence.'], + ['La perte de validation remonte alors que celle d’apprentissage baisse :', ['tout va bien', 'surapprentissage, arrêt précoce', 'taux trop faible', 'données manquantes'], 1, 'On arrête ou on régularise.'], + ['Qu’est-ce que la quantification ?', ['Compter les données', 'Réduire la précision des poids pour alléger le modèle', 'Ajouter des couches', 'Chiffrer le modèle'], 1, 'Souvent 8 bits.'] + ], + td: [ + { n: 'Application', c: 'Calculer à la main la sortie d’un neurone : entrées (1, 2), poids (0,5 ; −1), biais 0,2, activation ReLU.', r: '0,5 × 1 + (−1) × 2 + 0,2 = −1,3 → ReLU = 0' }, + { n: 'Application', c: 'Écrire en PyTorch un réseau à deux couches pour MNIST (784 → 128 → 10) et sa boucle d’entraînement.', r: 'model = nn.Sequential(nn.Flatten(), nn.Linear(784,128), nn.ReLU(), nn.Linear(128,10))\nopt = torch.optim.Adam(model.parameters(), 1e-3); loss = nn.CrossEntropyLoss()\nfor x, y in loader: opt.zero_grad(); l = loss(model(x), y); l.backward(); opt.step()' }, + { n: 'Approfondissement', c: 'Un OCR entraîné sur des copies scannées échoue sur des photos prises au téléphone. Proposer trois actions.', r: 'Augmentation (perspective, flou, éclairage) · fine-tuning sur un échantillon de photos annotées · prétraitement (redressement, binarisation) et surveillance de la dérive.' } + ], + tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Réseau dense sur des chiffres manuscrits, dans le navigateur.', etapes: ['Visualiser le jeu de données', 'Construire le réseau', 'Tracer les courbes de perte', 'Mesurer l’exactitude par chiffre', 'Tester l’augmentation de données'] }, + plusLoin: [['3Blue1Brown · Neural networks (vidéos)', 'https://www.3blue1brown.com/topics/neural-networks'], ['Tutoriels PyTorch', 'https://pytorch.org/tutorials/'], ['Fidle (CNRS) · formation deep learning en français', 'https://fidle.cnrs.fr/'], ['CNIL · la voix, une donnée à protéger', 'https://www.cnil.fr/']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Le module de transcription vocale des consignes, déployé en salle de TP, fonctionnait bien en tests mais produit des transcriptions incohérentes depuis l’installation des nouveaux casques.', etapes: [ + ['Comparer les entrées', 'soxi test_ancien.wav nouveau.wav\nSample Rate : 16000 | Sample Rate : 48000\n→ Les nouveaux casques enregistrent en 48 kHz ; le modèle attend du 16 kHz.'], + ['Vérifier le prétraitement', '→ Le code passait l’audio brut au modèle sans rééchantillonnage : le spectrogramme calculé était « compressé » en fréquence, comme une voix accélérée trois fois.'], + ['Corriger', 'audio = torchaudio.functional.resample(audio, orig_freq=sr, new_freq=16000)\n→ Rééchantillonnage systématique à l’entrée, avec contrôle du format.'], + ['Valider', 'WER sur 50 consignes de test : 38 % → 7 %\n→ Le taux d’erreur de mots revient au niveau des tests.'], + ['Prévenir', '→ Assertion sur la fréquence et le nombre de canaux, surveillance du WER sur un échantillon hebdomadaire, et traitement local sans conservation des enregistrements.'] + ] }, + erreurs: ['Pas de jeu de validation', 'Taux d’apprentissage choisi au hasard', 'Ignorer les courbes de perte', 'Prétraitement différent entre entraînement et production', 'Entraîner de zéro sans nécessité', 'Envoyer des voix d’élèves à un service tiers sans analyse RGPD'], + memo: [['Élément', 'À retenir'], ['nn.Linear · nn.Conv2d', 'couche dense · convolution'], ['nn.ReLU · softmax', 'activations'], ['nn.CrossEntropyLoss', 'perte de classification'], ['torch.optim.Adam(lr=1e-3)', 'optimiseur courant'], ['loss.backward() · opt.step()', 'rétropropagation et mise à jour'], ['model.eval() · torch.no_grad()', 'mode inférence'], ['Dropout · EarlyStopping', 'contre le surapprentissage'], ['torch.onnx.export', 'export pour la production'], ['WER · CER', 'erreurs de mots et de caractères (voix, OCR)']], + examen: { titre: 'Sujet type · 20 points', q: ['Calculer la sortie d’un petit réseau. (4 pts)', 'Expliquer descente de gradient et rétropropagation. (5 pts)', 'Justifier le choix d’un CNN pour l’OCR. (4 pts)', 'Analyser des courbes de perte fournies. (4 pts)', 'Citer deux précautions de mise en production. (3 pts)'], r: ['produit matriciel + activation', 'voir cours', 'motifs locaux, partage des poids', 'surapprentissage ou non', 'dérive, quantification, RGPD'] }, + prof: { deroule: [['Ouverture', 'Démonstration OCR sur une copie'], ['Cours', 'Neurone, perte, gradient'], ['Pratique', 'Réseau dense MNIST'], ['Cours', 'CNN et spectrogrammes'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : calculs à la main guidés. Élèves avancés : fine-tuning, Fidle.', eval: 'Notebook, sujet type.' } }, +exemples: [ + ['Vérifier les dimensions', 'Erreur à la première passe.', 'x, y = next(iter(loader)); print(x.shape, model(x).shape)', 'torch.Size([64, 1, 28, 28]) torch.Size([64, 10])', 'Un lot de 64 images en entrée, 10 scores en sortie : les dimensions sont cohérentes.'], + ['Suivre l’apprentissage', 'Entraînement en cours.', 'for e in range(5): … print(e, perte_train, perte_val, acc_val)', '0 0.412 0.228 0.934\n1 0.191 0.160 0.953\n2 0.139 0.131 0.961\n3 0.108 0.129 0.962\n4 0.085 0.134 0.961', 'La perte de validation remonte à l’époque 4 : on garde le modèle de l’époque 3 (arrêt précoce).'], + ['Taux d’apprentissage trop grand', 'La perte explose.', 'opt = torch.optim.SGD(model.parameters(), lr=5.0)', 'époque 0 perte 2.30 → époque 1 perte nan', 'La perte devient NaN : les poids divergent. On revient à lr=1e-2 pour SGD ou 1e-3 pour Adam.'], + ['Mesurer l’OCR', 'Évaluer sur un jeu annoté.', 'jiwer.cer(references, predictions)', '0.043', '4,3 % de caractères erronés : acceptable pour du texte imprimé, à surveiller pour l’écriture manuscrite.'], + ['Alléger le modèle', 'Déploiement sur un mini-PC de salle.', 'q = torch.quantization.quantize_dynamic(model, {nn.Linear}, dtype=torch.qint8)\nprint(taille(model), taille(q))', '1.62 Mo 0.43 Mo', 'Le modèle quantifié est près de quatre fois plus léger, pour une perte d’exactitude généralement faible, à mesurer.'], + ['Transcrire localement', 'Consigne vocale en salle.', 'whisper consigne.wav --model small --language fr', '[00:00.000 --> 00:04.200] Ouvrez le terminal et tapez ip addr.', 'Le modèle tourne sur la machine locale : aucune voix d’élève ne sort de l’établissement.'] +], +termes: ['réseau de neurones', 'neurone', 'couche', 'ReLU', 'softmax', 'fonction de perte', 'descente de gradient', 'rétropropagation', 'taux d’apprentissage', 'époque', 'lot', 'CNN', 'convolution', 'spectrogramme', 'transformeur', 'attention', 'apprentissage par transfert', 'dropout', 'arrêt précoce', 'ONNX', 'quantification', 'dérive', 'OCR', 'WER', 'PyTorch'] +}); +})(); diff --git a/portail/public/js/contenus-devops-1.js b/portail/public/js/contenus-devops-1.js new file mode 100644 index 0000000..3ad8f8d --- /dev/null +++ b/portail/public/js/contenus-devops-1.js @@ -0,0 +1,182 @@ +// Cours complets · DevOps lot 1 : Docker, Docker Compose, Ansible +(function () { +var reg = function (c) { + (window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours); + (window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus; + (window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples; + (window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] }; +}; + +reg({ +cours: { id: 'docker', match: /Docker(?! Compose)|Dockerfile|conteneur/i, + titre: 'Conteneurs Docker : images, Dockerfile, réseaux et volumes', duree: '', + niveau: 'BTS SIO SISR / SLAM · BTS CIEL IR · BUT R&T parcours DevCloud · AIS · Master', + prerequis: ['Le terminal Linux', 'Processus et services systemd', 'Réseau : ports, NAT'], + competences: ['BTS SIO SISR · Bloc 2 · Virtualisation et conteneurs', 'BTS SIO SLAM · Bloc 2 · Déployer une solution applicative', 'BUT R&T DevCloud · Compétence « Automatiser le déploiement »', 'AIS · Administrer une infrastructure virtualisée'], + intro: '« Chez moi ça marche. » Pendant des années, le passage d’une application du poste du développeur au serveur de production a été la principale source d’incidents : version de PHP différente, bibliothèque manquante, configuration oubliée. Le conteneur règle ce problème en livrant l’application avec tout son environnement d’exécution, dans une image immuable qui s’exécute à l’identique partout. Docker a popularisé cette approche en 2013 ; elle est aujourd’hui le socle du déploiement moderne, des petites applications de lycée jusqu’aux plateformes Kubernetes des hébergeurs. Ce module explique ce qu’est réellement un conteneur pour le noyau Linux, comment construire des images propres et sûres, et comment les exploiter en production.', + cm: [ + { t: 'Ce qu’est un conteneur pour le noyau', d: '', html: 'Un conteneur n’est pas une machine virtuelle. Il n’y a ni matériel émulé ni second noyau : c’est un <b>processus Linux ordinaire</b>, que le noyau isole grâce à deux mécanismes. Les <b>namespaces</b> donnent au processus sa propre vue du système : arborescence de processus (pid), interfaces réseau (net), points de montage (mnt), nom d’hôte (uts), utilisateurs (user), communications inter-processus (ipc). Les <b>cgroups</b> (control groups) limitent et mesurent les ressources qu’il consomme : processeur, mémoire, entrées-sorties.<br><br>Conséquences directes : un conteneur démarre en une fraction de seconde, consomme très peu de mémoire en plus de son application, et partage le noyau de l’hôte. C’est aussi sa limite de sécurité : une faille du noyau touche tous les conteneurs. Une VM isole plus fortement, un conteneur est plus léger ; en production, on combine souvent les deux (conteneurs dans des VM).', table: [['', 'Machine virtuelle', 'Conteneur'], ['Isolation', 'hyperviseur, noyau séparé', 'namespaces et cgroups, noyau partagé'], ['Démarrage', 'dizaines de secondes', 'fraction de seconde'], ['Taille', 'plusieurs Go', 'quelques Mo à centaines de Mo'], ['OS invité', 'n’importe lequel', 'même noyau que l’hôte (Linux)'], ['Usage type', 'serveur complet, isolation forte', 'application et ses dépendances']] }, + { t: 'Images, couches et registres', d: '', html: 'Une <b>image</b> est un modèle en lecture seule, composé de <b>couches</b> superposées : chaque instruction d’un Dockerfile qui modifie le système de fichiers ajoute une couche. Le pilote de stockage (overlay2) les empile et ajoute au-dessus une couche inscriptible propre à chaque conteneur. Plusieurs conteneurs issus de la même image partagent ses couches sur le disque.<br><br>Une image est identifiée par un nom, un <b>tag</b> et un <b>digest</b> (empreinte SHA-256 du contenu). Le tag est une étiquette mobile : <code>nginx:latest</code> ne désigne pas la même image aujourd’hui et dans six mois. En production, on fixe une version précise (<code>nginx:1.27.2-alpine</code>) voire le digest, pour que chaque déploiement soit reproductible.<br><br>Les images sont stockées dans des <b>registres</b> : Docker Hub, GitHub Container Registry, ou un registre privé (Harbor, Forgejo, GitLab). <code>docker pull</code> télécharge, <code>docker push</code> publie.', attention: 'Ne jamais utiliser le tag latest en production : un redémarrage peut récupérer silencieusement une nouvelle version majeure et casser l’application.' }, + { t: 'Écrire un Dockerfile de production', d: '', html: 'Le <b>Dockerfile</b> décrit la construction d’une image. Chaque instruction produit une couche, et Docker réutilise en cache les couches dont les instructions et les fichiers n’ont pas changé. L’ordre est donc décisif : on place en premier ce qui change rarement (système, dépendances) et en dernier ce qui change souvent (code).<br><br>Quatre bonnes pratiques font la différence entre un Dockerfile de TP et un Dockerfile de production. Partir d’une <b>image de base minimale et versionnée</b> (debian:bookworm-slim, alpine, distroless). Utiliser un <b>build multi-étapes</b> : on compile dans une première image complète, et on ne copie que le résultat dans une image finale légère, sans compilateurs. <b>Ne pas exécuter en root</b> : créer un utilisateur et le déclarer avec USER. Ajouter un <b>.dockerignore</b> pour ne pas envoyer .git, les secrets ou node_modules dans le contexte de construction.', exemple: '# syntax=docker/dockerfile:1\nFROM python:3.12-slim AS build\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir --prefix=/install -r requirements.txt\n\nFROM python:3.12-slim\nRUN useradd --create-home --uid 10001 app\nCOPY --from=build /install /usr/local\nWORKDIR /app\nCOPY --chown=app:app . .\nUSER app\nEXPOSE 8000\nHEALTHCHECK CMD python -c "import urllib.request;urllib.request.urlopen(\'http://localhost:8000/health\')"\nCMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]' }, + { t: 'Cycle de vie d’un conteneur', d: '', html: '<code>docker run</code> crée et démarre un conteneur à partir d’une image. Les options essentielles : <code>-d</code> en arrière-plan, <code>--name</code> pour le nommer, <code>-p 8080:80</code> pour publier un port, <code>-e</code> pour une variable d’environnement, <code>-v</code> pour un volume, <code>--restart unless-stopped</code> pour le redémarrer automatiquement, <code>--memory</code> et <code>--cpus</code> pour le limiter.<br><br>Le processus lancé par CMD ou ENTRYPOINT est le <b>PID 1</b> du conteneur : quand il s’arrête, le conteneur s’arrête. C’est pourquoi on ne lance pas un service en arrière-plan dans un conteneur, et pourquoi le programme doit traiter correctement le signal SIGTERM envoyé par <code>docker stop</code> (au bout de 10 secondes, Docker envoie SIGKILL).<br><br>Un conteneur est <b>jetable</b> : on ne le modifie pas à la main, on le recrée à partir d’une nouvelle image. Tout ce qui est écrit dans sa couche inscriptible disparaît avec <code>docker rm</code>.' }, + { t: 'Persister les données : volumes et bind mounts', d: '', html: 'Puisque le conteneur est jetable, les données qui doivent survivre (base de données, fichiers envoyés, configuration) vont dans un <b>volume</b>. Un <b>volume nommé</b> est géré par Docker dans /var/lib/docker/volumes : c’est la solution recommandée pour les données d’application. Un <b>bind mount</b> monte un dossier précis de l’hôte : pratique pour la configuration ou le développement, mais il dépend de l’arborescence de l’hôte et de ses droits.<br><br>La sauvegarde ne se fait pas en copiant un volume de base de données à chaud : on utilise l’outil de la base (pg_dump, mariadb-dump) depuis le conteneur, ou on arrête le service le temps de la copie.', exemple: 'docker volume create pgdata\ndocker run -d --name db -e POSTGRES_PASSWORD_FILE=/run/secrets/pg -v pgdata:/var/lib/postgresql/data postgres:16\ndocker exec db pg_dump -U postgres app > app-$(date +%F).sql' }, + { t: 'Réseaux Docker', d: '', html: 'Par défaut, chaque conteneur est relié au réseau <b>bridge</b> docker0 et reçoit une adresse privée en 172.17.0.0/16. Pour être joignable de l’extérieur, un port doit être publié avec <code>-p</code> : Docker crée alors une règle de <b>DNAT</b> dans netfilter vers le conteneur.<br><br>Sur un <b>réseau bridge utilisateur</b> (<code>docker network create</code>), les conteneurs se trouvent par leur <b>nom</b> grâce au DNS intégré de Docker (127.0.0.11) : l’application joint la base de données par le nom db, sans connaître son adresse. C’est la pratique recommandée. Le mode <b>host</b> supprime l’isolation réseau : le conteneur utilise directement les interfaces de l’hôte.', attention: 'Les ports publiés par Docker passent par ses propres règles netfilter, avant celles d’un pare-feu comme UFW : un -p 5432:5432 expose la base de données sur Internet même si UFW semble la bloquer. On publie uniquement sur la boucle locale (-p 127.0.0.1:5432:5432) ou pas du tout, en utilisant un réseau interne.' }, + { t: 'Sécurité des conteneurs', d: '', html: 'Appartenir au groupe <b>docker</b> équivaut à être root sur l’hôte : on peut lancer un conteneur qui monte / et le modifier. On réserve ce groupe aux administrateurs, ou on utilise Docker en mode <b>rootless</b> ou Podman.<br><br>Les règles de base : images officielles ou construites en interne, <b>analyse de vulnérabilités</b> des images (Trivy, Docker Scout) dans la chaîne d’intégration, exécution en utilisateur non privilégié, jamais de <code>--privileged</code> sans raison impérative, secrets passés par fichiers ou gestionnaire de secrets plutôt qu’en variables d’environnement visibles dans <code>docker inspect</code>, et limites de ressources pour qu’un conteneur ne puisse pas saturer l’hôte.' } + ], + synthese: ['Conteneur = processus isolé par namespaces et limité par cgroups, noyau partagé', 'Image en couches, immuable, identifiée par tag et digest ; jamais latest en production', 'Dockerfile : base minimale versionnée, multi-étapes, USER non root, .dockerignore', 'PID 1 et SIGTERM : un processus au premier plan par conteneur', 'Données dans des volumes nommés, sauvegarde avec les outils de la base', 'Réseau bridge utilisateur et DNS par nom ; attention aux ports publiés qui contournent UFW', 'Groupe docker = root ; scanner les images, limiter les ressources'], + qcm: [ + ['Quels mécanismes du noyau isolent un conteneur ?', ['Hyperviseur et BIOS', 'Namespaces et cgroups', 'chroot et sudo', 'SELinux uniquement'], 1, 'Namespaces pour la vue, cgroups pour les ressources.'], + ['Pourquoi éviter le tag latest en production ?', ['Il est payant', 'Il peut désigner une nouvelle version à chaque pull', 'Il est lent', 'Il n’existe pas'], 1, 'Le déploiement devient non reproductible.'], + ['Quel est l’intérêt d’un build multi-étapes ?', ['Accélérer le réseau', 'Obtenir une image finale sans outils de compilation', 'Lancer plusieurs conteneurs', 'Chiffrer l’image'], 1, 'Image plus petite et surface d’attaque réduite.'], + ['Que se passe-t-il quand le processus PID 1 d’un conteneur se termine ?', ['Rien', 'Le conteneur s’arrête', 'Docker le relance toujours', 'L’hôte redémarre'], 1, 'Le conteneur vit tant que son processus principal vit.'], + ['Où stocker les données d’une base PostgreSQL conteneurisée ?', ['Dans la couche du conteneur', 'Dans un volume nommé', 'Dans l’image', 'Dans /tmp'], 1, 'Elles survivent à la suppression du conteneur.'], + ['Comment une application joint-elle la base de données sur un réseau bridge utilisateur ?', ['Par l’adresse 172.17.0.1', 'Par le nom du conteneur', 'Par localhost', 'Impossible'], 1, 'DNS intégré de Docker.'], + ['Pourquoi -p 5432:5432 peut-il exposer la base malgré UFW ?', ['UFW est désactivé par Docker', 'Docker insère ses règles DNAT avant celles d’UFW', 'Le port 5432 est public par nature', 'Ce n’est pas possible'], 1, 'Publier sur 127.0.0.1 ou ne pas publier.'], + ['Être membre du groupe docker revient à :', ['n’avoir aucun droit', 'être root sur l’hôte', 'pouvoir seulement lire les journaux', 'être administrateur réseau'], 1, 'On peut monter / dans un conteneur.'] + ], + td: [ + { n: 'Échauffement', c: 'Classer VM ou conteneur : (a) serveur Windows pour un logiciel métier, (b) API Python déployée dix fois par jour, (c) isolation forte entre deux clients hébergés, (d) base de tests jetable pour chaque élève.', r: '(a) VM · (b) conteneur · (c) VM (ou conteneurs dans des VM séparées) · (d) conteneur' }, + { n: 'Application', c: 'Réécrire ce Dockerfile pour la production : « FROM python:latest / COPY . /app / RUN pip install -r /app/requirements.txt / CMD python /app/app.py ».', r: 'Version fixée (python:3.12-slim), COPY requirements.txt avant le code pour le cache, pip --no-cache-dir, utilisateur non root et USER, .dockerignore, CMD en forme exec ["python","app.py"], éventuellement multi-étapes et HEALTHCHECK.' }, + { n: 'Application', c: 'Lancer Nginx accessible uniquement depuis l’hôte sur le port 8080, redémarré automatiquement, limité à 256 Mo, avec un dossier de configuration en lecture seule.', r: 'docker run -d --name web --restart unless-stopped --memory 256m -p 127.0.0.1:8080:80 -v /srv/web/conf:/etc/nginx/conf.d:ro nginx:1.27-alpine' }, + { n: 'Approfondissement', c: 'Montrer avec des commandes qu’un conteneur n’est qu’un processus de l’hôte, et retrouver son PID et ses cgroups.', r: 'docker inspect -f "{{.State.Pid}}" web → 4312\nps -fp 4312 · cat /proc/4312/cgroup · ls -l /proc/4312/ns' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Une VM Debian avec Docker : construire, publier, exploiter.', etapes: ['Construire l’image d’une API Python multi-étapes', 'La lancer avec limites de ressources et redémarrage automatique', 'Ajouter PostgreSQL sur un réseau utilisateur avec volume', 'Vérifier les ports réellement exposés', 'Scanner l’image avec Trivy'] }, + plusLoin: [['Docker Docs · Dockerfile best practices', 'https://docs.docker.com/build/building/best-practices/'], ['ANSSI · Recommandations de sécurité relatives au déploiement de conteneurs Docker', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-au-deploiement-de-conteneurs-docker'], ['Trivy', 'https://trivy.dev/']] }, +plus: { + cas: { titre: '', texte: '' }, + resolu: { enonce: 'L’application de réservation de salles, conteneurisée, redémarre en boucle depuis la mise à jour de ce matin. Les utilisateurs obtiennent une erreur 502 du reverse proxy. Diagnostiquer et rétablir.', etapes: [ + ['Constater l’état', 'docker ps -a --filter name=resa\nNAMES IMAGE STATUS\nresa resa-app:2.4.0 Restarting (1) 12 seconds ago\n→ Le conteneur démarre puis s’arrête avec le code 1, et la politique de redémarrage le relance sans fin. Le reverse proxy ne trouve donc jamais de backend : d’où les 502.'], + ['Lire les journaux', 'docker logs --tail 20 resa\nKeyError: \'DATABASE_URL\'\n→ L’application s’arrête au démarrage faute de la variable DATABASE_URL. La nouvelle version attend un nom de variable différent de l’ancienne (DB_URL).'], + ['Comparer avec la version précédente', 'docker inspect -f "{{range .Config.Env}}{{println .}}{{end}}" resa | grep -i db\nDB_URL=postgresql://resa@db/resa\n→ Le conteneur est lancé avec l’ancien nom de variable. Le changement était annoncé dans les notes de version 2.4.0, mais le fichier de déploiement n’a pas été adapté.'], + ['Rétablir le service', 'docker rm -f resa\ndocker run -d --name resa --network resa-net --restart unless-stopped -e DATABASE_URL=postgresql://resa@db/resa resa-app:2.4.0\ndocker ps --filter name=resa\nresa resa-app:2.4.0 Up 20 seconds (healthy)\n→ On ne corrige pas le conteneur existant : on le recrée avec la bonne configuration. Le HEALTHCHECK confirme que l’application répond.'], + ['Vérifier et prévenir', 'curl -sI https://resa.lycee.local | head -1\nHTTP/2 200\n→ Service rétabli. Actions préventives : décrire le déploiement dans un fichier Compose versionné plutôt qu’une commande docker run tapée à la main, relire les notes de version, et tester la nouvelle image sur un environnement de préproduction avant la production.'] + ] }, + erreurs: ['Utiliser latest en production', 'Lancer le processus en root dans l’image', 'Stocker des données dans la couche du conteneur', 'Modifier un conteneur à la main au lieu de reconstruire l’image', 'Publier une base de données sur 0.0.0.0', 'Mettre des secrets dans le Dockerfile ou l’image', 'Oublier .dockerignore et embarquer .git', 'Donner le groupe docker à des utilisateurs non administrateurs'], + memo: [['Commande', 'Usage en production'], ['docker ps -a', 'conteneurs et leur état, y compris arrêtés'], ['docker logs -f --tail 100 <c>', 'journaux de l’application'], ['docker inspect <c>', 'configuration complète : env, montages, réseau'], ['docker exec -it <c> sh', 'shell dans un conteneur en cours'], ['docker stats', 'CPU et mémoire consommés en direct'], ['docker build -t app:1.2.0 .', 'construire une image versionnée'], ['docker image ls · docker image prune', 'lister et nettoyer les images'], ['docker volume ls', 'volumes de données'], ['docker network ls · inspect', 'réseaux et conteneurs rattachés'], ['docker system df', 'espace disque utilisé par Docker'], ['docker run --rm -it image sh', 'explorer une image sans la garder']], + examen: { titre: 'Sujet type · 20 points', q: ['Comparer VM et conteneur en citant les mécanismes du noyau. (4 pts)', 'Critiquer et corriger un Dockerfile fourni. (6 pts)', 'Écrire la commande de lancement d’une application avec volume, réseau, limites et redémarrage. (4 pts)', 'Diagnostiquer un conteneur en redémarrage permanent à partir de sorties fournies. (4 pts)', 'Citer quatre mesures de sécurité des conteneurs. (2 pts)'], r: ['namespaces, cgroups, noyau partagé', 'version, cache, USER, multi-étapes, .dockerignore', 'docker run -d --name … --restart … --memory … -v … --network …', 'logs, inspect, code de sortie', 'non root, scan, pas de --privileged, secrets hors image'] }, + prof: { deroule: [['Ouverture', 'Démonstration : la même application qui échoue sur deux machines, puis qui marche partout en conteneur'], ['Cours', 'Namespaces, cgroups, images et couches'], ['Démonstration', 'Dockerfile naïf puis Dockerfile de production, comparaison de taille'], ['Pratique', 'Lab : API + base de données, volumes et réseau'], ['Incident', 'Conteneur en boucle de redémarrage'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : Dockerfile à compléter, fiche mémo. Élèves avancés : mode rootless, scan Trivy dans une CI, image distroless.', eval: 'Étapes du lab validées, analyse du Dockerfile, sujet type.' } +}, +exemples: [ + ['Pourquoi ce conteneur s’arrête-t-il ?', 'Un conteneur passe en Exited juste après son lancement.', 'docker ps -a --filter name=api --format "{{.Names}} {{.Status}}"\ndocker logs api | tail -3', 'api Exited (137) 2 minutes ago\nKilled', 'Le code 137 = 128 + 9 : le processus a reçu SIGKILL. Sans docker stop, c’est presque toujours le noyau qui l’a tué pour dépassement de mémoire (OOM). On le confirme avec docker inspect -f "{{.State.OOMKilled}}" api, puis on augmente --memory ou on corrige la fuite.'], + ['Mesurer la consommation', 'Le serveur rame depuis le déploiement.', 'docker stats --no-stream --format "table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}"', 'NAME CPU % MEM USAGE / LIMIT\nresa 0.4% 182MiB / 512MiB\nglpi 187.3% 1.1GiB / 15.6GiB\ndb 3.1% 420MiB / 1GiB', 'Le conteneur glpi consomme presque deux cœurs et n’a pas de limite mémoire (la limite affichée est la RAM de l’hôte). On regarde ses journaux et on lui fixe --cpus et --memory pour protéger les autres services.'], + ['Vérifier les ports réellement exposés', 'Audit avant mise en production.', 'docker ps --format "{{.Names}} {{.Ports}}"', 'web 0.0.0.0:443->443/tcp\ndb 0.0.0.0:5432->5432/tcp\nredis 127.0.0.1:6379->6379/tcp', 'La base PostgreSQL est publiée sur toutes les interfaces : joignable depuis Internet malgré le pare-feu de l’hôte. On la retire du -p et on la laisse sur le réseau interne ; Redis, publié sur 127.0.0.1, est correct.'], + ['Comprendre le cache de construction', 'Chaque build prend trois minutes.', 'docker build -t app:dev .', ' => CACHED [2/5] WORKDIR /app\n => [3/5] COPY . .\n => [4/5] RUN pip install -r requirements.txt 172.4s', 'COPY . . est placé avant l’installation des dépendances : toute modification du code invalide le cache et relance pip. En copiant requirements.txt seul, puis en installant, puis en copiant le code, le build passe à quelques secondes.'], + ['Retrouver un conteneur par son nom', 'L’application ne joint pas la base de données.', 'docker exec api getent hosts db ; docker network inspect -f "{{range .Containers}}{{.Name}} {{end}}" appnet', '(aucune sortie)\napi', 'Le nom db ne se résout pas, car le conteneur de base de données n’est pas sur le réseau appnet. On le rattache : docker network connect appnet db.'], + ['Libérer de l’espace disque', 'Le disque de l’hôte est plein à 95 %.', 'docker system df', 'TYPE TOTAL ACTIVE SIZE RECLAIMABLE\nImages 48 6 21.3GB 18.9GB (88%)\nContainers 9 6 1.2GB 0.4GB\nLocal Volumes 12 5 8.7GB 3.1GB\nBuild Cache — — 6.4GB 6.4GB', 'Les anciennes images et le cache de construction occupent 25 Go récupérables. docker image prune -a et docker builder prune les suppriment. On évite docker volume prune sans vérifier : il supprimerait des données.'] +], +termes: ['Docker', 'conteneur', 'image', 'Dockerfile', 'namespaces', 'cgroups', 'couche', 'tag', 'digest', 'registre', 'volume', 'bind mount', 'bridge', 'docker run', 'docker ps', 'docker logs', 'docker exec', 'docker inspect', 'docker stats', 'PID 1', 'SIGTERM', 'SIGKILL', 'multi-étapes', 'USER', 'HEALTHCHECK', 'latest', 'Trivy', 'rootless', 'OOM'] +}); + +reg({ +cours: { id: 'compose', match: /Docker Compose/i, + titre: 'Docker Compose : décrire et exploiter une application multi-conteneurs', duree: '', + niveau: 'BTS SIO SISR / SLAM · BUT R&T DevCloud · AIS', + prerequis: ['Conteneurs Docker'], + competences: ['BTS SIO · Bloc 2 · Déployer une solution applicative', 'BUT R&T DevCloud · Automatiser le déploiement', 'AIS · Industrialiser le déploiement'], + intro: 'Une application réelle ne tient jamais dans un seul conteneur : un site Laravel a besoin d’un serveur web, de PHP-FPM, d’une base de données, souvent d’un cache Redis et d’un planificateur de tâches. Lancer et relier tout cela à coups de docker run est fragile et impossible à reproduire. Docker Compose décrit l’ensemble dans un fichier YAML versionné : services, réseaux, volumes, variables, ordre de démarrage. Ce fichier devient la documentation exécutable du déploiement.', + cm: [ + { t: 'Le fichier compose.yaml', d: '', html: 'Le fichier <code>compose.yaml</code> (anciennement docker-compose.yml) déclare des <b>services</b>, chacun correspondant à un conteneur issu d’une image ou d’un Dockerfile, ainsi que des <b>volumes</b> et des <b>réseaux</b>. Compose crée automatiquement un réseau par projet : chaque service y est joignable par son nom.<br><br>La commande actuelle est <code>docker compose</code> (plugin intégré, version 2), qui remplace l’ancien binaire docker-compose écrit en Python.', exemple: 'services:\n web:\n image: nginx:1.27-alpine\n ports: ["443:443"]\n volumes: ["./nginx:/etc/nginx/conf.d:ro", "certs:/etc/ssl/app:ro"]\n depends_on: [app]\n app:\n build: .\n env_file: .env\n depends_on:\n db: { condition: service_healthy }\n db:\n image: postgres:16\n environment:\n POSTGRES_PASSWORD_FILE: /run/secrets/db_password\n secrets: [db_password]\n volumes: ["pgdata:/var/lib/postgresql/data"]\n healthcheck:\n test: ["CMD", "pg_isready", "-U", "postgres"]\n interval: 5s\n retries: 10\nvolumes:\n pgdata:\n certs:\nsecrets:\n db_password:\n file: ./secrets/db_password.txt' }, + { t: 'Ordre de démarrage et healthchecks', d: '', html: '<code>depends_on</code> seul garantit uniquement que le conteneur de la base est <b>démarré</b> avant l’application, pas qu’il est <b>prêt</b> à accepter des connexions. PostgreSQL met plusieurs secondes à s’initialiser au premier lancement, et l’application qui s’y connecte trop tôt échoue.<br><br>La bonne pratique combine un <b>healthcheck</b> sur la base et <code>condition: service_healthy</code> dans le depends_on de l’application. L’application doit aussi savoir réessayer sa connexion : en production, un redémarrage de la base peut survenir à tout moment.' }, + { t: 'Variables, fichiers .env et secrets', d: '', html: 'Compose lit automatiquement le fichier <code>.env</code> du dossier du projet pour substituer les variables <code>${VAR}</code> du fichier YAML, et <code>env_file</code> injecte des variables dans un conteneur. Ces fichiers ne doivent <b>jamais être versionnés</b> s’ils contiennent des mots de passe : on versionne un <code>.env.example</code> sans valeurs.<br><br>Les <b>secrets</b> de Compose montent un fichier en lecture seule dans <code>/run/secrets/</code> : le mot de passe n’apparaît ni dans les variables d’environnement, ni dans <code>docker inspect</code>. Les images officielles comme postgres ou mariadb acceptent les variables en <code>_FILE</code> pour lire leur valeur dans ce fichier.' }, + { t: 'Exploiter un projet Compose', d: '', html: '<code>docker compose up -d</code> crée ou met à jour tout le projet ; il ne recrée que les services dont la configuration ou l’image a changé. <code>docker compose pull</code> récupère les nouvelles images, <code>docker compose up -d --build</code> reconstruit celles qui ont un Dockerfile. <code>docker compose ps</code>, <code>logs -f service</code>, <code>exec service sh</code> et <code>down</code> complètent l’exploitation courante.<br><br><code>docker compose down</code> supprime conteneurs et réseau mais <b>conserve les volumes</b> ; <code>down -v</code> supprime aussi les volumes, donc les données. Cette option ne doit jamais être tapée par réflexe en production.', attention: 'docker compose down -v sur le serveur de production efface la base de données. Sauvegarder avant toute opération de maintenance, et réserver -v aux environnements de test.' }, + { t: 'Plusieurs environnements', d: '', html: 'Un même projet tourne en développement, en préproduction et en production avec des différences : ports, volumes de code montés, niveau de journalisation, nombre de répliques. Compose fusionne plusieurs fichiers : <code>compose.yaml</code> contient la base commune, <code>compose.override.yaml</code> (chargé automatiquement) les réglages de développement, et <code>compose.prod.yaml</code> ceux de production, appliqués avec <code>docker compose -f compose.yaml -f compose.prod.yaml up -d</code>. Les <b>profils</b> permettent d’activer des services optionnels (outils d’administration, tests).' }, + { t: 'Limites de Compose', d: '', html: 'Compose pilote les conteneurs d’<b>une seule machine</b>. Il ne répartit pas la charge entre plusieurs serveurs, ne déplace pas un service quand un nœud tombe, et ne gère pas les mises à jour sans interruption. Pour un établissement ou une PME, c’est souvent largement suffisant et bien plus simple à maintenir. Au-delà, on passe à un orchestrateur comme <b>Kubernetes</b>, dont les notions (services, volumes, secrets, sondes de santé) prolongent directement celles de Compose.' } + ], + synthese: ['compose.yaml versionné = documentation exécutable du déploiement', 'Réseau par projet, services joignables par leur nom', 'depends_on + healthcheck + condition service_healthy', '.env jamais versionné ; secrets montés dans /run/secrets', 'up -d ne recrée que ce qui a changé ; down -v efface les données', 'Plusieurs fichiers pour plusieurs environnements', 'Une seule machine : au-delà, Kubernetes'], + qcm: [ + ['Que garantit depends_on sans condition ?', ['Que la base accepte les connexions', 'Que le conteneur de la base est démarré avant', 'Que la base est sauvegardée', 'Rien'], 1, 'Démarré ne veut pas dire prêt.'], + ['Que fait docker compose down -v ?', ['Redémarre', 'Supprime conteneurs, réseau et volumes', 'Met à jour les images', 'Affiche la version'], 1, 'Les données sont effacées.'], + ['Comment le service app joint-il le service db ?', ['Par l’IP de l’hôte', 'Par le nom db', 'Par localhost', 'Par un port publié'], 1, 'DNS du réseau du projet.'], + ['Pourquoi préférer les secrets aux variables d’environnement pour un mot de passe ?', ['Plus rapide', 'Le mot de passe n’apparaît pas dans docker inspect', 'Obligatoire', 'Chiffrement automatique sur le disque'], 1, 'Fichier monté en lecture seule.'], + ['Quel fichier est chargé automatiquement en plus de compose.yaml ?', ['compose.prod.yaml', 'compose.override.yaml', 'docker.yaml', '.env.example'], 1, 'Réglages de développement.'], + ['Après modification de l’image d’un seul service, docker compose up -d :', ['recrée tous les services', 'recrée seulement ce service', 'ne fait rien', 'supprime les volumes'], 1, 'Mise à jour ciblée.'] + ], + td: [ + { n: 'Application', c: 'Écrire le compose.yaml d’un GLPI : service web (image diouxx/glpi ou équivalent), service MariaDB avec volume et healthcheck, mot de passe en secret, seul le web publié sur 443.', r: 'services: web (ports 443, depends_on db service_healthy) ; db (mariadb:11, MARIADB_ROOT_PASSWORD_FILE, secrets, volume dbdata, healthcheck healthcheck.sh --connect) ; volumes: dbdata ; secrets: db_root.' }, + { n: 'Application', c: 'Écrire la procédure de mise à jour d’un projet Compose en production sans perte de données.', r: '1. Sauvegarde (docker compose exec db pg_dump …) · 2. docker compose pull · 3. docker compose up -d · 4. docker compose ps et logs · 5. test fonctionnel · 6. en cas d’échec : revenir au tag précédent dans compose.yaml puis up -d.' }, + { n: 'Approfondissement', c: 'Séparer la configuration de développement (code monté, port 8000 publié, débogage) de celle de production (image construite, pas de port applicatif publié).', r: 'compose.yaml : base commune · compose.override.yaml : volumes ./:/app, ports 8000, APP_DEBUG=1 · compose.prod.yaml : image registre:tag, restart unless-stopped, pas de ports sur app · lancement prod : -f compose.yaml -f compose.prod.yaml.' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Déployer une application web, sa base et son cache avec Compose.', etapes: ['Écrire compose.yaml avec trois services', 'Ajouter healthcheck et secrets', 'Démarrer, vérifier ps et logs', 'Mettre à jour une image sans perte de données', 'Sauvegarder et restaurer la base'] }, + plusLoin: [['Docker Docs · Compose file reference', 'https://docs.docker.com/reference/compose-file/'], ['Docker Docs · Secrets in Compose', 'https://docs.docker.com/compose/how-tos/use-secrets/']] }, +plus: { + cas: { titre: '', texte: '' }, + resolu: { enonce: 'Après un redémarrage du serveur, l’application Laravel du CDI affiche « SQLSTATE[HY000] [2002] Connection refused » pendant plusieurs minutes, puis parfois reste en erreur. Diagnostiquer et fiabiliser.', etapes: [ + ['Observer l’ordre de démarrage', 'docker compose ps\nNAME SERVICE STATUS\ncdi-app app Up 3 minutes\ncdi-db db Up 3 minutes (health: starting)\n→ L’application tourne alors que la base n’est pas encore prête.'], + ['Lire les journaux', 'docker compose logs app | head -3\nSQLSTATE[HY000] [2002] Connection refused\ndocker compose logs db | tail -2\nInnoDB: Starting crash recovery...\nready for connections.\n→ Après un arrêt brutal, MariaDB effectue une récupération qui prend du temps. L’application a tenté de se connecter trop tôt et a mis la base en échec dans son cache de configuration.'], + ['Lire le fichier Compose', 'grep -A3 depends_on compose.yaml\n depends_on:\n - db\n→ depends_on sans condition : Compose attend seulement que le conteneur db soit lancé.'], + ['Corriger', 'db:\n healthcheck:\n test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]\n interval: 5s\n retries: 30\napp:\n depends_on:\n db: { condition: service_healthy }\n restart: unless-stopped\ndocker compose up -d\n→ L’application ne démarre plus qu’une fois la base réellement prête, et redémarre automatiquement en cas d’échec.'], + ['Valider', 'sudo reboot (sur la préproduction)\ndocker compose ps\ncdi-db Up 2 minutes (healthy)\ncdi-app Up 1 minute\n→ L’ordre est respecté. Compte rendu : cause (depends_on sans healthcheck), correction, test de redémarrage ajouté à la procédure de validation.'] + ] }, + erreurs: ['depends_on sans healthcheck', 'Versionner le fichier .env avec les mots de passe', 'Taper docker compose down -v en production', 'Publier les ports de la base de données', 'Ne pas fixer les versions des images', 'Mettre à jour sans sauvegarde préalable'], + memo: [['Commande', 'Usage'], ['docker compose up -d', 'créer ou mettre à jour le projet'], ['docker compose ps', 'état et santé des services'], ['docker compose logs -f <s>', 'journaux d’un service'], ['docker compose exec <s> sh', 'shell dans un service'], ['docker compose pull', 'récupérer les nouvelles images'], ['docker compose up -d --build', 'reconstruire et redéployer'], ['docker compose config', 'afficher la configuration fusionnée et validée'], ['docker compose down', 'arrêter en conservant les volumes'], ['docker compose -f a.yaml -f b.yaml up -d', 'combiner des fichiers d’environnement'], ['docker compose --profile outils up -d', 'activer des services optionnels']], + examen: { titre: 'Sujet type · 20 points', q: ['Écrire le compose.yaml d’une application trois tiers. (8 pts)', 'Expliquer depends_on et les healthchecks. (4 pts)', 'Rédiger la procédure de mise à jour sans perte de données. (5 pts)', 'Justifier le passage à Kubernetes pour un cas donné. (3 pts)'], r: ['services, volumes, secrets, réseau', 'démarré ≠ prêt, condition service_healthy', 'sauvegarde, pull, up -d, contrôle, retour arrière', 'plusieurs nœuds, haute disponibilité'] }, + prof: { deroule: [['Ouverture', 'Lancer à la main trois conteneurs reliés, puis la même chose en un fichier'], ['Cours', 'Services, réseaux, volumes, secrets'], ['Pratique', 'Lab application trois tiers'], ['Incident', 'Ordre de démarrage au redémarrage'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : compose.yaml à compléter. Élèves avancés : plusieurs environnements, profils, déploiement par CI.', eval: 'Lab validé, sujet type.' } +}, +exemples: [ + ['Valider le fichier avant déploiement', 'Une modification a été faite dans compose.yaml.', 'docker compose config --quiet && echo OK', 'services.app.depends_on.db.condition must be one of the following: "service_started", "service_healthy", "service_completed_successfully"', 'Faute de frappe dans la condition. docker compose config valide et fusionne les fichiers sans rien lancer : à exécuter avant chaque up.'], + ['Voir la santé des services', 'L’application répond lentement au démarrage.', 'docker compose ps --format "table {{.Service}}\\t{{.Status}}"', 'SERVICE STATUS\ndb Up 40 seconds (healthy)\nredis Up 40 seconds\napp Up 12 seconds (health: starting)', 'La base est prête, l’application exécute encore son healthcheck. Tant qu’elle n’est pas healthy, le reverse proxy peut renvoyer des erreurs : c’est normal pendant quelques secondes.'], + ['Mettre à jour une seule image', 'Correctif de sécurité publié pour Nginx.', 'docker compose pull web && docker compose up -d web', '[+] Pulling web … Pulled\n[+] Running 1/1 ✔ Container cdi-web Recreated', 'Seul le service web est recréé ; la base et l’application continuent de tourner.'], + ['Sauvegarder la base d’un projet', 'Avant une mise à jour.', 'docker compose exec -T db pg_dump -U app app | gzip > backup-$(date +%F).sql.gz ; ls -lh backup-*', '-rw-r--r-- 1 admin admin 14M sept. 30 07:02 backup-2026-09-30.sql.gz', '-T désactive le pseudo-terminal pour que la sortie puisse être redirigée proprement dans un fichier.'], + ['Retrouver le nom réel des ressources', 'Un collègue cherche le volume de la base.', 'docker volume ls --filter label=com.docker.compose.project=cdi', 'DRIVER VOLUME NAME\nlocal cdi_pgdata', 'Compose préfixe les volumes et réseaux par le nom du projet (le dossier). Deux projets peuvent donc avoir chacun un volume pgdata sans conflit.'], + ['Afficher les journaux de plusieurs services', 'Erreur 500 intermittente.', 'docker compose logs --since 10m app web | grep -E " 500 |ERROR"', 'cdi-web | "POST /reservations HTTP/1.1" 500\ncdi-app | ERROR: duplicate key value violates unique constraint "resa_salle_creneau"', 'En croisant les journaux, on relie l’erreur 500 vue par Nginx à une violation de contrainte dans l’application : deux réservations simultanées du même créneau. C’est un bug applicatif, pas une panne d’infrastructure.'] +], +termes: ['Docker Compose', 'compose.yaml', 'service', 'depends_on', 'healthcheck', 'service_healthy', 'secrets', '.env', 'env_file', 'docker compose up', 'docker compose down', 'docker compose ps', 'docker compose logs', 'docker compose config', 'profils', 'volume', 'Kubernetes'] +}); + +reg({ +cours: { id: 'ansible', match: /Ansible|IaC|infrastructure as code/i, + titre: 'Ansible : automatiser la configuration des serveurs', duree: '', + niveau: 'BTS SIO SISR · BUT R&T DevCloud · AIS · Master', + prerequis: ['Le terminal Linux', 'SSH par clé', 'Paquets et services'], + competences: ['BTS SIO SISR · Bloc 2 · Automatiser les tâches d’administration', 'BUT R&T DevCloud · Automatiser le déploiement d’une infrastructure', 'AIS · Industrialiser l’administration'], + intro: 'Configurer un serveur à la main prend une heure ; en configurer quarante de façon identique est impossible sans erreur, et personne ne sait plus, six mois après, ce qui a été fait sur lequel. Ansible décrit l’état attendu des machines dans des fichiers YAML versionnés, et l’applique par SSH, sans agent à installer. Relancé autant de fois qu’on veut, il ne change que ce qui n’est pas conforme. C’est l’outil d’automatisation le plus répandu dans les services informatiques, des établissements scolaires aux grands hébergeurs.', + cm: [ + { t: 'Principe : décrire un état, pas des commandes', d: '', html: 'Un script Bash enchaîne des actions : « installe Nginx, copie ce fichier, redémarre ». Lancé deux fois, il peut échouer ou dupliquer une ligne. Ansible décrit un <b>état désiré</b> : « Nginx doit être installé, ce fichier doit avoir ce contenu, le service doit être actif ». Chaque <b>module</b> vérifie l’état actuel et n’agit que si nécessaire : c’est l’<b>idempotence</b>. Un playbook relancé sur une machine déjà conforme affiche <code>changed=0</code>.<br><br>Ansible est <b>sans agent</b> : le poste de contrôle se connecte en SSH, envoie un petit programme Python, l’exécute et le supprime. Il suffit d’un accès SSH et de Python sur les machines gérées.' }, + { t: 'L’inventaire', d: '', html: 'L’<b>inventaire</b> liste les machines gérées et les range en <b>groupes</b>. Il peut être statique (fichier INI ou YAML) ou dynamique (généré depuis un hyperviseur ou un fournisseur cloud). Les <b>variables</b> se placent dans <code>group_vars/</code> et <code>host_vars/</code>, ce qui sépare les données (quelle machine, quelle valeur) de la logique (les tâches).', exemple: '# inventaire/hosts.yaml\nall:\n children:\n web:\n hosts:\n web1.lycee.local:\n web2.lycee.local:\n db:\n hosts:\n db1.lycee.local:\n ansible_host: 10.62.40.31\n vars:\n ansible_user: admin\n\n$ ansible -i inventaire all -m ping\nweb1.lycee.local | SUCCESS => {"changed": false, "ping": "pong"}' }, + { t: 'Playbooks, tâches et modules', d: '', html: 'Un <b>playbook</b> est une liste de <b>plays</b> : chacun associe un groupe de machines à une liste de <b>tâches</b>. Chaque tâche appelle un <b>module</b> avec des paramètres : <code>ansible.builtin.apt</code> pour les paquets, <code>template</code> pour générer un fichier de configuration, <code>service</code> ou <code>systemd</code> pour les services, <code>user</code>, <code>lineinfile</code>, <code>copy</code>, <code>file</code>. On utilise les noms complets (FQCN) pour éviter toute ambiguïté.<br><br><code>become: true</code> élève les privilèges avec sudo. Les <b>handlers</b> sont des tâches déclenchées par <code>notify</code> seulement si une tâche a réellement changé quelque chose : on ne redémarre Nginx que si sa configuration a été modifiée.', exemple: '- name: Serveurs web\n hosts: web\n become: true\n tasks:\n - name: Installer nginx\n ansible.builtin.apt:\n name: nginx\n state: present\n update_cache: true\n - name: Déployer la configuration du site\n ansible.builtin.template:\n src: site.conf.j2\n dest: /etc/nginx/conf.d/site.conf\n mode: "0644"\n notify: Recharger nginx\n - name: Nginx actif au démarrage\n ansible.builtin.service:\n name: nginx\n state: started\n enabled: true\n handlers:\n - name: Recharger nginx\n ansible.builtin.service:\n name: nginx\n state: reloaded' }, + { t: 'Templates Jinja2 et variables', d: '', html: 'Le module <code>template</code> génère un fichier à partir d’un modèle <b>Jinja2</b> : <code>{{ variable }}</code> insère une valeur, <code>{% for %}</code> et <code>{% if %}</code> ajoutent de la logique. La même configuration sert ainsi à toutes les machines, avec leurs valeurs propres. Les <b>facts</b> collectés automatiquement (adresse IP, distribution, mémoire) sont aussi disponibles comme variables.<br><br>Les variables ont un ordre de <b>priorité</b> : valeurs par défaut d’un rôle, group_vars, host_vars, variables du playbook, puis <code>-e</code> en ligne de commande, qui l’emporte sur tout. Mal maîtrisé, cet ordre explique des valeurs « inattendues ».' }, + { t: 'Rôles et collections', d: '', html: 'Au-delà de quelques dizaines de tâches, on range le code en <b>rôles</b> : une arborescence normalisée (tasks, handlers, templates, defaults, vars, files) qui regroupe tout ce qu’il faut pour une fonction, par exemple « serveur web » ou « durcissement SSH ». Un rôle se réutilise dans plusieurs playbooks et se partage. Les <b>collections</b> distribuent rôles et modules (community.general, ansible.posix) et s’installent avec <code>ansible-galaxy</code>.' }, + { t: 'Tester avant d’appliquer et protéger les secrets', d: '', html: '<code>--check</code> simule l’exécution sans rien modifier, <code>--diff</code> montre les différences de fichiers, <code>--limit web1</code> restreint à une machine, <code>--tags</code> à certaines tâches. En production, on valide toujours sur une machine avant tout le parc.<br><br>Les mots de passe et clés ne doivent pas être en clair dans le dépôt Git. <b>Ansible Vault</b> chiffre un fichier de variables avec AES-256 ; le mot de passe du coffre est fourni à l’exécution. <code>ansible-lint</code> détecte les mauvaises pratiques (tâches non nommées, modules shell là où un module dédié existe, droits de fichiers absents).', attention: 'Les modules command et shell ne sont pas idempotents par nature : ils s’exécutent à chaque passage. Préférer un module dédié, ou ajouter creates, removes ou changed_when pour décrire l’état.' } + ], + synthese: ['Ansible décrit un état désiré ; les modules sont idempotents', 'Sans agent : SSH et Python sur les machines gérées', 'Inventaire en groupes, variables dans group_vars et host_vars', 'Playbook → plays → tâches → modules ; become pour sudo', 'Handlers déclenchés uniquement en cas de changement', 'Templates Jinja2, rôles réutilisables', '--check --diff et --limit avant d’appliquer ; Vault pour les secrets'], + qcm: [ + ['Que signifie qu’une tâche Ansible est idempotente ?', ['Elle est rapide', 'La relancer sur une machine conforme ne change rien', 'Elle s’exécute en parallèle', 'Elle est chiffrée'], 1, 'changed=0 au second passage.'], + ['De quoi Ansible a-t-il besoin sur les machines gérées ?', ['Un agent Ansible', 'SSH et Python', 'Docker', 'Un serveur web'], 1, 'Architecture sans agent.'], + ['Quand un handler s’exécute-t-il ?', ['À chaque tâche', 'Quand une tâche qui le notifie a produit un changement', 'Au début du playbook', 'Jamais automatiquement'], 1, 'Par exemple recharger Nginx seulement si sa configuration a changé.'], + ['Quelle option simule sans modifier ?', ['--dry', '--check', '--test', '--simulate'], 1, 'Souvent combinée avec --diff.'], + ['Où placer une variable commune à tous les serveurs web ?', ['host_vars/web1.yaml', 'group_vars/web.yaml', 'dans le template', 'dans /etc/ansible/ansible.cfg'], 1, 'Variables de groupe.'], + ['Comment protéger un mot de passe dans le dépôt ?', ['Le mettre en commentaire', 'Ansible Vault', 'Le coder en base64', 'Le mettre dans le template'], 1, 'Chiffrement AES-256.'], + ['Pourquoi éviter le module shell pour installer un paquet ?', ['Il est interdit', 'Il n’est pas idempotent et masque l’état réel', 'Il est plus lent', 'Il ne supporte pas sudo'], 1, 'Utiliser apt ou dnf.'] + ], + td: [ + { n: 'Application', c: 'Écrire un playbook qui crée l’utilisateur technicien, lui dépose une clé SSH publique, désactive l’authentification SSH par mot de passe et recharge sshd seulement si nécessaire.', r: 'user (name, groups: sudo, shell) · ansible.posix.authorized_key · lineinfile sur /etc/ssh/sshd_config avec regexp "^#?PasswordAuthentication" line "PasswordAuthentication no" validate "sshd -t -f %s" notify Recharger ssh · handler service ssh reloaded.' }, + { n: 'Application', c: 'Transformer ce passage en tâche idempotente : « shell: echo "10.62.40.20 ent" >> /etc/hosts ».', r: 'ansible.builtin.lineinfile: path=/etc/hosts line="10.62.40.20 ent" (la ligne n’est ajoutée qu’une fois).' }, + { n: 'Approfondissement', c: 'Organiser en rôle le déploiement d’un serveur web : arborescence, variables par défaut, template et handler.', r: 'roles/web/{tasks/main.yml, handlers/main.yml, templates/site.conf.j2, defaults/main.yml (web_port: 443, web_root: /var/www/site)} · playbook : roles: [web].' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un poste de contrôle et trois VM Debian à configurer.', etapes: ['Écrire l’inventaire et tester ansible -m ping', 'Playbook : paquets et utilisateurs', 'Déployer Nginx avec un template et un handler', 'Vérifier l’idempotence (changed=0)', 'Chiffrer un secret avec Vault', 'Passer ansible-lint'] }, + plusLoin: [['Documentation Ansible', 'https://docs.ansible.com/'], ['Ansible Lint', 'https://ansible.readthedocs.io/projects/lint/'], ['Xavki · tutoriels Ansible en français', 'https://www.youtube.com/@xavki']] }, +plus: { + cas: { titre: '', texte: '' }, + resolu: { enonce: 'Le playbook de mise à jour des 12 serveurs web s’est interrompu cette nuit : 4 serveurs ont la nouvelle configuration Nginx, 8 l’ancienne, et web3 ne répond plus. Diagnostiquer, rétablir et fiabiliser.', etapes: [ + ['Lire le récapitulatif', 'PLAY RECAP\nweb1 ok=6 changed=2 failed=0\nweb3 ok=4 changed=1 failed=1\nweb4…web12 (non traités)\n→ Ansible s’est arrêté après l’échec sur web3 : par défaut, un play traite les machines par lots et s’arrête si toutes celles du lot échouent. L’état du parc est hétérogène.'], + ['Identifier l’erreur', 'TASK [Recharger nginx] fatal: [web3]: FAILED! => {"msg": "Unable to reload service nginx: Job for nginx.service failed"}\nssh web3 sudo nginx -t\nnginx: [emerg] unknown directive "ssl_protocol" in /etc/nginx/conf.d/site.conf:7\n→ Le template contient une faute (ssl_protocol au lieu de ssl_protocols). La configuration invalide a été déposée, le rechargement a échoué et Nginx s’est arrêté sur web3.'], + ['Rétablir web3', 'Correction du template site.conf.j2 (ssl_protocols TLSv1.2 TLSv1.3;)\nansible-playbook site.yml --limit web3 --diff\nweb3 ok=6 changed=2 failed=0\ncurl -sI https://web3.lycee.local | head -1\nHTTP/2 200\n→ On corrige la source (le template), jamais le fichier à la main sur le serveur : sinon le prochain passage réintroduirait l’erreur.'], + ['Fiabiliser le playbook', '- ansible.builtin.template:\n src: site.conf.j2\n dest: /etc/nginx/conf.d/site.conf\n validate: nginx -t -c /etc/nginx/nginx.conf\n notify: Recharger nginx\n- hosts: web\n serial: 2\n max_fail_percentage: 0\n→ validate teste le fichier avant de le mettre en place ; serial: 2 déploie deux serveurs à la fois et arrête tout au premier échec. Une erreur ne touche plus qu’un lot.'], + ['Appliquer au parc et contrôler', 'ansible-playbook site.yml --check --diff --limit web4\nansible-playbook site.yml\nweb1…web12 failed=0\nansible-playbook site.yml (second passage)\nweb1…web12 changed=0\n→ Le parc est homogène et le second passage confirme l’idempotence. Compte rendu : cause, impact, correction, et règle d’équipe : --check sur une machine témoin avant tout déploiement, validation des templates dans la CI avec ansible-lint.'] + ] }, + erreurs: ['Utiliser shell ou command là où un module dédié existe', 'Modifier un fichier à la main sur un serveur géré par Ansible', 'Laisser des mots de passe en clair dans le dépôt', 'Déployer sur tout le parc sans --check ni serial', 'Redémarrer un service à chaque passage au lieu d’un handler', 'Déposer une configuration sans validate'], + memo: [['Commande', 'Usage'], ['ansible all -m ping', 'tester la connexion à tout l’inventaire'], ['ansible web -m ansible.builtin.setup', 'afficher les facts d’un groupe'], ['ansible-playbook site.yml --check --diff', 'simuler et voir les différences'], ['ansible-playbook site.yml --limit web1', 'restreindre à une machine'], ['ansible-playbook site.yml --tags nginx', 'n’exécuter que certaines tâches'], ['ansible-inventory --graph', 'visualiser l’inventaire'], ['ansible-vault create|edit|encrypt', 'gérer les secrets chiffrés'], ['ansible-playbook … --ask-vault-pass', 'fournir le mot de passe du coffre'], ['ansible-galaxy collection install', 'installer une collection'], ['ansible-lint', 'contrôler les bonnes pratiques']], + examen: { titre: 'Sujet type · 20 points', q: ['Expliquer l’idempotence et l’architecture sans agent. (4 pts)', 'Écrire un inventaire et un playbook de déploiement avec template et handler. (8 pts)', 'Rendre idempotentes trois tâches shell fournies. (4 pts)', 'Proposer une stratégie de déploiement sûr sur 50 serveurs. (4 pts)'], r: ['État désiré, SSH + Python', 'hosts.yaml, apt, template, service, handler', 'lineinfile, apt, creates', '--check, serial, validate, max_fail_percentage'] }, + prof: { deroule: [['Ouverture', 'Configurer trois VM à la main en chronométrant, puis avec Ansible'], ['Cours', 'Inventaire, playbooks, modules, idempotence'], ['Pratique', 'Lab trois VM'], ['Incident', 'Template invalide sur un parc'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : playbook à compléter. Élèves avancés : rôles, Vault, CI avec ansible-lint, Molecule.', eval: 'Lab validé (idempotence vérifiée), sujet type.' } +}, +exemples: [ + ['Tester l’accès à tout le parc', 'Avant un déploiement.', 'ansible -i inventaire all -m ping -o', 'web1.lycee.local | SUCCESS => {"changed": false,"ping": "pong"}\nweb2.lycee.local | UNREACHABLE! => {"msg": "Failed to connect to the host via ssh: Connection timed out"}', 'web2 est injoignable en SSH : on le traite avant de lancer le playbook, sinon le parc sera partiellement configuré.'], + ['Simuler un changement', 'Nouvelle version du template Nginx.', 'ansible-playbook site.yml --check --diff --limit web1', '--- before: /etc/nginx/conf.d/site.conf\n+++ after: site.conf.j2\n- client_max_body_size 8m;\n+ client_max_body_size 64m;\nweb1 : ok=5 changed=1 failed=0', 'Seule la ligne attendue change. On peut appliquer sereinement.'], + ['Vérifier l’idempotence', 'Contrôle qualité d’un playbook.', 'ansible-playbook site.yml | tail -3', 'PLAY RECAP\nweb1 : ok=7 changed=1 unreachable=0 failed=0\nweb2 : ok=7 changed=1 unreachable=0 failed=0', 'Au second passage, une tâche est encore « changed » : c’est une tâche shell sans changed_when. Il faut la remplacer par un module dédié ou décrire sa condition de changement.'], + ['Lire les facts d’une machine', 'Adapter un template à la mémoire disponible.', 'ansible db1 -m ansible.builtin.setup -a "filter=ansible_memtotal_mb"', 'db1 | SUCCESS => {"ansible_facts": {"ansible_memtotal_mb": 7957}}', 'La variable ansible_memtotal_mb est utilisable dans un template, par exemple pour dimensionner shared_buffers de PostgreSQL au quart de la mémoire.'], + ['Chiffrer un secret', 'Mot de passe de la base dans group_vars.', 'ansible-vault encrypt group_vars/db/vault.yml ; head -1 group_vars/db/vault.yml', 'Encryption successful\n$ANSIBLE_VAULT;1.1;AES256', 'Le fichier peut être versionné : sans le mot de passe du coffre, il est illisible.'], + ['Détecter les mauvaises pratiques', 'Revue de code avant fusion.', 'ansible-lint site.yml', 'command-instead-of-module: apt-get used in place of apt module\nsite.yml:14 Task/Handler: shell apt-get install -y nginx\nname[missing]: All tasks should be named\nsite.yml:22', 'ansible-lint signale une installation via shell et une tâche sans nom. On l’intègre dans la CI pour bloquer ces erreurs avant qu’elles atteignent la production.'] +], +termes: ['Ansible', 'playbook', 'inventaire', 'module', 'tâche', 'handler', 'notify', 'idempotence', 'become', 'group_vars', 'host_vars', 'Jinja2', 'template', 'facts', 'rôle', 'collection', 'ansible-galaxy', 'Ansible Vault', 'ansible-lint', '--check', '--diff', '--limit', 'serial', 'validate', 'changed=0'] +}); +})(); diff --git a/portail/public/js/contenus-devops-2.js b/portail/public/js/contenus-devops-2.js new file mode 100644 index 0000000..7770712 --- /dev/null +++ b/portail/public/js/contenus-devops-2.js @@ -0,0 +1,122 @@ +// Cours complets · DevOps lot 2 : Git, CI/CD +(function () { +var reg = function (c) { + (window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours); + (window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus; + (window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples; + (window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] }; +}; + +reg({ +cours: { id: 'git', match: /\bGit\b|versionn|gestion de versions/i, + titre: 'Git : versionner, collaborer et revenir en arrière', duree: '', + niveau: 'NSI Terminale · BTS SIO SLAM / SISR · BUT Informatique / R&T · Master', + prerequis: ['Le terminal Linux'], + competences: ['BTS SIO · Bloc 1 · Travailler en mode projet', 'BTS SIO SLAM · Bloc 2 · Gérer les versions d’une solution applicative', 'BUT Info · Compétence « Collaborer au sein d’une équipe informatique »'], + intro: 'Un fichier « rapport_final_v3_corrigé_VRAIMENT_final.docx » résume le problème que Git résout. Dès que plusieurs personnes modifient le même code, ou qu’une seule personne veut pouvoir revenir à la version d’hier, il faut un historique fiable. Git enregistre chaque état du projet, permet de travailler en parallèle sur des branches et de fusionner le travail de chacun. C’est l’outil commun à tous les développeurs et, de plus en plus, aux administrateurs qui versionnent leurs configurations, leurs playbooks et leurs fichiers Compose.', + cm: [ + { t: 'Le modèle de Git : instantanés et empreintes', d: '', html: 'Git ne stocke pas des différences mais des <b>instantanés</b> complets du projet. Chaque fichier, chaque dossier et chaque <b>commit</b> est un objet identifié par l’empreinte de son contenu (SHA-1, en transition vers SHA-256). Deux fichiers identiques ne sont stockés qu’une fois, et toute modification de l’historique change les empreintes : l’intégrité est vérifiable.<br><br>Un commit contient un instantané, un auteur, une date, un message et un lien vers son ou ses <b>parents</b>. L’historique est donc un graphe orienté de commits. Une <b>branche</b> n’est qu’un pointeur mobile vers un commit, et <b>HEAD</b> désigne la branche ou le commit sur lequel on travaille.' }, + { t: 'Les trois zones', d: '', html: 'Un fichier passe par trois zones. Le <b>répertoire de travail</b> contient les fichiers tels qu’on les édite. L’<b>index</b> (staging area) contient ce qui sera enregistré au prochain commit : on y ajoute les modifications avec <code>git add</code>. Le <b>dépôt</b> (.git) contient l’historique, et <code>git commit</code> y enregistre l’index.<br><br>Cette étape intermédiaire permet de faire des commits cohérents : on n’ajoute que les modifications qui vont ensemble, même si on a travaillé sur plusieurs sujets à la fois. <code>git status</code> montre à tout moment l’état des trois zones.', exemple: 'git init\ngit add index.html style.css\ngit commit -m "Page d’accueil du TP"\ngit status\ngit log --oneline --graph' }, + { t: 'Branches et fusions', d: '', html: 'On crée une branche pour chaque fonctionnalité ou correction (<code>git switch -c correctif-login</code>). La branche principale (main) reste stable. Quand le travail est terminé, on le <b>fusionne</b>. Si main n’a pas bougé, Git avance simplement le pointeur (<b>fast-forward</b>). Sinon, il crée un <b>commit de fusion</b> qui a deux parents.<br><br>Si les deux branches ont modifié les mêmes lignes, Git ne peut pas choisir : c’est un <b>conflit</b>. Il marque les zones concernées dans le fichier (<code><<<<<<<</code>, <code>=======</code>, <code>>>>>>>></code>). On édite pour garder la bonne version, on ajoute le fichier et on termine le commit.<br><br>Le <b>rebase</b> est l’alternative à la fusion : il rejoue les commits d’une branche au-dessus d’une autre, pour un historique linéaire. Il réécrit les commits, donc on ne rebase jamais une branche déjà partagée.' }, + { t: 'Travailler avec un dépôt distant', d: '', html: 'Un <b>dépôt distant</b> (GitHub, GitLab, Forgejo auto-hébergé) sert de point de rencontre. <code>git clone</code> copie un dépôt, <code>git fetch</code> récupère les nouveautés sans les appliquer, <code>git pull</code> récupère et fusionne, <code>git push</code> publie ses commits. Les branches distantes apparaissent sous le nom <code>origin/main</code>.<br><br>En équipe, on ne pousse pas directement sur main : on pousse une branche et on ouvre une <b>demande de fusion</b> (pull request ou merge request), relue par un collègue et validée par les tests automatiques avant d’être intégrée.', attention: 'Un push refusé (« rejected, non-fast-forward ») signifie que le dépôt distant a des commits que vous n’avez pas. On fait git pull (ou fetch puis rebase), jamais git push --force sur une branche partagée : on effacerait le travail des autres.' }, + { t: 'Revenir en arrière', d: '', html: 'Git offre plusieurs façons d’annuler, selon ce qu’on veut défaire. <code>git restore fichier</code> annule les modifications non ajoutées. <code>git restore --staged fichier</code> retire un fichier de l’index. <code>git commit --amend</code> corrige le dernier commit s’il n’a pas été poussé. <code>git revert id</code> crée un nouveau commit qui annule un commit précédent : c’est la seule méthode sûre sur un historique partagé. <code>git reset</code> déplace la branche en arrière et réécrit l’historique local.<br><br>Même après une erreur, rien n’est perdu tout de suite : <code>git reflog</code> liste les positions successives de HEAD et permet de retrouver un commit « supprimé ».' }, + { t: 'Ce qui ne va pas dans Git', d: '', html: 'Le fichier <code>.gitignore</code> exclut ce qui ne doit pas être versionné : dépendances (node_modules, vendor), fichiers générés, journaux, et surtout <b>secrets</b> (.env, clés). Un mot de passe poussé une fois reste dans l’historique même après suppression du fichier : il faut le considérer comme compromis et le changer.<br><br>Un bon <b>message de commit</b> décrit pourquoi, pas seulement quoi : « Corrige le calcul de la moyenne quand un élève est absent » plutôt que « fix ». Beaucoup d’équipes adoptent la convention Conventional Commits (feat:, fix:, docs:).' } + ], + synthese: ['Git stocke des instantanés identifiés par empreinte ; une branche est un pointeur', 'Répertoire de travail → index (add) → dépôt (commit)', 'Une branche par fonctionnalité, fusion ou rebase ; conflit = même ligne modifiée des deux côtés', 'fetch, pull, push ; demande de fusion relue avant main', 'revert sur l’historique partagé, reset seulement en local, reflog pour retrouver', '.gitignore et aucun secret dans l’historique'], + qcm: [ + ['Que contient l’index ?', ['L’historique', 'Ce qui sera enregistré au prochain commit', 'Les branches distantes', 'Les fichiers ignorés'], 1, 'On y ajoute avec git add.'], + ['Qu’est-ce qu’une branche pour Git ?', ['Une copie du projet', 'Un pointeur mobile vers un commit', 'Un dossier', 'Un dépôt distant'], 1, 'Créer une branche est instantané.'], + ['Quand survient un conflit de fusion ?', ['Toujours', 'Quand les deux branches ont modifié les mêmes lignes', 'Quand on oublie git add', 'Quand le dépôt est trop gros'], 1, 'Git ne peut pas choisir seul.'], + ['Comment annuler proprement un commit déjà poussé sur main ?', ['git reset --hard', 'git revert', 'git push --force', 'supprimer .git'], 1, 'revert ajoute un commit d’annulation.'], + ['Différence entre fetch et pull ?', ['Aucune', 'fetch récupère sans fusionner, pull récupère et fusionne', 'fetch publie', 'pull supprime'], 1, 'fetch est sans risque.'], + ['Un mot de passe a été poussé puis supprimé au commit suivant. Il est :', ['effacé', 'toujours dans l’historique, à changer', 'chiffré', 'invisible'], 1, 'L’historique garde tout.'], + ['Quelle commande retrouve un commit perdu après un reset ?', ['git log', 'git reflog', 'git status', 'git show'], 1, 'Historique des positions de HEAD.'] + ], + td: [ + { n: 'Échauffement', c: 'Initialiser un dépôt, créer README.md, le versionner, le modifier, afficher la différence, puis faire un second commit.', r: 'git init · git add README.md · git commit -m "…" · git diff · git commit -am "…"' }, + { n: 'Application', c: 'Créer une branche nouvelle-page, y faire deux commits, revenir sur main, faire un commit qui touche la même ligne, fusionner et résoudre le conflit.', r: 'git switch -c nouvelle-page … git switch main … git merge nouvelle-page → conflit → éditer, git add, git commit' }, + { n: 'Application', c: 'Écrire le .gitignore d’un projet Laravel.', r: '/vendor\n/node_modules\n.env\n/storage/*.key\n/public/storage\n*.log' }, + { n: 'Approfondissement', c: 'Vous avez fait git reset --hard HEAD~2 par erreur. Retrouver les deux commits.', r: 'git reflog → repérer l’id avant le reset · git reset --hard <id> (ou git branch secours <id>)' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un dépôt partagé sur un Forgejo de lab, travail en binôme.', etapes: ['Cloner le dépôt', 'Créer une branche et ouvrir une demande de fusion', 'Provoquer et résoudre un conflit avec le binôme', 'Annuler un commit avec revert', 'Retrouver un commit avec reflog'] }, + plusLoin: [['Pro Git (livre officiel, en français)', 'https://git-scm.com/book/fr/v2'], ['Learn Git Branching (exercices interactifs)', 'https://learngitbranching.js.org/?locale=fr_FR'], ['Conventional Commits', 'https://www.conventionalcommits.org/fr/']] }, +plus: { + cas: { titre: '', texte: '' }, + resolu: { enonce: 'Vendredi 17 h : un développeur a poussé sur main un commit qui casse la page de connexion de l’application de vie scolaire, puis un collègue a déjà poussé deux commits par-dessus. Rétablir sans perdre le travail du collègue.', etapes: [ + ['Identifier le commit fautif', 'git log --oneline -5\n9c1e4f2 Traductions du menu\n51ab0d7 Ajout export CSV\ne73f210 Refonte formulaire connexion\n4d02a9b Correctif date\n→ La panne est apparue après e73f210. Les deux commits suivants ne touchent pas à la connexion.'], + ['Confirmer avec git bisect si le doute subsiste', 'git bisect start ; git bisect bad ; git bisect good 4d02a9b\n… git bisect good / bad à chaque étape\ne73f210 is the first bad commit\ngit bisect reset\n→ bisect teste par dichotomie et désigne le premier commit fautif.'], + ['Annuler ce seul commit', 'git revert e73f210\n[main 0f5a8c3] Revert "Refonte formulaire connexion"\n→ Un nouveau commit annule les modifications de e73f210 ; les commits du collègue sont conservés. Surtout pas de reset suivi d’un push --force sur main.'], + ['Publier et vérifier', 'git push origin main\n→ La chaîne d’intégration redéploie ; la connexion fonctionne à nouveau.'], + ['Reprendre le travail proprement', 'git switch -c refonte-connexion\ngit revert 0f5a8c3 (réapplique la refonte sur la branche)\n→ La refonte est corrigée sur une branche, testée, puis fusionnée par demande de fusion. Action préventive : protéger main (fusion uniquement par demande relue et tests verts).'] + ] }, + erreurs: ['Pousser directement sur main', 'Utiliser push --force sur une branche partagée', 'Versionner .env ou des clés', 'Faire un seul énorme commit pour tout', 'Messages de commit du type « fix » ou « modif »', 'Rebaser une branche déjà partagée'], + memo: [['Commande', 'Usage'], ['git status', 'état des trois zones'], ['git add -p', 'ajouter une partie des modifications'], ['git commit -m "…"', 'enregistrer'], ['git log --oneline --graph --all', 'voir le graphe des commits'], ['git diff · git diff --staged', 'différences non ajoutées / ajoutées'], ['git switch -c <branche>', 'créer une branche et s’y placer'], ['git merge <branche>', 'fusionner'], ['git pull --rebase', 'récupérer en gardant un historique linéaire'], ['git push -u origin <branche>', 'publier une branche'], ['git revert <id>', 'annuler un commit partagé'], ['git reflog', 'retrouver une position perdue'], ['git bisect', 'trouver le commit qui a introduit un bug']], + examen: { titre: 'Sujet type · 20 points', q: ['Expliquer les trois zones de Git avec un schéma. (4 pts)', 'Décrire les commandes d’un travail en branche jusqu’à la demande de fusion. (5 pts)', 'Résoudre un conflit fourni. (4 pts)', 'Choisir entre revert et reset dans trois situations. (4 pts)', 'Rédiger le .gitignore d’un projet donné. (3 pts)'], r: ['working tree, index, dépôt', 'switch -c, add, commit, push -u, PR', 'selon le fichier', 'partagé → revert, local → reset', 'dépendances, secrets, fichiers générés'] }, + prof: { deroule: [['Ouverture', 'Le fichier « final_v3_VRAIMENT_final » projeté'], ['Cours', 'Commits, index, branches'], ['Pratique', 'Exercices Learn Git Branching'], ['Pratique', 'Binômes sur un dépôt partagé, conflit provoqué'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : fiche mémo, un seul flux sans branches. Élèves avancés : rebase interactif, bisect, hooks.', eval: 'Historique du dépôt de binôme évalué, sujet type.' } +}, +exemples: [ + ['Lire l’état du dépôt', 'Avant un commit.', 'git status -s', ' M app/Http/Controllers/NoteController.php\nM routes/web.php\n?? notes.csv', 'M en deuxième colonne : modifié, pas encore ajouté. M en première : ajouté à l’index. ?? : fichier non suivi ; ici un export de notes à ne surtout pas versionner, à ajouter au .gitignore.'], + ['Voir le graphe', 'Comprendre où en sont les branches.', 'git log --oneline --graph --all -6', '* 9c1e4f2 (HEAD -> main, origin/main) Traductions du menu\n| * 3aa71be (export-pdf) Export PDF des bulletins\n|/\n* 51ab0d7 Ajout export CSV', 'La branche export-pdf est partie de 51ab0d7 ; main a avancé depuis. La fusion créera un commit de fusion, ou un rebase rendra l’historique linéaire.'], + ['Push refusé', 'Publication de son travail.', 'git push', '! [rejected] main -> main (fetch first)\nhint: Updates were rejected because the remote contains work that you do not have locally.', 'Un collègue a poussé avant vous. git pull --rebase rejoue vos commits au-dessus des siens, puis git push passe.'], + ['Résoudre un conflit', 'Fusion de deux branches.', 'git merge nouvelle-page ; git diff --name-only --diff-filter=U', 'CONFLICT (content): Merge conflict in index.html\nAutomatic merge failed; fix conflicts and then commit the result.\nindex.html', 'On ouvre index.html, on choisit le contenu entre les marqueurs, on supprime les marqueurs, puis git add index.html et git commit.'], + ['Retrouver un travail « perdu »', 'Après un reset malheureux.', 'git reflog -4', '4d02a9b HEAD@{0}: reset: moving to HEAD~2\n9c1e4f2 HEAD@{1}: commit: Traductions du menu\n51ab0d7 HEAD@{2}: commit: Ajout export CSV', 'Le commit 9c1e4f2 existe toujours. git reset --hard 9c1e4f2 le récupère.'], + ['Trouver qui a modifié une ligne', 'Une règle de calcul semble fausse.', 'git blame -L 42,44 app/Services/Moyenne.php', '51ab0d7 (Inès K. 2026-09-24) $coef = $note->coef ?? 1;\ne73f210 (Tom L. 2026-09-27) $total += $note->valeur;\ne73f210 (Tom L. 2026-09-27) return $total / count($notes);', 'blame associe chaque ligne à son dernier commit. La division par count($notes) ignore les coefficients : on regarde le commit e73f210 avec git show pour comprendre le changement.'] +], +termes: ['Git', 'commit', 'index', 'branche', 'HEAD', 'main', 'merge', 'rebase', 'conflit', 'fast-forward', 'git add', 'git commit', 'git status', 'git log', 'git diff', 'git switch', 'git pull', 'git push', 'git fetch', 'git revert', 'git reset', 'git reflog', 'git bisect', 'git blame', '.gitignore', 'pull request', 'origin'] +}); + +reg({ +cours: { id: 'cicd', match: /CI\/CD|intégration continue|déploiement continu|pipeline/i, + titre: 'CI/CD : intégration et déploiement continus', duree: '', + niveau: 'BTS SIO SLAM / SISR · BUT Informatique / R&T DevCloud · AIS · Master', + prerequis: ['Git', 'Conteneurs Docker'], + competences: ['BTS SIO SLAM · Bloc 2 · Déployer et tester une solution applicative', 'BUT R&T DevCloud · Automatiser le déploiement', 'AIS · Industrialiser les mises en production'], + intro: 'Livrer à la main, c’est se connecter au serveur, tirer le code, lancer les migrations, vider les caches et espérer ne rien oublier. Une chaîne d’intégration et de déploiement continus automatise ces étapes : à chaque push, les tests s’exécutent, l’image est construite et, si tout est vert, l’application est déployée. Les erreurs sont détectées en minutes plutôt qu’en production, et chaque livraison devient un non-événement.', + cm: [ + { t: 'Intégration, livraison, déploiement', d: '', html: 'L’<b>intégration continue</b> (CI) fusionne le travail de chacun fréquemment et vérifie automatiquement chaque modification : compilation, analyse statique, tests. La <b>livraison continue</b> (continuous delivery) produit à chaque fois un artefact prêt à déployer, la mise en production restant une décision humaine. Le <b>déploiement continu</b> (continuous deployment) va jusqu’à la production sans intervention.<br><br>Le principe commun : la branche principale est toujours dans un état déployable, et toute régression est signalée immédiatement à celui qui l’a introduite.' }, + { t: 'Anatomie d’un pipeline', d: '', html: 'Un <b>pipeline</b> est une suite d’<b>étapes</b> (stages) composées de <b>jobs</b>, exécutés par des <b>runners</b> dans des environnements propres, souvent des conteneurs. On y trouve typiquement : vérification du style (lint), tests unitaires, construction de l’image, analyse de sécurité, publication dans un registre, déploiement en préproduction, puis en production.<br><br>Chaque job reçoit le code du commit, produit éventuellement des <b>artefacts</b> (paquet, rapport de tests, image) et renvoie un statut. Un seul échec arrête le pipeline.', exemple: '# .github/workflows/ci.yml (GitHub Actions)\nname: ci\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with: { python-version: "3.12" }\n - run: pip install -r requirements.txt\n - run: ruff check .\n - run: pytest -q\n image:\n needs: tests\n if: github.ref == \'refs/heads/main\'\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: docker build -t registre.lycee.local/resa:${{ github.sha }} .\n - run: trivy image --exit-code 1 --severity HIGH,CRITICAL registre.lycee.local/resa:${{ github.sha }}\n - run: docker push registre.lycee.local/resa:${{ github.sha }}' }, + { t: 'Les tests, cœur de la CI', d: '', html: 'Une CI ne vaut que par ses tests. Les <b>tests unitaires</b> vérifient une fonction isolée, en millisecondes. Les <b>tests d’intégration</b> vérifient l’application avec sa base de données, souvent démarrée comme service du pipeline. Les <b>tests de bout en bout</b> pilotent un navigateur (Playwright, Cypress). On en écrit beaucoup des premiers, peu des derniers : c’est la pyramide des tests.<br><br>Un test instable (qui échoue une fois sur dix sans raison) est pire que pas de test : l’équipe apprend à ignorer le rouge. Il se corrige en priorité.' }, + { t: 'Déployer sans interruption', d: '', html: 'Le déploiement tire l’image construite et testée, identifiée par l’empreinte du commit, jamais par latest. Plusieurs stratégies limitent le risque. Le <b>déploiement progressif</b> (rolling) remplace les instances une à une. Le <b>blue-green</b> prépare la nouvelle version à côté de l’ancienne puis bascule le trafic. Le <b>canary</b> envoie d’abord une petite part des utilisateurs sur la nouvelle version.<br><br>Dans tous les cas, on prévoit le <b>retour arrière</b> : redéployer l’image précédente doit prendre une commande. Les migrations de base de données doivent rester compatibles avec la version précédente le temps de la bascule.' }, + { t: 'Secrets et sécurité de la chaîne', d: '', html: 'Le pipeline manipule des secrets : accès au registre, clés SSH de déploiement, jetons d’API. Ils se stockent dans le gestionnaire de secrets de la plateforme (secrets GitHub, variables masquées GitLab), jamais dans le dépôt, et avec des droits minimaux (une clé de déploiement ne peut que déployer).<br><br>La chaîne elle-même est une cible : une action tierce compromise ou une dépendance malveillante s’exécute avec les secrets du pipeline. On épingle les actions sur une version précise, on analyse les dépendances (Dependabot, Renovate) et on protège les branches : pas de déploiement en production depuis une branche non relue.' } + ], + synthese: ['CI : vérifier chaque modification ; CD : livrer ou déployer automatiquement', 'Pipeline = étapes → jobs → runners, arrêt au premier échec', 'Pyramide des tests ; un test instable se corrige en priorité', 'Image identifiée par le commit ; stratégie de déploiement et retour arrière prévus', 'Secrets dans la plateforme, actions épinglées, branches protégées'], + qcm: [ + ['Différence entre livraison continue et déploiement continu ?', ['Aucune', 'La livraison laisse la mise en production à une décision humaine', 'Le déploiement ne teste pas', 'La livraison est manuelle'], 1, 'Continuous delivery vs deployment.'], + ['Que se passe-t-il quand un job échoue ?', ['Le pipeline continue', 'Le pipeline s’arrête et signale l’échec', 'Le job est ignoré', 'Le code est supprimé'], 1, 'On ne déploie pas du rouge.'], + ['Pourquoi taguer l’image avec l’empreinte du commit ?', ['Esthétique', 'Pour savoir exactement quel code tourne et pouvoir revenir en arrière', 'Obligatoire', 'Plus léger'], 1, 'Traçabilité et reproductibilité.'], + ['Quelle stratégie envoie d’abord 5 % des utilisateurs sur la nouvelle version ?', ['Rolling', 'Blue-green', 'Canary', 'Big bang'], 2, 'Détection précoce sur un petit échantillon.'], + ['Où stocker la clé de déploiement ?', ['Dans le dépôt', 'Dans les secrets de la plateforme CI', 'Dans le Dockerfile', 'Dans le README'], 1, 'Jamais dans le code.'], + ['Que faire d’un test qui échoue au hasard ?', ['L’ignorer', 'Le corriger en priorité', 'Le supprimer toujours', 'Relancer jusqu’au vert'], 1, 'Sinon on ne croit plus au rouge.'] + ], + td: [ + { n: 'Application', c: 'Écrire un pipeline GitLab CI (.gitlab-ci.yml) en trois étapes : lint, test, build, avec une base PostgreSQL en service pour les tests.', r: 'stages: [lint, test, build]\nlint: { stage: lint, image: python:3.12, script: [pip install ruff, ruff check .] }\ntest: { stage: test, image: python:3.12, services: [postgres:16], variables: { POSTGRES_PASSWORD: test, DATABASE_URL: postgresql://postgres:test@postgres/postgres }, script: [pip install -r requirements.txt, pytest] }\nbuild: { stage: build, image: docker:27, services: [docker:27-dind], script: [docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA ., docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA], rules: [{ if: $CI_COMMIT_BRANCH == "main" }] }' }, + { n: 'Application', c: 'Proposer une procédure de retour arrière pour une application déployée avec Compose et des images taguées par commit.', r: 'Garder le tag précédent dans l’historique des déploiements · modifier le tag dans compose.yaml (ou variable IMAGE_TAG) · docker compose pull && up -d · vérifier · migrations compatibles N-1.' }, + { n: 'Approfondissement', c: 'Lister les risques de sécurité d’un pipeline et une mesure pour chacun.', r: 'Secret fuité → secrets masqués, droits minimaux · action tierce compromise → épingler par empreinte · dépendance malveillante → analyse et verrouillage des versions · déploiement non relu → branches protégées.' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un Forgejo avec runner, une API et son déploiement.', etapes: ['Écrire le workflow lint + tests', 'Casser un test et observer le blocage', 'Construire et scanner l’image', 'Déployer sur la VM de préproduction', 'Revenir à la version précédente'] }, + plusLoin: [['GitHub Actions · documentation', 'https://docs.github.com/fr/actions'], ['GitLab CI/CD · documentation', 'https://docs.gitlab.com/ee/ci/'], ['Forgejo Actions', 'https://forgejo.org/docs/latest/user/actions/']] }, +plus: { + cas: { titre: '', texte: '' }, + resolu: { enonce: 'Le pipeline de l’application de réservation est rouge depuis ce matin sur l’étape tests, alors que les tests passent sur le poste du développeur. Plus aucune correction ne peut être déployée.', etapes: [ + ['Lire le job en échec', 'tests / pytest\nE sqlalchemy.exc.OperationalError: could not connect to server: Connection refused\n Is the server running on host "localhost" and accepting TCP/IP connections on port 5432?\n→ Ce n’est pas un test fonctionnel qui échoue : l’application ne trouve pas sa base.'], + ['Comparer les environnements', 'Sur le poste : PostgreSQL installé localement, DATABASE_URL absente → valeur par défaut localhost.\nDans le pipeline : la base est un service nommé postgres.\n→ Un commit d’hier a supprimé la variable DATABASE_URL du workflow en « nettoyant » le fichier.'], + ['Corriger le workflow', 'services:\n postgres:\n image: postgres:16\n env: { POSTGRES_PASSWORD: test }\n options: --health-cmd pg_isready --health-interval 5s --health-retries 10\nenv:\n DATABASE_URL: postgresql://postgres:test@localhost:5432/postgres\n→ Le service est déclaré avec un healthcheck et la variable est rétablie.'], + ['Vérifier', 'git push → pipeline : lint ✓ tests ✓ image ✓\n→ Le pipeline est vert ; la correction en attente est déployée.'], + ['Prévenir', '→ Le fichier de workflow passe sous relue obligatoire (CODEOWNERS), et un test de fumée vérifie au démarrage que DATABASE_URL est définie, avec un message explicite plutôt qu’une erreur de connexion.'] + ] }, + erreurs: ['Pipeline sans tests réels', 'Tolérer un pipeline rouge « connu »', 'Déployer l’image latest', 'Secrets en clair dans le workflow', 'Actions tierces non épinglées', 'Pas de procédure de retour arrière'], + memo: [['Élément', 'À retenir'], ['on: push / pull_request', 'déclencheurs d’un workflow GitHub'], ['needs:', 'ordre entre jobs'], ['services:', 'base de données pour les tests'], ['secrets.NOM', 'accès à un secret masqué'], ['rules / if', 'n’exécuter que sur main'], ['artifacts', 'conserver rapports et paquets'], ['cache', 'accélérer les dépendances'], ['${{ github.sha }} / $CI_COMMIT_SHA', 'taguer l’image par commit'], ['trivy image --exit-code 1', 'bloquer sur une vulnérabilité critique'], ['environments + approbation', 'validation humaine avant production']], + examen: { titre: 'Sujet type · 20 points', q: ['Définir CI, livraison et déploiement continus. (3 pts)', 'Écrire un pipeline complet pour une application fournie. (8 pts)', 'Comparer trois stratégies de déploiement. (4 pts)', 'Diagnostiquer un pipeline en échec à partir d’un journal. (3 pts)', 'Citer deux risques de sécurité de la chaîne. (2 pts)'], r: ['voir cours', 'lint, tests, build, push, deploy', 'rolling, blue-green, canary', 'selon le journal', 'secrets, actions tierces'] }, + prof: { deroule: [['Ouverture', 'Déploiement manuel chronométré, puis le même en un push'], ['Cours', 'Pipeline, tests, artefacts'], ['Pratique', 'Workflow lint + tests'], ['Pratique', 'Build et déploiement'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : workflow à compléter. Élèves avancés : déploiement canary, analyse de dépendances.', eval: 'Pipeline vert et retour arrière démontré, sujet type.' } +}, +exemples: [ + ['Lire l’état des pipelines', 'Après un push.', 'gh run list --limit 3', 'STATUS TITLE WORKFLOW BRANCH\n✓ Export CSV ci main\nX Refonte connexion ci refonte\n✓ Traductions ci main', 'La branche refonte est rouge : elle ne pourra pas être fusionnée tant que la protection de branche exige des tests verts.'], + ['Voir le journal d’un job', 'Pipeline rouge.', 'gh run view --log-failed | tail -4', 'tests FAILED tests/test_moyenne.py::test_absent - ZeroDivisionError\ntests 1 failed, 47 passed in 3.21s', 'Un seul test échoue, avec une erreur précise : la moyenne d’un élève sans note divise par zéro. La CI a attrapé le bug avant la production.'], + ['Relancer localement comme la CI', 'Reproduire un échec.', 'docker run --rm -v "$PWD":/app -w /app python:3.12 sh -c "pip install -q -r requirements.txt && pytest -q"', '1 failed, 47 passed', 'Exécuter les tests dans la même image que la CI élimine le « chez moi ça marche ».'], + ['Bloquer une image vulnérable', 'Étape de sécurité.', 'trivy image --exit-code 1 --severity CRITICAL registre.lycee.local/resa:9c1e4f2', 'Total: 1 (CRITICAL: 1)\nopenssl 3.0.2 CVE-2024-XXXX fixed in 3.0.14', 'Le code de sortie 1 fait échouer le job : l’image n’est pas publiée. On met à jour l’image de base et on reconstruit.'], + ['Déployer une version précise', 'Déploiement depuis le pipeline.', 'ssh deploy@preprod "cd /srv/resa && IMAGE_TAG=9c1e4f2 docker compose up -d && docker compose ps"', 'resa-app registre.lycee.local/resa:9c1e4f2 Up 5 seconds (healthy)', 'Le tag est l’empreinte du commit : on sait exactement quel code tourne.'], + ['Revenir en arrière', 'La nouvelle version provoque des erreurs.', 'ssh deploy@prod "cd /srv/resa && IMAGE_TAG=51ab0d7 docker compose up -d"', 'Container resa-app Recreated', 'Retour à la version précédente en une commande, parce que chaque image est conservée avec son tag de commit.'] +], +termes: ['CI/CD', 'intégration continue', 'déploiement continu', 'livraison continue', 'pipeline', 'job', 'runner', 'artefact', 'workflow', 'GitHub Actions', 'GitLab CI', 'Forgejo', 'tests unitaires', 'lint', 'canary', 'blue-green', 'rolling', 'retour arrière', 'secrets', 'Trivy'] +}); +})(); diff --git a/portail/public/js/contenus-exemples.js b/portail/public/js/contenus-exemples.js new file mode 100644 index 0000000..0850877 --- /dev/null +++ b/portail/public/js/contenus-exemples.js @@ -0,0 +1,183 @@ +// Exemples concrets commentés : [titre, contexte, code, sortie, explication] +window.CONTENUS_EXEMPLES = { +binaire: [ + ['Lire une adresse MAC', 'Sur l’étiquette d’une carte réseau.', '3C:52:82:1A:0F:9B', '6 octets · 3C = 0011 1100 = 60', 'Chaque paire hexadécimale est un octet. Les 3 premiers identifient le fabricant (OUI), les 3 derniers la carte.'], + ['Décoder une couleur web', 'Dans une feuille CSS.', '#FF6FB5', 'rouge 255 · vert 111 · bleu 181', 'Trois octets en hexadécimal. FF est le maximum : la couleur est très rouge, un peu bleue, c’est un rose.'], + ['Calculer un masque de tête', 'Un collègue demande « c’est quoi un /22 ? ».', '/22 = 11111111.11111111.11111100.00000000', '255.255.252.0 · 1 022 hôtes', '22 bits à 1 : deux octets pleins, puis 6 bits à 1 (128+64+32+16+8+4 = 252). Il reste 10 bits d’hôte : 2¹⁰ − 2 = 1 022.'], + ['Convertir en Python', 'Vérifier un calcul fait à la main.', 'bin(178), hex(178), int("B2", 16), int("10110010", 2)', "('0b10110010', '0xb2', 178, 178)", 'bin et hex convertissent vers le texte ; int avec une base convertit dans l’autre sens.'], + ['Débordement sur 8 bits', 'Un compteur de capteur repasse à zéro.', 'compteur = (255 + 1) % 256', '0', 'Sur un octet non signé, 255 + 1 ne tient plus : le 9e bit est perdu et l’on repart de 0.'], + ['Taille d’une image', 'Estimer le poids d’une photo non compressée.', '1920 × 1080 pixels × 3 octets', '6 220 800 octets ≈ 5,9 Mio', 'Chaque pixel a 3 couleurs sur 1 octet. C’est pour cela qu’on compresse en JPEG ou PNG.']], +terminal: [ + ['Préparer un TP', 'Créer d’un coup l’arborescence d’un projet.', 'mkdir -p ~/tp/{reseau,systeme,web} && ls ~/tp', 'reseau systeme web', 'Les accolades génèrent trois chemins. -p évite l’erreur si ~/tp existe déjà.'], + ['Retrouver un fichier perdu', 'Un élève ne sait plus où il a enregistré rapport.txt.', 'find ~ -name "rapport*.txt" 2>/dev/null', '/home/eleve/Documents/old/rapport-v2.txt', 'find part du dossier personnel ; 2>/dev/null masque les dossiers inaccessibles.'], + ['Voir les derniers événements', 'Le service web ne démarre pas.', 'sudo journalctl -u apache2 -n 20 --no-pager', 'AH00072: make_sock: could not bind to address [::]:80', 'Le port 80 est déjà pris par un autre programme : on le cherche avec sudo ss -tlnp | grep :80.'], + ['Sauvegarder un dossier', 'Avant une modification risquée.', 'tar czf site-$(date +%F).tar.gz /var/www/site', 'site-2026-09-30.tar.gz', '$(date +%F) insère la date du jour : chaque sauvegarde a un nom unique.'], + ['Repérer ce qui ralentit la machine', 'Le serveur répond lentement.', 'ps aux --sort=-%cpu | head -5', 'www-data 8812 97.3 … php-fpm', 'Un processus PHP consomme presque tout le processeur : on regarde le script en cause avant de le tuer.'], + ['Rejouer une commande', 'Vous avez tapé une longue commande hier.', 'Ctrl+R puis « rsync »', '(reverse-i-search)`rsync\': rsync -av site/ srv:/var/www/', 'La recherche dans l’historique évite de tout retaper.']], +droits: [ + ['Lire une ligne de ls -l', 'Comprendre pourquoi un fichier est inaccessible.', 'ls -l notes.csv', '-rw-r----- 1 alice profs 4096 sept. 30 notes.csv', 'alice lit et écrit, le groupe profs lit, les autres rien. Un élève ne peut pas l’ouvrir.'], + ['Rendre un script exécutable', '« Permission non accordée » au lancement.', 'chmod u+x sauvegarde.sh && ./sauvegarde.sh', 'Sauvegarde terminée.', 'Seul le propriétaire reçoit le droit d’exécution, pas tout le monde.'], + ['Créer un espace partagé', 'Les profs de SIO veulent un dossier commun.', 'sudo mkdir /srv/sio && sudo chown root:profsio /srv/sio && sudo chmod 2770 /srv/sio', 'drwxrws--- root profsio /srv/sio', 'Le s du groupe (SGID) fait que chaque fichier créé appartient à profsio.'], + ['Vérifier ses groupes', 'Après avoir été ajouté à un groupe.', 'id', 'uid=1001(bob) … groupes=1001(bob),1002(devs)', 'Si devs n’apparaît pas, il faut se déconnecter puis se reconnecter.'], + ['Trouver les fichiers trop ouverts', 'Audit rapide d’un serveur.', 'sudo find /var/www -perm -o+w -type f', '/var/www/site/config.php', 'Un fichier de configuration modifiable par tous est une faille : chmod o-w config.php.'], + ['Donner un accès ponctuel', 'Un stagiaire doit lire un seul rapport.', 'setfacl -m u:stagiaire:r rapport.pdf && getfacl rapport.pdf', 'user:stagiaire:r--', 'L’ACL donne l’accès à une seule personne sans toucher au groupe.']], +tubes: [ + ['Compter les comptes humains', 'Combien d’utilisateurs ont un shell ?', 'grep -c "/bin/bash$" /etc/passwd', '14', 'grep -c compte au lieu d’afficher ; $ ancre la recherche en fin de ligne.'], + ['Top des pages visitées', 'Préparer un bilan pour la direction.', "awk '{print $7}' /var/log/apache2/access.log | sort | uniq -c | sort -rn | head -3", ' 812 /\n 430 /planning\n 211 /cours', 'Le 7e champ d’une ligne Apache est le chemin demandé.'], + ['Suivre les connexions en direct', 'Pendant un examen en ligne.', 'tail -f /var/log/auth.log | grep --line-buffered sshd', 'Accepted publickey for prof from 10.0.0.12', 'tail -f affiche au fil de l’eau ; --line-buffered évite que grep retienne les lignes.'], + ['Nettoyer les vieux journaux', 'Libérer de l’espace chaque semaine.', 'find /var/log/app -name "*.log" -mtime +30 | xargs rm -v', "supprimé '/var/log/app/2026-07-01.log'", 'xargs transforme la liste en arguments pour rm.'], + ['Remplacer dans un fichier', 'Passer toutes les URL en HTTPS.', "sed -i 's#http://#https://#g' index.html", '(aucune sortie)', '-i modifie le fichier sur place ; # remplace / comme séparateur pour éviter d’échapper les barres.'], + ['Garder une trace d’un diagnostic', 'Envoyer le résultat au support.', './diagnostic.sh 2>&1 | tee rapport.txt', '(affichage à l’écran + fichier rapport.txt)', '2>&1 fusionne les erreurs dans la sortie, tee écrit tout dans le fichier.']], +osi: [ + ['Tester la couche 3', 'Le poste accède-t-il à la passerelle ?', 'ping -c 3 192.168.10.1', '3 packets transmitted, 3 received, 0% packet loss', 'La couche réseau fonctionne jusqu’à la passerelle.'], + ['Suivre le chemin d’un paquet', 'Un site distant est lent.', 'traceroute www.lycee.fr', ' 1 192.168.10.1 1 ms\n 2 10.0.0.1 4 ms\n 3 * * *\n 4 93.184.216.34 18 ms', 'Chaque ligne est un routeur. Les étoiles indiquent un routeur qui ne répond pas à ICMP, pas forcément une panne.'], + ['Voir les services qui écoutent', 'Le site ne répond pas.', 'sudo ss -tlnp', 'LISTEN 0 511 *:80 users:(("apache2",pid=812))', 'Apache écoute bien sur le port 80 en TCP : le problème est ailleurs (pare-feu, DNS).'], + ['Interroger le DNS', 'Vérifier une nouvelle entrée.', 'dig +short intranet.lycee.fr', '192.168.10.20', 'Le nom se résout : la couche application peut être testée avec curl.'], + ['Lire la table ARP', 'Suspicion de conflit d’adresse.', 'ip neigh', '192.168.10.1 dev eth0 lladdr 3c:52:82:1a:0f:9b REACHABLE', 'Si deux IP différentes affichent la même MAC, on soupçonne une usurpation ARP.'], + ['Capturer une poignée de main', 'Observer TCP en vrai.', 'sudo tcpdump -i eth0 -n "tcp port 443 and tcp[tcpflags] & tcp-syn != 0"', 'Flags [S] … Flags [S.] …', '[S] = SYN envoyé, [S.] = SYN-ACK reçu.']], +sql: [ + ['Liste de classe', 'Imprimer la liste de BTS SIO 1.', "SELECT nom, prenom FROM eleves e JOIN classes c ON c.id = e.classe_id WHERE c.libelle = 'BTS SIO 1' ORDER BY nom;", 'Diallo Adam · Kaci Inès · Martin Léa …', 'La jointure permet de filtrer par libellé plutôt que par numéro.'], + ['Moyenne par classe', 'Préparer le conseil de classe.', 'SELECT c.libelle, ROUND(AVG(e.moyenne), 1) AS moy FROM eleves e JOIN classes c ON c.id = e.classe_id GROUP BY c.libelle;', 'BTS SIO 1 | 11.8\nBTS SIO 2 | 12.6', 'AVG calcule la moyenne de chaque groupe, ROUND arrondit.'], + ['Élèves sans rendu', 'Relancer ceux qui n’ont pas rendu le DM.', 'SELECT e.nom FROM eleves e LEFT JOIN rendus r ON r.eleve_id = e.id AND r.devoir_id = 3 WHERE r.eleve_id IS NULL;', 'Petit · Leroy', 'LEFT JOIN garde tout le monde ; IS NULL isole ceux qui n’ont pas de correspondance.'], + ['Top 3', 'Afficher les meilleures notes du DM.', 'SELECT e.nom, r.note FROM rendus r JOIN eleves e ON e.id = r.eleve_id WHERE r.devoir_id = 3 ORDER BY r.note DESC LIMIT 3;', 'Kaci 19 · Nguyen 16 · Martin 11', 'ORDER BY … DESC puis LIMIT pour un classement.'], + ['Corriger une erreur de saisie', 'Une note a été saisie à 1,5 au lieu de 15.', 'UPDATE rendus SET note = 15 WHERE eleve_id = 7 AND devoir_id = 3;', '1 ligne modifiée', 'Le WHERE est testé d’abord avec un SELECT pour vérifier qu’il ne vise qu’une ligne.'], + ['Bloquer une injection', 'Formulaire de connexion en PHP.', '$st = $pdo->prepare("SELECT id FROM users WHERE login = ?");\n$st->execute([$_POST["login"]]);', '(la saisie n’est jamais interprétée comme du SQL)', 'Même si l’utilisateur tape \' OR 1=1 --, la requête cherche ce texte littéral.']], +python: [ + ['Moyenne d’une classe', 'Calcul rapide à partir d’une liste.', 'notes = [12, 15, 9, 18, 7]\nprint(round(sum(notes) / len(notes), 2))', '12.2', 'sum additionne, len compte ; round arrondit à 2 décimales.'], + ['Saisie robuste', 'Le programme ne doit pas planter sur « douze ».', 'try:\n n = int(input("Note : "))\nexcept ValueError:\n print("Entrez un nombre")', 'Note : douze\nEntrez un nombre', 'L’exception est capturée ; le programme continue.'], + ['Compter les mots', 'Analyser un texte.', 'texte = "le chat et le chien"\nc = {}\nfor m in texte.split():\n c[m] = c.get(m, 0) + 1\nprint(c)', "{'le': 2, 'chat': 1, 'et': 1, 'chien': 1}", 'get(m, 0) renvoie 0 si le mot n’est pas encore dans le dictionnaire.'], + ['Filtrer en une ligne', 'Lister les élèves admis.', 'eleves = {"Léa": 12, "Tom": 8, "Inès": 17}\nprint([n for n, v in eleves.items() if v >= 10])', "['Léa', 'Inès']", 'Liste en compréhension : on lit « les noms n pour chaque paire si la note ≥ 10 ».'], + ['Lire un CSV exporté', 'Fichier de notes du logiciel de vie scolaire.', 'import csv\nwith open("notes.csv", encoding="utf-8") as f:\n for l in csv.DictReader(f, delimiter=";"):\n if float(l["note"]) < 8: print(l["nom"])', 'Petit\nLeroy', 'DictReader donne chaque ligne comme un dictionnaire, indexé par les en-têtes.'], + ['Jeu du nombre mystère', 'Premier programme complet.', 'import random\ns = random.randint(1, 100)\nwhile (p := int(input("? "))) != s:\n print("plus" if p < s else "moins")\nprint("Trouvé !")', '? 50\nplus\n? 75\nmoins\n? 62\nTrouvé !', 'while répète tant que la proposition est fausse ; l’opérateur := affecte et teste en même temps.']], +web: [ + ['Voir une requête HTTP', 'Comprendre ce qu’envoie le serveur.', 'curl -I https://example.org', 'HTTP/2 200\ncontent-type: text/html; charset=UTF-8', '-I n’affiche que les en-têtes : code, type de contenu, taille.'], + ['Page minimale valide', 'Point de départ de tout site.', '<!doctype html>\n<html lang="fr">\n<head><meta charset="utf-8"><title>TP\n

Bonjour

\n', '(une page avec un titre)', 'lang aide les lecteurs d’écran, charset évite les accents cassés.'], + ['Grille de 3 cartes', 'Mise en page moderne.', '.grille { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }\n@media (max-width: 700px) { .grille { grid-template-columns: 1fr; } }', '3 colonnes sur ordinateur, 1 sur téléphone', 'Deux règles suffisent pour une mise en page adaptative.'], + ['Réagir à un clic', 'Bouton « Lever la main ».', 'document.querySelector("#main").addEventListener("click", () => {\n document.body.style.background = "#FFD23F";\n});', '(la page devient jaune au clic)', 'addEventListener branche une fonction sur un événement.'], + ['Diagnostiquer une 404', 'Une image ne s’affiche pas.', 'sudo tail -1 /var/log/apache2/access.log', '"GET /img/Logo.png HTTP/1.1" 404', 'Linux distingue les majuscules : le fichier s’appelle logo.png.'], + ['Empêcher une XSS', 'Afficher un commentaire de visiteur en PHP.', 'echo htmlspecialchars($commentaire, ENT_QUOTES, "UTF-8");', '<script> s’affiche comme du texte', 'Les caractères spéciaux sont échappés : le navigateur n’exécute rien.']], +crypto: [ + ['Vérifier un téléchargement', 'Image ISO de Debian.', 'sha256sum -c SHA256SUMS --ignore-missing', 'debian-13.iso: Réussi', 'L’empreinte du fichier correspond à celle publiée : le fichier est intact.'], + ['Constater l’effet avalanche', 'Changer une seule lettre.', 'echo -n bonjour | sha256sum\necho -n Bonjour | sha256sum', '2cb4b143… \n9172e8ee…', 'Une majuscule suffit à changer complètement l’empreinte.'], + ['Hacher un mot de passe en Python', 'Application web.', 'import bcrypt\nh = bcrypt.hashpw(b"Soleil2026!", bcrypt.gensalt())\nbcrypt.checkpw(b"Soleil2026!", h)', 'True', 'gensalt produit un sel aléatoire, stocké dans le hachage lui-même.'], + ['Générer une clé SSH', 'Première connexion à un serveur.', 'ssh-keygen -t ed25519 -C "eleve@syllabus"', 'Your public key has been saved in ~/.ssh/id_ed25519.pub', 'Deux fichiers : la clé privée (à garder) et la clé publique (.pub, à déposer).'], + ['Lire un certificat', 'Vérifier qui a signé le certificat d’un site.', 'echo | openssl s_client -connect example.org:443 2>/dev/null | openssl x509 -noout -issuer -dates', "issuer=… DigiCert …\nnotAfter=Jan 15 2027", 'On voit l’autorité et la date d’expiration.'], + ['Chiffrer un fichier', 'Envoyer les bulletins à la direction.', 'gpg -c --cipher-algo AES256 bulletins.pdf', 'bulletins.pdf.gpg', 'Le mot de passe se transmet par un autre canal (téléphone), jamais dans le même mail.']] +}; + +// === DIAGNOSTIC RÉSEAU === Cours complet : ip, ping, traceroute, ss, ARP, tcpdump / Wireshark +(function () { +window.CONTENUS_PRO = window.CONTENUS_PRO || []; +window.CONTENUS_PRO.unshift({ + id: 'diagnostic', + match: /diagnostic réseau|outils de diagnostic|dépannage réseau/i, + titre: 'Diagnostic réseau en ligne de commande : ip, ping, traceroute, ss, ARP, tcpdump', + duree: '', + niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · Titre pro TSSR / AIS', + prerequis: ['Modèles OSI et TCP/IP, encapsulation', 'Adressage IPv4 et masques', 'Le terminal Linux'], + competences: ['BTS SIO · Bloc 1 · Répondre aux incidents et aux demandes d’assistance', 'BTS SIO SISR · Bloc 2 · Exploiter, dépanner et superviser une solution d’infrastructure réseau', 'BTS CIEL · C09 · Maintenir un réseau', 'BUT R&T · Compétence « Administrer les réseaux et l’Internet »', 'TSSR · Exploiter et maintenir les serveurs et le réseau'], + intro: 'Un serveur de production n’a presque jamais d’interface graphique. Il est rangé dans une baie, dans un datacenter à Paris ou à Roubaix, et l’on s’y connecte en SSH depuis un poste qui peut être à l’autre bout du pays. Quand un utilisateur appelle pour dire « le site ne marche plus », il n’y a ni fenêtre de paramètres réseau ni icône Wi-Fi à regarder : il y a un terminal, et une poignée d’outils en ligne de commande. Ce cours présente ces outils un par un, explique précisément ce que chacun mesure et ce qu’il ne mesure pas, puis les assemble en une méthode de diagnostic que vous pourrez appliquer à n’importe quel incident.', + cm: [ + { t: 'La méthode avant les outils', d: '', html: 'Le réflexe du débutant est de tout essayer en même temps : redémarrer le service, changer le câble, vider le cache du navigateur, relancer la machine. Parfois cela marche, mais on ne sait pas pourquoi, et la panne reviendra. Le professionnel suit une démarche par couches, du bas vers le haut, et ne passe à la couche suivante que lorsque la précédente est validée.

Concrètement, on se pose cinq questions dans l’ordre. Le lien est-il actif (couche 1-2) ? La machine a-t-elle une adresse IP correcte et une route vers la destination (couche 3 locale) ? Les paquets atteignent-ils la passerelle, puis la destination (couche 3 de bout en bout) ? Le service écoute-t-il sur le bon port et le pare-feu le laisse-t-il passer (couche 4) ? L’application répond-elle correctement (couche 7) ? Chaque outil de ce cours répond à une de ces questions.

Enfin, on note tout. Une commande, sa sortie, l’heure. Un diagnostic écrit peut être relu par un collègue, joint à un ticket, et sert de preuve quand le problème vient d’un prestataire.', table: [['Question', 'Couche', 'Outil principal'], ['Le lien est-il monté ?', '1-2', 'ip link'], ['Ai-je une adresse et une route ?', '3 locale', 'ip addr, ip route'], ['Mon voisin direct répond-il ?', '2-3', 'ip neigh, ping passerelle'], ['La destination est-elle joignable ?', '3', 'ping, traceroute, mtr'], ['Le service écoute-t-il ?', '4', 'ss'], ['Que se passe-t-il vraiment sur le fil ?', 'toutes', 'tcpdump, Wireshark'], ['Le nom se résout-il ?', '7', 'dig'], ['L’application répond-elle ?', '7', 'curl']] }, + { t: 'ip : l’état réseau de la machine', d: '', html: 'La commande ip, du paquet iproute2, remplace depuis longtemps les anciens ifconfig, route et arp, qui ne sont plus installés par défaut sur Debian. Elle se décline en sous-commandes : ip link pour les interfaces, ip addr pour les adresses, ip route pour la table de routage, ip neigh pour la table de voisinage (ARP).

ip link indique si l’interface est administrativement activée (UP) et si la couche physique détecte une porteuse (LOWER_UP). Une interface « UP » sans « LOWER_UP » a été activée mais ne voit pas de câble ou de switch en face : le problème est physique. ip -s link ajoute les compteurs de paquets, d’erreurs et de pertes, très utiles pour repérer un câble abîmé ou un duplex mal négocié.

ip addr affiche les adresses. L’option -br (brief) donne une ligne par interface, idéale pour un coup d’œil. On y vérifie l’adresse, mais surtout le masque : un poste en 192.168.10.65/24 alors que le réseau est un /26 croira que 192.168.10.130 est sur son réseau local et ne passera jamais par la passerelle.

ip route montre la table de routage. La ligne default via 192.168.10.1 est la passerelle par défaut : sans elle, la machine ne sait joindre que son propre réseau. La commande ip route get 8.8.8.8 est précieuse : elle indique quelle route, quelle interface et quelle adresse source seraient utilisées pour joindre une destination donnée, sans envoyer le moindre paquet.', exemple: '$ ip -br link\nlo UNKNOWN 00:00:00:00:00:00 \nens18 UP bc:24:11:5e:2a:10 \n\n$ ip -br addr\nlo UNKNOWN 127.0.0.1/8 ::1/128\nens18 UP 192.168.10.20/24 fe80::be24:11ff:fe5e:2a10/64\n\n$ ip route\ndefault via 192.168.10.1 dev ens18 proto static\n192.168.10.0/24 dev ens18 proto kernel scope link src 192.168.10.20\n\n$ ip route get 1.1.1.1\n1.1.1.1 via 192.168.10.1 dev ens18 src 192.168.10.20 uid 1000', attention: 'Les modifications faites avec ip addr add ou ip route add sont perdues au redémarrage. Sur Debian, la configuration permanente est dans /etc/network/interfaces ou gérée par systemd-networkd / NetworkManager. On teste avec ip, on pérennise dans le fichier.' }, + { t: 'ping : la preuve de joignabilité', d: '', html: 'ping envoie des messages ICMP Echo Request et attend des Echo Reply. Il mesure trois choses : si la destination répond, en combien de temps (le RTT, round-trip time, aller et retour), et quelle proportion des messages se perd en route.

Chaque ligne de réponse contient un numéro de séquence (icmp_seq), un TTL et un temps. Des numéros de séquence qui sautent indiquent des pertes. Le TTL de la réponse donne un indice sur la distance et le système distant : Linux part en général de 64, Windows de 128, beaucoup d’équipements réseau de 255 ; un TTL reçu de 57 signifie qu’un paquet parti à 64 a traversé 7 routeurs.

Le résumé final est la partie la plus importante. packet loss doit être à 0 % sur un réseau sain ; quelques pour cent de pertes suffisent à rendre une visioconférence pénible et un transfert de fichiers très lent, car TCP réduit son débit à chaque perte. La ligne rtt min/avg/max/mdev donne le temps minimum, moyen, maximum et l’écart moyen (gigue). Une gigue élevée révèle un lien saturé ou un Wi-Fi perturbé.

Options utiles : -c 5 limite le nombre de paquets (sans elle, ping tourne jusqu’à Ctrl+C), -i 0.2 accélère l’envoi, -s 1472 -M do envoie un gros paquet interdit de fragmentation pour tester la MTU d’un chemin, -I ens18 force l’interface de sortie.', exemple: '$ ping -c 4 192.168.10.1\nPING 192.168.10.1 (192.168.10.1) 56(84) bytes of data.\n64 bytes from 192.168.10.1: icmp_seq=1 ttl=64 time=0.412 ms\n64 bytes from 192.168.10.1: icmp_seq=2 ttl=64 time=0.387 ms\n64 bytes from 192.168.10.1: icmp_seq=3 ttl=64 time=0.401 ms\n64 bytes from 192.168.10.1: icmp_seq=4 ttl=64 time=0.395 ms\n\n--- 192.168.10.1 ping statistics ---\n4 packets transmitted, 4 received, 0% packet loss, time 3055ms\nrtt min/avg/max/mdev = 0.387/0.398/0.412/0.009 ms', attention: 'Un ping qui échoue ne prouve pas qu’une machine est éteinte. De nombreux pare-feux, dont celui de Windows par défaut et ceux des hébergeurs, bloquent ICMP. À l’inverse, un ping qui répond ne prouve pas qu’un service fonctionne : la machine peut répondre au ping avec un serveur web planté.' }, + { t: 'traceroute et mtr : voir le chemin', d: '', html: 'Quand la destination ne répond pas ou répond lentement, il faut savoir où se situe le problème. traceroute exploite astucieusement le champ TTL de l’en-tête IP. Chaque routeur décrémente ce TTL de 1 ; lorsqu’il atteint 0, le routeur détruit le paquet et renvoie un message ICMP Time Exceeded à l’expéditeur. traceroute envoie donc un premier paquet avec un TTL de 1 (il meurt au premier routeur, qui se dénonce), puis un TTL de 2 (il meurt au deuxième), et ainsi de suite jusqu’à la destination.

Chaque ligne correspond à un saut, avec trois mesures de temps. Des étoiles (* * *) signifient qu’aucune réponse n’est revenue pour ce saut. Si les sauts suivants répondent, ce n’est pas une panne : le routeur est simplement configuré pour ne pas répondre ou pour limiter ses réponses ICMP. Si tout s’arrête à un saut et que plus rien ne répond ensuite, le problème se situe à cet endroit ou juste après.

Par défaut, traceroute sous Linux envoie des datagrammes UDP vers des ports élevés. Les pare-feux les bloquent souvent ; traceroute -I utilise ICMP comme Windows (tracert), et traceroute -T -p 443 utilise des paquets TCP SYN vers le port 443, qui passent presque partout puisqu’ils ressemblent à une connexion web.

mtr combine ping et traceroute : il interroge chaque saut en continu et affiche pour chacun le taux de perte et les temps. C’est l’outil de référence pour documenter un problème de qualité auprès d’un opérateur. Attention à l’interprétation : une perte affichée sur un saut intermédiaire mais pas sur les suivants vient d’un routeur qui limite ses réponses, pas d’une vraie perte. Seule compte la perte qui persiste jusqu’à la destination.', exemple: '$ traceroute -n 1.1.1.1\ntraceroute to 1.1.1.1, 30 hops max, 60 byte packets\n 1 192.168.10.1 0.512 ms 0.470 ms 0.455 ms\n 2 10.255.0.1 2.118 ms 2.097 ms 2.230 ms\n 3 * * *\n 4 62.34.2.57 9.874 ms 9.812 ms 9.930 ms\n 5 1.1.1.1 10.402 ms 10.377 ms 10.361 ms', attention: 'Un temps qui augmente brusquement à un saut puis reste élevé indique un lien lent ou long (câble sous-marin, par exemple). Un temps élevé sur un seul saut, suivi de temps normaux, n’indique rien : le routeur traite simplement ses réponses ICMP avec une faible priorité.' }, + { t: 'ss : qui écoute, qui est connecté', d: '', html: 'ss (socket statistics) remplace netstat. Il liste les sockets ouvertes sur la machine, c’est-à-dire les points de communication entre un programme et le réseau. C’est l’outil qui répond à la question « le service écoute-t-il ? », la plus fréquente après « la machine est-elle joignable ? ».

La combinaison à connaître par cœur est ss -tlnp : t pour TCP, l pour les sockets en écoute (LISTEN), n pour afficher les numéros de port au lieu des noms, p pour afficher le processus propriétaire (nécessite sudo). Remplacez t par u pour UDP, ou mettez les deux.

La colonne Local Address:Port est décisive. 0.0.0.0:80 ou *:80 signifie que le service écoute sur toutes les interfaces. 127.0.0.1:3306 signifie qu’il n’écoute que sur la boucle locale : il est invisible depuis le réseau, ce qui est voulu pour une base de données, mais une erreur classique pour un service censé être public.

Sans l’option l, ss montre les connexions établies et leurs états TCP. ESTAB : connexion établie. SYN-SENT : on a envoyé un SYN et on attend la réponse, souvent le signe d’un pare-feu qui jette les paquets. TIME-WAIT : connexion fermée récemment, normal en grand nombre sur un serveur web. CLOSE-WAIT en grand nombre : l’application ne referme pas ses connexions, c’est un bug applicatif. Des filtres permettent d’être précis : ss -tn state established '( dport = :443 )'.', exemple: '$ sudo ss -tlnp\nState Recv-Q Send-Q Local Address:Port Peer Address:Port Process\nLISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=611,fd=3))\nLISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("apache2",pid=842,fd=4))\nLISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=790,fd=21))\n\n$ ss -tn state established\nRecv-Q Send-Q Local Address:Port Peer Address:Port\n0 0 192.168.10.20:22 192.168.10.54:51522' }, + { t: 'ARP et ip neigh : le voisinage', d: '', html: 'Pour envoyer une trame Ethernet, il faut l’adresse MAC du destinataire direct, c’est-à-dire la machine elle-même si elle est sur le même réseau, sinon la passerelle. Le protocole ARP fait la correspondance : la machine diffuse à tout le réseau local « qui a 192.168.10.1 ? », la passerelle répond avec sa MAC, et la réponse est gardée en cache.

ip neigh affiche ce cache avec l’état de chaque entrée. REACHABLE : le voisin a été confirmé récemment. STALE : l’entrée est ancienne mais encore utilisable, elle sera vérifiée au prochain envoi. INCOMPLETE : une requête ARP est partie, aucune réponse n’est encore arrivée. FAILED : le voisin n’a jamais répondu.

Une passerelle en FAILED explique à elle seule une panne complète : la machine a une adresse, une route, mais ne parvient même pas à parler à son routeur. Les causes sont souvent au niveau 2 : mauvais VLAN sur le port du switch, câble dans la mauvaise prise, passerelle éteinte.

arping envoie des requêtes ARP directement et permet de détecter un conflit d’adresse : si deux MAC différentes répondent pour la même IP, deux machines ont été configurées avec la même adresse. Le même phénomène peut signaler une attaque par usurpation ARP (ARP spoofing), où un attaquant se fait passer pour la passerelle afin d’intercepter le trafic.', exemple: '$ ip neigh\n192.168.10.1 dev ens18 lladdr 3c:52:82:1a:0f:9b REACHABLE\n192.168.10.54 dev ens18 lladdr 00:15:5d:0a:11:03 STALE\n192.168.10.99 dev ens18 FAILED\n\n$ sudo arping -c 3 -I ens18 192.168.10.30\nUnicast reply from 192.168.10.30 [00:15:5D:0A:11:07] 0.645ms\nUnicast reply from 192.168.10.30 [08:00:27:3B:C4:12] 0.702ms\nUnicast reply from 192.168.10.30 [00:15:5D:0A:11:07] 0.610ms', attention: 'Deux adresses MAC différentes pour une même IP, comme ci-dessus : conflit d’adresse à traiter immédiatement. Les symptômes typiques sont des coupures aléatoires de quelques secondes pour les deux machines.' }, + { t: 'tcpdump et Wireshark : regarder le fil', d: '', html: 'Tous les outils précédents déduisent ce qui se passe. tcpdump montre ce qui passe réellement sur l’interface, paquet par paquet. C’est l’outil de dernier recours, et souvent celui qui tranche les débats entre équipes (« le pare-feu bloque » contre « l’application ne répond pas »).

tcpdump s’exécute avec sudo et accepte un filtre BPF qui limite la capture à ce qui nous intéresse : host 192.168.10.54, port 443, tcp, icmp, combinables avec and, or, not. Sans filtre, sur un serveur actif, l’écran défile trop vite pour être lu, et votre propre session SSH apparaît dans la capture ; on l’exclut avec not port 22. Les options essentielles : -i ens18 choisit l’interface, -n évite la résolution des noms (plus rapide, plus lisible), -c 20 s’arrête après 20 paquets, -w capture.pcap enregistre dans un fichier.

Chaque ligne TCP affiche des drapeaux entre crochets. [S] : SYN, demande d’ouverture. [S.] : SYN-ACK, le serveur accepte. [.] : simple ACK. [P.] : données poussées. [F.] : fin de connexion. [R] ou [R.] : RST, refus ou coupure brutale. Savoir lire ces drapeaux permet de distinguer en une seconde un port fermé (le serveur répond RST) d’un port filtré (aucune réponse, les SYN se répètent).

Wireshark est l’interface graphique de référence pour analyser une capture. Sur un serveur sans écran, on capture avec tcpdump -w, on rapatrie le fichier .pcap avec scp, puis on l’ouvre dans Wireshark sur son poste. Wireshark décode chaque protocole couche par couche, reconstitue les échanges avec « Suivre le flux TCP » et permet des filtres d’affichage comme tcp.flags.syn == 1 && tcp.flags.ack == 0 ou http.response.code == 500.', exemple: '$ sudo tcpdump -ni ens18 -c 6 "tcp port 80 and host 192.168.10.54"\n10:14:02.118 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [S], seq 3308715651, win 64240\n10:14:02.118 IP 192.168.10.20.80 > 192.168.10.54.51544: Flags [S.], seq 1920458830, ack 3308715652\n10:14:02.119 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [.], ack 1\n10:14:02.119 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [P.], length 78: HTTP: GET / HTTP/1.1\n10:14:02.121 IP 192.168.10.20.80 > 192.168.10.54.51544: Flags [P.], length 3460: HTTP: HTTP/1.1 200 OK\n10:14:02.122 IP 192.168.10.54.51544 > 192.168.10.20.80: Flags [.], ack 3461', attention: 'Capturer du trafic, c’est potentiellement lire des données personnelles et des mots de passe transmis en clair. Sur un réseau d’entreprise ou d’établissement, on ne capture qu’avec une autorisation et on supprime les fichiers après analyse. C’est une obligation liée au RGPD et à la charte informatique.' }, + { t: 'Les compléments indispensables : dig, curl, nc', d: '', html: 'Trois outils finissent le diagnostic au niveau applicatif. dig interroge le DNS et montre la réponse brute : dig +short intranet.lycee.fr donne l’adresse, dig @1.1.1.1 intranet.lycee.fr interroge un serveur précis pour comparer, et le champ status: NXDOMAIN indique que le nom n’existe pas. Beaucoup de « pannes réseau » sont en réalité des pannes DNS.

curl joue le rôle du navigateur sans navigateur. curl -I affiche les en-têtes de réponse, curl -v détaille toute la négociation (résolution, connexion TCP, poignée de main TLS, requête, réponse), curl --resolve site.fr:443:192.168.10.20 https://site.fr teste un serveur précis en contournant le DNS.

nc (netcat) teste l’ouverture d’un port TCP sans protocole applicatif : nc -zv 192.168.10.20 3306 répond « succeeded » ou « Connection refused ». C’est le moyen le plus rapide de vérifier qu’un pare-feu laisse passer un flux entre deux serveurs.' } + ], + synthese: ['On diagnostique de bas en haut et on note chaque résultat', 'ip link / ip addr / ip route : lien, adresse, masque, passerelle', 'ping mesure joignabilité, latence, pertes ; ICMP peut être filtré', 'traceroute et mtr localisent l’endroit du problème ; seule compte la perte qui persiste jusqu’à la destination', 'ss -tlnp : le service écoute-t-il, et sur quelle adresse ?', 'ip neigh : une passerelle en FAILED oriente vers la couche 2', 'tcpdump tranche : RST = port fermé, silence = port filtré', 'dig, curl, nc terminent le diagnostic applicatif'], + qcm: [ + ['ip link affiche ens18 en UP mais sans LOWER_UP. Que faut-il vérifier en priorité ?', ['Le DNS', 'Le câble et le port du switch', 'Le pare-feu', 'Le service web'], 1, 'L’interface est activée mais ne détecte pas de lien physique.'], + ['Quelle commande indique la route et l’adresse source utilisées pour joindre 8.8.8.8, sans rien envoyer ?', ['ping 8.8.8.8', 'ip route get 8.8.8.8', 'traceroute 8.8.8.8', 'ss -r'], 1, 'ip route get interroge la table de routage.'], + ['Un ping renvoie « 100 % packet loss » vers un serveur web qui affiche pourtant bien son site. Explication la plus probable ?', ['Le serveur est éteint', 'ICMP est filtré par un pare-feu', 'Le DNS est en panne', 'Le câble est coupé'], 1, 'Le site répond, donc la machine est joignable ; seul ICMP est bloqué.'], + ['Sur quel mécanisme repose traceroute ?', ['Le numéro de séquence TCP', 'La décrémentation du TTL et les messages ICMP Time Exceeded', 'Les requêtes ARP', 'Le DNS inverse'], 1, 'Chaque routeur qui met le TTL à 0 se signale.'], + ['Dans mtr, le saut 4 affiche 40 % de pertes, les sauts 5 à 9 et la destination 0 %. Conclusion ?', ['Le lien du saut 4 est défaillant', 'Le routeur 4 limite ses réponses ICMP, pas de vraie perte', 'La destination est saturée', 'Il faut changer d’opérateur'], 1, 'Une perte qui ne se propage pas n’est pas une vraie perte.'], + ['ss -tlnp montre 127.0.0.1:8080 pour l’application. Les postes du réseau ne peuvent pas y accéder. Pourquoi ?', ['Le port 8080 est interdit', 'L’application n’écoute que sur la boucle locale', 'Il manque sudo', 'Le pare-feu est désactivé'], 1, 'Il faut la configurer pour écouter sur 0.0.0.0 ou l’IP du serveur.'], + ['Que signifie l’état SYN-SENT persistant dans ss ?', ['Connexion établie', 'On attend une réponse au SYN, souvent un pare-feu qui jette les paquets', 'Connexion fermée proprement', 'Le serveur a refusé'], 1, 'Un refus donnerait un RST immédiat.'], + ['ip neigh affiche la passerelle en FAILED. Où chercher ?', ['Au niveau DNS', 'Au niveau 2 : VLAN, câble, passerelle éteinte', 'Dans Apache', 'Dans le navigateur'], 1, 'La machine n’obtient même pas la MAC de son routeur.'], + ['Dans tcpdump, un SYN reçoit immédiatement une réponse [R.]. Le port est :', ['filtré', 'fermé', 'ouvert', 'chiffré'], 1, 'La machine répond activement qu’il n’y a rien sur ce port.'], + ['Pourquoi ajouter not port 22 à une capture tcpdump lancée en SSH ?', ['Pour aller plus vite', 'Pour ne pas capturer sa propre session', 'C’est obligatoire', 'Pour chiffrer la capture'], 1, 'Sinon chaque ligne affichée génère du trafic SSH capturé.'], + ['Quel est le rôle de l’option -n de tcpdump et ss ?', ['Capturer moins', 'Afficher les adresses et ports en numérique, sans résolution de noms', 'Ignorer IPv6', 'Afficher en couleur'], 1, 'Plus rapide et plus lisible pour le diagnostic.'], + ['Deux MAC différentes répondent à arping pour la même IP. C’est :', ['normal', 'un conflit d’adresse ou une usurpation ARP', 'un problème DNS', 'un effet du Wi-Fi'], 1, 'À traiter immédiatement.'] + ], + td: [ + { n: 'Échauffement', c: 'Pour chaque symptôme, indiquer la couche concernée et la première commande à lancer : (a) « la carte réseau n’a pas de voyant », (b) « je n’ai pas d’adresse IP », (c) « je ping la passerelle mais pas Internet », (d) « le site s’ouvre par son IP mais pas par son nom ».', r: '(a) couches 1-2 · ip link\n(b) couche 3 locale · ip addr puis journal DHCP\n(c) couche 3 au-delà de la passerelle · ip route puis traceroute\n(d) couche 7 · dig nom-du-site' }, + { n: 'Échauffement', c: 'Interpréter la ligne : « 64 bytes from 10.0.0.5: icmp_seq=7 ttl=121 time=38.2 ms ». Que peut-on dire du système distant et de la distance ?', r: 'TTL initial probablement 128 (Windows) : 128 − 121 = 7 routeurs traversés. RTT de 38 ms aller-retour.' }, + { n: 'Application', c: 'Un serveur répond au ping mais le site ne s’affiche pas. Écrire la suite de commandes, sur le serveur, qui permet de savoir si Apache tourne, sur quel port et sur quelle adresse il écoute, et s’il répond localement.', r: 'systemctl status apache2\nsudo ss -tlnp | grep -E ":80|:443"\ncurl -I http://127.0.0.1\n(puis, depuis un poste client : curl -I http://IP-du-serveur)' }, + { n: 'Application', c: 'Écrire la commande tcpdump qui capture, sur ens18, les 50 premiers paquets échangés avec 192.168.10.54 sur le port 443, sans résolution de noms, dans le fichier https.pcap.', r: 'sudo tcpdump -ni ens18 -c 50 -w https.pcap "host 192.168.10.54 and tcp port 443"' }, + { n: 'Application', c: 'À partir de la sortie mtr fournie par l’enseignant, déterminer si la perte vient du réseau local, de l’opérateur ou de la destination, et rédiger le message à envoyer au support de l’opérateur.', r: 'Repérer le premier saut où la perte apparaît ET persiste jusqu’à la destination. Si c’est un saut de l’opérateur, joindre la sortie mtr -rwc 100, l’heure, l’IP source et l’IP destination.' }, + { n: 'Approfondissement', c: 'Proposer une méthode, avec les commandes, pour distinguer un port fermé d’un port filtré par un pare-feu, depuis un client Linux.', r: 'nc -zv IP port : « refused » immédiat = fermé ; attente puis timeout = filtré.\nConfirmation : sudo tcpdump -ni ens18 host IP and port N → RST reçu (fermé) ou SYN répétés sans réponse (filtré).' }, + { n: 'Approfondissement', c: 'Un transfert de gros fichiers entre deux sites échoue alors que les petites requêtes passent. Formuler une hypothèse sur la MTU et la vérifier avec ping.', r: 'Hypothèse : un tunnel ou un lien réduit la MTU et les paquets ICMP « Fragmentation needed » sont filtrés.\nTest : ping -M do -s 1472 IP (1500 octets) puis réduire -s jusqu’à ce que ça passe, par exemple 1392 → MTU 1420.' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Deux VM Debian reliées, pannes injectées par l’enseignant, diagnostic vérifié étape par étape.', etapes: ['Relever l’état réseau complet avec ip -br link, ip -br addr, ip route', 'Mesurer latence et pertes vers la passerelle et vers Internet', 'Localiser une panne avec traceroute -n puis mtr', 'Trouver pourquoi le service web est injoignable avec ss et curl', 'Capturer une connexion avec tcpdump et identifier SYN, SYN-ACK, ACK', 'Rédiger le compte rendu d’incident'] }, + plusLoin: [['Documentation iproute2 (man ip, man ss)', 'https://man7.org/linux/man-pages/man8/ip.8.html'], ['Documentation officielle de tcpdump et des filtres pcap', 'https://www.tcpdump.org/manpages/pcap-filter.7.html'], ['Wireshark User’s Guide', 'https://www.wireshark.org/docs/wsug_html_chunked/'], ['RFC 792 · ICMP', 'https://www.rfc-editor.org/rfc/rfc792'], ['RFC 9293 · TCP', 'https://www.rfc-editor.org/rfc/rfc9293']] +}); + +window.CONTENUS_PLUS = window.CONTENUS_PLUS || {}; +window.CONTENUS_PLUS.diagnostic = { + cas: { titre: 'Mise en situation · Le lundi où l’ENT n’a plus répondu', texte: 'Lundi, 8 h 05. Le service informatique du lycée reçoit une dizaine d’appels : l’intranet pédagogique ne s’affiche plus, ni en salle de classe ni en salle des professeurs. Le serveur est une VM Debian hébergée sur l’hyperviseur de l’établissement. Le technicien s’y connecte en SSH, suit sa méthode couche par couche, découvre que le service web a bien redémarré après la mise à jour du week-end mais qu’il n’écoute plus que sur la boucle locale à cause d’une directive Listen modifiée, corrige la configuration, vérifie depuis un poste client puis rédige un compte rendu d’incident pour l’équipe. Temps d’indisponibilité : 25 minutes, dont 5 de diagnostic effectif.' }, + erreurs: ['Conclure qu’une machine est éteinte parce qu’elle ne répond pas au ping', 'Interpréter les étoiles d’un saut de traceroute comme une panne alors que les sauts suivants répondent', 'Lancer tcpdump sans filtre en SSH et capturer sa propre session', 'Oublier sudo avec ss -p : la colonne Process reste vide', 'Vérifier l’adresse IP sans vérifier le masque', 'Modifier la configuration avec ip addr add et oublier de la rendre permanente', 'Tout redémarrer avant d’avoir compris, et perdre les traces de la panne', 'Tester uniquement depuis le serveur lui-même (curl localhost) sans tester depuis un client'], + memo: [ + ['Commande', 'À quoi ça sert', 'Ce qu’on regarde'], + ['ip -br link', 'état des interfaces', 'UP et LOWER_UP'], + ['ip -br addr', 'adresses et masques', 'IP attendue, bon préfixe /24, /26…'], + ['ip route', 'table de routage', 'ligne default via '], + ['ip neigh', 'cache ARP', 'passerelle en REACHABLE, pas FAILED'], + ['ping -c 4 ', 'joignabilité, latence', '% packet loss, rtt avg et mdev'], + ['traceroute -n ', 'chemin saut par saut', 'dernier saut qui répond'], + ['mtr -rwc 100 ', 'pertes par saut dans la durée', 'perte qui persiste jusqu’à la destination'], + ['sudo ss -tlnp', 'services en écoute', 'port attendu, adresse 0.0.0.0 ou 127.0.0.1'], + ['sudo tcpdump -ni ', 'trafic réel', 'drapeaux [S] [S.] [R.], absence de réponse'], + ['dig +short ', 'résolution DNS', 'adresse renvoyée, NXDOMAIN'], + ['curl -Iv ', 'réponse applicative', 'connexion, TLS, code HTTP'], + ['nc -zv ', 'test d’ouverture de port', 'succeeded, refused ou timeout'] + ], + resolu: { enonce: 'Le serveur web intranet (192.168.10.20) ne répond plus depuis les postes du lycée. Diagnostiquer et rétablir le service.', etapes: [ + ['1 · Qualifier le symptôme depuis un client', 'curl -I --max-time 5 http://192.168.10.20\ncurl: (7) Failed to connect to 192.168.10.20 port 80: Connection refused\n→ « Connection refused » est une information précieuse : la machine a répondu, et elle a refusé. Elle est donc allumée et joignable. Un timeout aurait orienté vers un problème réseau ou un pare-feu.'], + ['2 · Confirmer la couche 3', 'ping -c 3 192.168.10.20\n3 packets transmitted, 3 received, 0% packet loss\n→ Réseau et routage fonctionnent. Inutile de regarder les câbles, les switchs ou le DHCP. On passe sur le serveur.'], + ['3 · Vérifier l’état réseau du serveur', 'ssh admin@192.168.10.20\nip -br addr\nens18 UP 192.168.10.20/24\nip route | head -1\ndefault via 192.168.10.1 dev ens18\n→ Adresse, masque et passerelle corrects. Le problème est au-dessus de la couche 3.'], + ['4 · Le service tourne-t-il et où écoute-t-il ?', 'systemctl is-active apache2\nactive\nsudo ss -tlnp | grep apache2\nLISTEN 0 511 127.0.0.1:80 0.0.0.0:* users:(("apache2",pid=1204,fd=4))\n→ Voilà la cause : Apache tourne, mais n’écoute que sur 127.0.0.1. Depuis le réseau, le port 80 est fermé, d’où le refus.'], + ['5 · Trouver l’origine et corriger', 'grep -rn "^Listen" /etc/apache2/\n/etc/apache2/ports.conf:5:Listen 127.0.0.1:80\nsudo sed -i "s/^Listen 127.0.0.1:80/Listen 80/" /etc/apache2/ports.conf\nsudo apachectl configtest && sudo systemctl restart apache2\nsudo ss -tlnp | grep apache2\nLISTEN 0 511 *:80 *:*\n→ La directive avait été modifiée lors de la maintenance du week-end. On valide la syntaxe avant de redémarrer.'], + ['6 · Vérifier depuis un client et documenter', 'curl -I http://192.168.10.20\nHTTP/1.1 200 OK\n→ Service rétabli. Compte rendu : heure de détection, symptôme, commandes et sorties clés, cause (directive Listen), correction, heure de rétablissement, action préventive (relecture des changements de configuration avant maintenance, supervision du port 80 depuis l’extérieur).'] + ] }, + vf: [['Un ping sans réponse prouve que la machine est éteinte.', 0, 'ICMP est souvent filtré.'], ['Des étoiles au saut 3 de traceroute, puis des réponses aux sauts 4 et 5, ne sont pas une panne.', 1, 'Le routeur 3 ne répond simplement pas à ICMP.'], ['127.0.0.1:3306 dans ss signifie que MariaDB est accessible depuis le réseau.', 0, 'Seulement depuis la machine elle-même.'], ['Une réponse RST à un SYN signifie que le port est fermé.', 1, 'Un port filtré ne répond pas du tout.']], + examen: { titre: 'Sujet type · 20 points', q: ['Donner, dans l’ordre, les cinq vérifications d’une démarche de diagnostic par couches, avec une commande pour chacune. (5 pts)', 'Interpréter la sortie ping fournie : pertes, latence moyenne, gigue, et conclusion sur la qualité du lien. (3 pts)', 'Expliquer le fonctionnement de traceroute et interpréter une ligne « * * * ». (4 pts)', 'Un service écoute en 127.0.0.1:8080. Expliquer la conséquence et la correction. (3 pts)', 'Écrire la commande tcpdump qui capture les SYN vers le port 443 de 10.0.0.5, puis expliquer comment distinguer un port fermé d’un port filtré. (5 pts)'], r: ['ip link · ip addr / ip route · ping passerelle puis destination · ss -tlnp · curl / dig', 'Selon la sortie : % packet loss, avg, mdev', 'TTL croissant, ICMP Time Exceeded ; étoiles = pas de réponse du saut, pas forcément une panne', 'Injoignable depuis le réseau ; écouter sur 0.0.0.0:8080 ou l’IP du serveur', 'sudo tcpdump -ni ens18 "dst host 10.0.0.5 and tcp dst port 443 and tcp[tcpflags] & tcp-syn != 0" · RST = fermé, pas de réponse = filtré'] }, + prof: { deroule: [['Ouverture', 'Scénario réel : « l’ENT ne répond plus », la classe propose des hypothèses, on les classe par couche'], ['Cours', 'ip, ping, traceroute, avec démonstration en direct sur une VM projetée'], ['Cours', 'ss, ARP, tcpdump ; lecture commentée d’une capture'], ['Pratique', 'Lab VM : pannes injectées (mauvais masque, passerelle absente, service sur 127.0.0.1, pare-feu)'], ['Clôture', 'Quiz flash, puis chaque binôme présente un diagnostic en 2 minutes']], diff: 'Élèves en difficulté : fiche mémo imprimée, un seul outil par panne, indices débloqués après 3 essais. Élèves avancés : analyse d’une capture dans Wireshark, diagnostic MTU, rédaction d’un compte rendu d’incident au format ITIL.', eval: 'Chaque panne du lab est validée automatiquement dans la VM. Le compte rendu d’incident est évalué sur la rigueur (commandes, sorties, cause, correction, prévention). Sujet type sur 20.' } +}; + +window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {}; +window.CONTENUS_EXEMPLES.diagnostic = [ + ['Diagnostiquer un port bloqué avec ss', 'L’application métier est installée, mais aucun poste n’y accède sur le port 8080.', 'sudo ss -tlnp "sport = :8080"', 'State Recv-Q Send-Q Local Address:Port Peer Address:Port Process\nLISTEN 0 4096 127.0.0.1:8080 0.0.0.0:* users:(("java",pid=2214,fd=38))', 'Le processus Java écoute bien, mais uniquement sur 127.0.0.1 : il est invisible depuis le réseau. Il faut changer l’adresse d’écoute dans sa configuration (server.address=0.0.0.0 pour une application Spring, par exemple), puis vérifier que le pare-feu autorise le port 8080. Si la ligne avait affiché 0.0.0.0:8080, on aurait orienté la recherche vers le pare-feu avec nc -zv depuis un client.'], + ['Mesurer une perte de paquets', 'Les visioconférences de la salle B204 coupent régulièrement.', 'ping -c 100 -i 0.2 -q 192.168.20.1', '--- 192.168.20.1 ping statistics ---\n100 packets transmitted, 93 received, 7% packet loss, time 19821ms\nrtt min/avg/max/mdev = 0.9/14.6/212.4/31.8 ms', '7 % de pertes vers la passerelle locale, c’est énorme : sur un réseau filaire sain, on attend 0 %. La gigue (mdev) de 31,8 ms et un maximum à 212 ms confirment un lien instable. -q n’affiche que le résumé, -i 0.2 envoie 5 paquets par seconde. Suite du diagnostic : ip -s link pour chercher des erreurs sur l’interface, puis vérification du câble ou de la borne Wi-Fi.'], + ['Lire une poignée de main TCP', 'Vérifier qu’une connexion au serveur de base de données s’établit.', 'sudo tcpdump -ni ens18 -c 3 "host 192.168.10.30 and tcp port 5432"', '09:41:12.004 IP 192.168.10.20.40818 > 192.168.10.30.5432: Flags [S], seq 17290431, win 64240, options [mss 1460]\n09:41:12.005 IP 192.168.10.30.5432 > 192.168.10.20.40818: Flags [S.], seq 91022876, ack 17290432, win 65160\n09:41:12.005 IP 192.168.10.20.40818 > 192.168.10.30.5432: Flags [.], ack 1, win 502', 'Les trois temps de l’ouverture TCP sont visibles : [S] le client demande, [S.] le serveur accepte en acquittant le numéro de séquence + 1, [.] le client confirme. La connexion est établie en 1 ms. Le port source 40818 est choisi au hasard par le client, le port 5432 est celui de PostgreSQL. Si le problème persiste, il est donc applicatif (authentification, base inexistante), pas réseau.'], + ['Distinguer port fermé et port filtré', 'Un collègue affirme que « le pare-feu bloque le port 3306 ».', 'nc -zv -w 3 192.168.10.30 3306\nnc -zv -w 3 192.168.10.30 3307', 'nc: connect to 192.168.10.30 port 3306 (tcp) failed: Connection refused\nnc: connect to 192.168.10.30 port 3307 (tcp) timed out: Operation now in progress', 'Deux comportements différents. Sur 3306, « refused » immédiat : la machine a répondu par un RST, donc aucun pare-feu ne bloque, mais aucun service n’écoute ; il faut regarder MariaDB (arrêté ou en écoute sur 127.0.0.1). Sur 3307, « timed out » : aucun retour, les paquets sont jetés en route, c’est la signature d’un pare-feu. Deux commandes suffisent à trancher le débat.'], + ['Localiser une panne avec traceroute', 'Le serveur de sauvegarde distant (site B) est injoignable depuis le site A.', 'traceroute -n -T -p 22 10.50.0.10', 'traceroute to 10.50.0.10, 30 hops max\n 1 192.168.10.1 0.48 ms 0.44 ms 0.41 ms\n 2 10.255.0.1 2.31 ms 2.28 ms 2.40 ms\n 3 10.254.8.2 6.12 ms 6.05 ms 6.20 ms\n 4 * * *\n 5 * * *\n 6 * * *', 'Les trois premiers sauts répondent : réseau local, routeur du site, puis entrée du lien inter-sites. Plus rien au-delà du saut 3, et la destination ne répond pas non plus : le problème se situe sur le lien entre le site A et le site B ou sur le routeur du site B. -T -p 22 envoie des SYN TCP vers le port SSH, qui passent les pare-feux mieux que l’UDP par défaut. On transmet cette sortie à l’équipe réseau ou à l’opérateur.'], + ['Repérer un problème de voisinage ARP', 'Un serveur fraîchement déplacé dans une autre baie n’a plus aucun accès réseau.', 'ip neigh show dev ens18\nping -c 2 192.168.30.1', '192.168.30.1 dev ens18 FAILED\nFrom 192.168.30.15 icmp_seq=1 Destination Host Unreachable\nFrom 192.168.30.15 icmp_seq=2 Destination Host Unreachable', 'La passerelle est en FAILED : aucune réponse ARP. Le message « Destination Host Unreachable » émis par la machine elle-même (From 192.168.30.15) confirme qu’elle n’a jamais obtenu la MAC du routeur. Adresse et route peuvent être parfaites, rien ne sortira. La cause est presque toujours au niveau 2 : port du switch dans le mauvais VLAN après le déplacement, ou câble branché sur la mauvaise prise. Vérification côté switch : show mac address-table interface .'] +]; + +window.CONTENUS_LONG = window.CONTENUS_LONG || {}; +var K = 'stroke="#000" stroke-width="3"', F = 'font-family="Archivo,sans-serif" font-weight="800"'; +var bx = function (x, y, w, h, c, t) { return '' + t + ''; }; +window.CONTENUS_LONG.diagnostic = { + figure: '' + [['7 · Application', 'curl · dig', '#FF6FB5'], ['4 · Transport', 'ss · nc', '#C4B5FD'], ['3 · Réseau', 'ping · traceroute · mtr · ip route', '#7CF2C0'], ['2 · Liaison', 'ip neigh · arping', '#FFD23F'], ['1 · Physique', 'ip link · ip -s link', '#F6F5F1']].map(function (r, i) { return bx(20, 10 + i * 56, 220, 48, r[2], r[0]) + bx(260, 10 + i * 56, 440, 48, '#fff', r[1]); }).join('') + '', + legende: 'Chaque outil répond à une couche. On remonte de bas en haut, tcpdump observe toutes les couches à la fois.', + photo: 'Photo : baie serveur avec switchs et câbles RJ45 étiquetés', + lecture: [ + 'Sur un poste de travail, quand Internet ne fonctionne plus, on regarde l’icône réseau, on ouvre les paramètres, on clique sur « Résoudre les problèmes ». Sur un serveur, rien de tout cela n’existe. Les machines qui font tourner les ENT, les sites des établissements, les bases de données de vie scolaire ou les services des hébergeurs français sont installées sans interface graphique, pour des raisons de sécurité, de stabilité et de performances : chaque logiciel installé en plus est une faille potentielle et consomme de la mémoire. L’administrateur s’y connecte en SSH, et tout se fait au clavier.', + 'Ces outils en ligne de commande ont un autre avantage décisif : leur sortie est du texte. On peut la copier dans un ticket, l’envoyer à un collègue, la comparer avec celle de la veille, la filtrer avec grep ou l’intégrer dans un script de supervision qui alerte automatiquement quand une valeur dérape. Une capture d’écran d’une fenêtre de paramètres ne se prête à rien de tout cela. Dans une équipe d’exploitation, on attend d’un technicien qu’il fournisse les commandes et les sorties qui prouvent son diagnostic, pas une impression.', + 'Ils ont aussi l’avantage d’être partout. ip, ping, ss et tcpdump sont présents ou installables en une commande sur toutes les distributions Linux, dans les conteneurs, sur les routeurs et les pare-feux basés sur Linux, et leurs équivalents existent sur Windows Server et sur les équipements réseau. Ce que vous apprenez ici se transpose directement sur un switch Cisco, un pare-feu pfSense ou une instance cloud chez Scaleway ou OVHcloud.', + 'La compétence visée n’est pas de connaître toutes les options, elles sont dans le manuel. C’est de savoir quel outil lancer pour quelle question, et de savoir lire ce qu’il répond, y compris ce qu’il ne dit pas. Un ping sans réponse ne dit pas qu’une machine est éteinte. Des étoiles dans un traceroute ne disent pas qu’un lien est coupé. Un service « active » dans systemctl ne dit pas qu’il est joignable. Savoir faire ces nuances, c’est ce qui distingue un technicien qui cherche au hasard d’un technicien qui résout un incident en cinq minutes.' + ] +}; +window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {}; +window.CONTENUS_SCHEMAS.diagnostic = { + termes: ['ip link', 'ip addr', 'ip route', 'ip route get', 'ip neigh', 'ip -s link', 'ip -br', 'ping', 'traceroute', 'mtr', 'ss -tlnp', 'ss', 'netstat', 'ifconfig', 'arping', 'tcpdump', 'Wireshark', 'dig', 'curl', 'nc -zv', 'ICMP', 'TTL', 'RTT', 'ARP', 'SYN', 'SYN-ACK', 'RST', 'ACK', 'LISTEN', 'ESTAB', 'SYN-SENT', 'TIME-WAIT', 'CLOSE-WAIT', 'REACHABLE', 'STALE', 'FAILED', 'INCOMPLETE', 'LOWER_UP', 'MTU', '127.0.0.1', '0.0.0.0', 'BPF', 'Time Exceeded', 'Echo Request', 'Echo Reply'], + figs: [ + [3, 'traceroute : le TTL augmente d’un cran à chaque essai', '' + bx(10, 90, 110, 48, '#FFD23F', 'Poste') + [1, 2, 3].map(function (n, i) { return bx(160 + i * 150, 90, 110, 48, '#F6F5F1', 'Routeur ' + n) + 'TTL=' + n + 'Time Exceeded'; }).join('') + bx(610, 90, 100, 48, '#7CF2C0', 'Cible') + 'réponse'], + [4, 'Lire la colonne Local Address de ss', '' + bx(20, 20, 220, 50, '#7CF2C0', '0.0.0.0:80') + 'toutes les interfaces : joignable' + bx(260, 20, 220, 50, '#FFD23F', '192.168.10.20:80') + 'une seule interface' + bx(500, 20, 200, 50, '#FF6FB5', '127.0.0.1:80') + 'boucle locale : invisible'], + [6, 'Port fermé ou port filtré, vu par tcpdump', '' + bx(20, 20, 330, 44, '#F6F5F1', 'Fermé : [S] → [R.] immédiat') + bx(20, 80, 330, 44, '#FF6FB5', 'Filtré : [S] … [S] … [S] sans réponse') + bx(380, 20, 320, 44, '#7CF2C0', 'Ouvert : [S] → [S.] → [.]') + 'nc -zv : succeeded / refused / timed out'] + ] +}; +})(); diff --git a/portail/public/js/contenus-infra-3.js b/portail/public/js/contenus-infra-3.js new file mode 100644 index 0000000..5a872de --- /dev/null +++ b/portail/public/js/contenus-infra-3.js @@ -0,0 +1,172 @@ +// Cours complets · Infra lot 3 : Kubernetes, supervision, annuaire LDAP / Active Directory +(function () { +var reg = function (c) { + (window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours); + (window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus; + (window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples; + (window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] }; +}; + +reg({ +cours: { id: 'k8s', match: /Kubernetes|k8s|orchestr/i, + titre: 'Kubernetes : orchestrer des conteneurs en production', duree: '', + niveau: 'BUT R&T DevCloud · BUT Informatique · AIS · Master', + prerequis: ['Conteneurs Docker', 'Docker Compose', 'Réseau : DNS, NAT, équilibrage'], + competences: ['BUT R&T DevCloud · Déployer et exploiter une plateforme de conteneurs', 'AIS · Administrer une infrastructure cloud', 'Master · Architecture des systèmes distribués'], + intro: 'Compose pilote une machine. Quand l’application doit tenir la charge de milliers d’utilisateurs, survivre à la panne d’un serveur et se mettre à jour sans coupure, il faut un orchestrateur. Kubernetes répartit les conteneurs sur un groupe de machines, les relance quand ils tombent, les déplace quand un nœud disparaît et ajuste leur nombre à la demande. Toutes les offres cloud françaises (Scaleway Kapsule, OVHcloud Managed Kubernetes, Outscale) le proposent. Ce module présente son architecture, ses objets principaux et le diagnostic d’un déploiement en panne.', + cm: [ + { t: 'Architecture d’un cluster', d: '', html: 'Un cluster se compose d’un plan de contrôle et de nœuds de travail. Le plan de contrôle comprend l’API server, point d’entrée unique de toutes les commandes ; etcd, base clé-valeur qui stocke l’état du cluster ; le scheduler, qui choisit le nœud de chaque nouveau pod ; et le controller manager, qui fait converger l’état réel vers l’état désiré. Chaque nœud exécute kubelet, qui démarre les conteneurs via un moteur (containerd), et kube-proxy ou un équivalent qui programme le réseau des services.

Le principe central est la boucle de réconciliation : on déclare un état (« 3 répliques de cette image ») et des contrôleurs corrigent sans cesse tout écart. On ne dit jamais « démarre un conteneur », on décrit ce qui doit exister.' }, + { t: 'Pods, Deployments et ReplicaSets', d: '', html: 'Le pod est la plus petite unité : un ou plusieurs conteneurs qui partagent une adresse IP et des volumes. On ne crée presque jamais un pod directement : un Deployment décrit l’image, le nombre de répliques et la stratégie de mise à jour, et crée un ReplicaSet qui maintient ce nombre de pods. Une mise à jour d’image crée un nouveau ReplicaSet et fait basculer progressivement les pods (rolling update) ; kubectl rollout undo revient à la version précédente.', exemple: 'apiVersion: apps/v1\nkind: Deployment\nmetadata: { name: resa, namespace: vie-scolaire }\nspec:\n replicas: 3\n selector: { matchLabels: { app: resa } }\n template:\n metadata: { labels: { app: resa } }\n spec:\n containers:\n - name: app\n image: registre.lycee.local/resa:9c1e4f2\n ports: [{ containerPort: 8000 }]\n resources:\n requests: { cpu: 100m, memory: 128Mi }\n limits: { memory: 256Mi }\n readinessProbe: { httpGet: { path: /health, port: 8000 } }\n livenessProbe: { httpGet: { path: /health, port: 8000 }, initialDelaySeconds: 10 }' }, + { t: 'Services et Ingress', d: '', html: 'Les pods sont éphémères et changent d’adresse. Un Service fournit une adresse stable et un nom DNS (resa.vie-scolaire.svc.cluster.local) et répartit le trafic entre les pods dont les labels correspondent à son sélecteur. Le type ClusterIP est interne, NodePort ouvre un port sur chaque nœud, LoadBalancer demande un équilibreur au fournisseur cloud.

Pour le trafic HTTP, un Ingress route selon le nom d’hôte et le chemin vers les services, et termine TLS. Il nécessite un contrôleur d’Ingress (ingress-nginx, Traefik) ; cert-manager y ajoute des certificats Let’s Encrypt automatiques.', attention: 'Un Service qui ne sélectionne aucun pod (faute de frappe dans un label) ne renvoie aucune erreur à sa création : il n’a simplement aucun endpoint. kubectl get endpoints le révèle immédiatement.' }, + { t: 'Configuration, secrets et stockage', d: '', html: 'Les ConfigMap portent la configuration non sensible, les Secret les mots de passe et clés ; ils s’injectent en variables ou en fichiers. Les Secret ne sont qu’encodés en base64 par défaut : on active le chiffrement d’etcd et on limite leur lecture par RBAC.

Le stockage persistant passe par des PersistentVolumeClaim : le pod demande « 10 Gi en lecture-écriture », et une StorageClass provisionne le volume chez le fournisseur. Pour une base de données, on utilise un StatefulSet, qui donne à chaque réplique un nom et un volume stables.' }, + { t: 'Sondes, ressources et haute disponibilité', d: '', html: 'La readinessProbe indique si un pod peut recevoir du trafic : tant qu’elle échoue, le Service ne lui envoie rien. La livenessProbe indique s’il est vivant : en cas d’échec répété, kubelet redémarre le conteneur. Confondre les deux provoque des redémarrages en boucle au démarrage d’une application lente.

Les requests servent au scheduler pour placer les pods ; les limits plafonnent la consommation. Un conteneur qui dépasse sa limite mémoire est tué (OOMKilled). Le HorizontalPodAutoscaler ajuste le nombre de répliques selon la charge, et un PodDisruptionBudget garantit un nombre minimal de pods pendant une maintenance.' }, + { t: 'Accès et sécurité', d: '', html: 'L’accès à l’API passe par des certificats ou des jetons, et les droits par RBAC : Role et ClusterRole définissent des verbes sur des ressources, RoleBinding les attribue. Les namespaces séparent les équipes ou les environnements. Les NetworkPolicy filtrent le trafic entre pods : sans elles, tout pod peut joindre tout autre pod. Les Pod Security Standards interdisent les conteneurs privilégiés ou en root.' } + ], + synthese: ['Plan de contrôle (API server, etcd, scheduler, contrôleurs) et nœuds (kubelet)', 'État désiré et boucle de réconciliation', 'Deployment → ReplicaSet → pods ; rollout et rollout undo', 'Service stable par labels, Ingress pour HTTP et TLS', 'ConfigMap, Secret, PVC et StatefulSet', 'readiness ≠ liveness ; requests et limits', 'RBAC, namespaces, NetworkPolicy'], + qcm: [ + ['Quel composant stocke l’état du cluster ?', ['kubelet', 'etcd', 'kube-proxy', 'Ingress'], 1, 'Base clé-valeur répliquée.'], + ['Que se passe-t-il si l’on supprime un pod géré par un Deployment ?', ['Il disparaît définitivement', 'Le ReplicaSet en recrée un', 'Le Deployment est supprimé', 'Le cluster s’arrête'], 1, 'Réconciliation.'], + ['Comment un Service trouve-t-il ses pods ?', ['Par leur IP', 'Par sélecteur de labels', 'Par leur nom', 'Par le nœud'], 1, 'Les endpoints suivent les labels.'], + ['Une readinessProbe qui échoue provoque :', ['un redémarrage', 'le retrait du pod du Service', 'la suppression du nœud', 'rien'], 1, 'La liveness, elle, redémarre.'], + ['Que signifie OOMKilled ?', ['Réseau coupé', 'Dépassement de la limite mémoire', 'Image absente', 'Droits insuffisants'], 1, 'Le noyau tue le conteneur.'], + ['Quel objet pour une base de données répliquée ?', ['Deployment', 'StatefulSet', 'DaemonSet', 'Job'], 1, 'Identités et volumes stables.'], + ['Sans NetworkPolicy, les pods :', ['sont isolés', 'peuvent tous se joindre', 'n’ont pas de réseau', 'ne sortent pas'], 1, 'Tout est ouvert par défaut.'] + ], + td: [ + { n: 'Application', c: 'Écrire le Service ClusterIP et l’Ingress qui publient le Deployment resa sur resa.lycee.fr en HTTPS.', r: 'Service : selector app: resa, port 80 → targetPort 8000.\nIngress : host resa.lycee.fr, path /, backend service resa port 80, tls secretName resa-tls, annotation cert-manager.io/cluster-issuer.' }, + { n: 'Application', c: 'Mettre à jour l’image vers le tag 51ab0d7, suivre le déploiement, puis revenir en arrière.', r: 'kubectl -n vie-scolaire set image deploy/resa app=registre.lycee.local/resa:51ab0d7\nkubectl -n vie-scolaire rollout status deploy/resa\nkubectl -n vie-scolaire rollout undo deploy/resa' }, + { n: 'Approfondissement', c: 'Écrire une NetworkPolicy qui n’autorise vers la base PostgreSQL que les pods labellisés app: resa.', r: 'podSelector: { matchLabels: { app: postgres } }, policyTypes: [Ingress], ingress: [{ from: [{ podSelector: { matchLabels: { app: resa } } }], ports: [{ port: 5432 }] }]' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un cluster k3s à trois nœuds.', etapes: ['Déployer l’application en 3 répliques', 'La publier par Service et Ingress', 'Tuer un pod et observer la réconciliation', 'Mettre à jour puis revenir en arrière', 'Isoler la base par NetworkPolicy'] }, + plusLoin: [['Documentation Kubernetes (fr)', 'https://kubernetes.io/fr/docs/home/'], ['k3s', 'https://k3s.io/'], ['ANSSI · Recommandations de sécurité relatives au déploiement de conteneurs', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-au-deploiement-de-conteneurs-docker']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Après la mise à jour de l’application de réservation, le site renvoie « 503 Service Temporarily Unavailable ». Diagnostiquer et rétablir.', etapes: [ + ['Regarder les pods', 'kubectl -n vie-scolaire get pods -l app=resa\nNAME READY STATUS RESTARTS\nresa-7d9f6c5b8-2xkq4 0/1 CrashLoopBackOff 6\nresa-7d9f6c5b8-h7m2p 0/1 CrashLoopBackOff 6\n→ Aucun pod n’est prêt : l’Ingress n’a aucun backend, d’où les 503.'], + ['Lire les événements et les journaux', 'kubectl -n vie-scolaire describe pod resa-7d9f6c5b8-2xkq4 | tail -3\nWarning Unhealthy Liveness probe failed: Get "http://10.42.1.17:8000/health": connection refused\nWarning BackOff Back-off restarting failed container\nkubectl -n vie-scolaire logs resa-7d9f6c5b8-2xkq4 --previous | tail -1\nRunning database migrations… (this may take 40s)\n→ La nouvelle version exécute des migrations au démarrage ; la livenessProbe démarre à 10 s et tue le conteneur avant la fin.'], + ['Rétablir immédiatement', 'kubectl -n vie-scolaire rollout undo deploy/resa\nkubectl -n vie-scolaire rollout status deploy/resa\ndeployment "resa" successfully rolled out\n→ L’ancienne version est revenue ; le site répond.'], + ['Corriger la cause', 'Migrations déplacées dans un Job exécuté avant le déploiement, et ajout d’une startupProbe :\nstartupProbe: { httpGet: { path: /health, port: 8000 }, failureThreshold: 30, periodSeconds: 5 }\n→ La liveness n’est évaluée qu’une fois la startupProbe réussie.'], + ['Redéployer et documenter', 'kubectl apply -f resa.yaml ; kubectl -n vie-scolaire get pods -l app=resa\n3/3 Running\n→ Compte rendu : cause (liveness trop agressive face à des migrations), impact, rollback, correction, et test en préproduction avec une base de taille réelle.'] + ] }, + erreurs: ['Confondre readiness et liveness', 'Pas de requests ni de limits', 'Utiliser latest comme image', 'Laisser les Secret non chiffrés et lisibles par tous', 'Aucune NetworkPolicy', 'Faute de label entre Service et pods', 'Base de données en Deployment sans volume persistant'], + memo: [['Commande', 'Usage'], ['kubectl get pods -A -o wide', 'tous les pods et leur nœud'], ['kubectl describe pod

', 'événements, sondes, raisons d’échec'], ['kubectl logs

--previous', 'journaux du conteneur précédent'], ['kubectl get events --sort-by=.lastTimestamp', 'derniers événements'], ['kubectl get endpoints ', 'pods réellement derrière un Service'], ['kubectl rollout status|undo deploy/', 'suivre ou annuler un déploiement'], ['kubectl exec -it

-- sh', 'shell dans un pod'], ['kubectl top pods', 'consommation CPU et mémoire'], ['kubectl apply -f fichier.yaml', 'appliquer un état désiré'], ['kubectl auth can-i ', 'tester des droits RBAC']], + examen: { titre: 'Sujet type · 20 points', q: ['Décrire l’architecture d’un cluster. (4 pts)', 'Écrire Deployment, Service et Ingress pour une application donnée. (8 pts)', 'Expliquer readiness, liveness et startup probes. (3 pts)', 'Diagnostiquer un CrashLoopBackOff à partir de sorties fournies. (3 pts)', 'Citer deux mesures de sécurité. (2 pts)'], r: ['API server, etcd, scheduler, contrôleurs, kubelet', 'voir cours', 'trafic, redémarrage, démarrage lent', 'describe et logs --previous', 'RBAC, NetworkPolicy'] }, + prof: { deroule: [['Ouverture', 'Arrêter un serveur Compose en direct, puis un nœud Kubernetes'], ['Cours', 'Architecture et réconciliation'], ['Pratique', 'Lab k3s'], ['Incident', 'CrashLoopBackOff'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : manifestes à compléter. Élèves avancés : Helm, HPA, GitOps avec Argo CD.', eval: 'Lab validé, sujet type.' } }, +exemples: [ + ['Voir où tournent les pods', 'Un nœud va être mis en maintenance.', 'kubectl get pods -n vie-scolaire -o wide', 'NAME READY STATUS NODE\nresa-6b8c7d-4kxz2 1/1 Running noeud-1\nresa-6b8c7d-9mpq7 1/1 Running noeud-1\nresa-6b8c7d-tt2r8 1/1 Running noeud-2', 'Deux répliques sont sur noeud-1 : drainer ce nœud en coupe deux sur trois. Un PodDisruptionBudget minAvailable: 2 et une règle d’anti-affinité répartiraient mieux la charge.'], + ['Service sans endpoints', 'Le site renvoie 503 alors que les pods tournent.', 'kubectl -n vie-scolaire get endpoints resa', 'NAME ENDPOINTS AGE\nresa 3d', 'Aucun pod ne correspond au sélecteur du Service. On compare kubectl get svc resa -o yaml (selector app: resa) avec les labels des pods (app: reservation) : faute de nommage.'], + ['Conteneur tué pour mémoire', 'Redémarrages fréquents.', 'kubectl -n vie-scolaire describe pod resa-6b8c7d-4kxz2 | grep -A2 "Last State"', 'Last State: Terminated\n Reason: OOMKilled\n Exit Code: 137', 'La limite de 256 Mi est trop basse pour la charge actuelle. On mesure avec kubectl top pods puis on ajuste requests et limits.'], + ['Suivre une mise à jour', 'Déploiement d’une nouvelle version.', 'kubectl -n vie-scolaire rollout status deploy/resa', 'Waiting for deployment "resa" rollout to finish: 1 of 3 updated replicas are available...\ndeployment "resa" successfully rolled out', 'Les pods sont remplacés un à un ; le service reste disponible pendant la mise à jour.'], + ['Tester un droit RBAC', 'Un enseignant doit pouvoir lire les journaux de son namespace.', 'kubectl auth can-i get pods/log -n vie-scolaire --as prof-sio', 'yes', 'La commande vérifie les droits effectifs sans les tester réellement.'], + ['Lire les derniers événements', 'Pod en Pending.', 'kubectl -n vie-scolaire get events --sort-by=.lastTimestamp | tail -2', 'Warning FailedScheduling pod/db-0 0/3 nodes are available: 3 Insufficient memory.', 'Aucun nœud n’a assez de mémoire libre pour la request demandée : réduire la request ou ajouter un nœud.'] +], +termes: ['Kubernetes', 'cluster', 'pod', 'Deployment', 'ReplicaSet', 'StatefulSet', 'Service', 'ClusterIP', 'Ingress', 'ConfigMap', 'Secret', 'PersistentVolumeClaim', 'namespace', 'etcd', 'kubelet', 'API server', 'scheduler', 'readinessProbe', 'livenessProbe', 'startupProbe', 'requests', 'limits', 'OOMKilled', 'CrashLoopBackOff', 'RBAC', 'NetworkPolicy', 'kubectl', 'rollout', 'k3s'] +}); + +reg({ +cours: { id: 'supervision', match: /supervision|Zabbix|Prometheus|Grafana|monitoring|Nagios/i, + titre: 'Supervision : mesurer, alerter et anticiper avec Prometheus et Zabbix', duree: '', + niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR / AIS', + prerequis: ['Diagnostic réseau en ligne de commande', 'Services systemd'], + competences: ['BTS SIO SISR · Bloc 2 · Superviser une infrastructure', 'BTS CIEL · C09 · Maintenir', 'TSSR · Superviser l’infrastructure'], + intro: 'Un service informatique qui apprend une panne par un appel d’utilisateur a déjà perdu. La supervision mesure en permanence l’état des serveurs, des réseaux et des applications, déclenche une alerte avant que les utilisateurs ne soient gênés et conserve l’historique qui permet de comprendre un incident ou d’anticiper une saturation. Ce module présente les notions (métriques, seuils, alertes), puis les deux outils les plus répandus dans les établissements et les entreprises : Zabbix et le couple Prometheus-Grafana.', + cm: [ + { t: 'Que superviser ?', d: '', html: 'On distingue la disponibilité (le service répond-il ?), la performance (en combien de temps ?), la capacité (reste-t-il de la place, de la mémoire, de la bande passante ?) et les erreurs (taux de réponses 5xx, échecs de sauvegarde). Pour une application, la méthode des « quatre signaux d’or » résume l’essentiel : latence, trafic, erreurs, saturation.

On supervise à plusieurs niveaux : matériel (disques, température), système (CPU, mémoire, charge, espace disque), réseau (interfaces, pertes), services (ports, processus), et surtout expérience utilisateur : une sonde qui se connecte réellement à l’ENT et vérifie que la page de connexion s’affiche en moins de deux secondes.' }, + { t: 'Collecte : agent, SNMP, export', d: '', html: 'Trois modes de collecte coexistent. Un agent installé sur la machine (Zabbix agent, node_exporter) remonte des métriques système détaillées. SNMP interroge les équipements réseau qui ne peuvent pas recevoir d’agent : switchs, bornes, onduleurs, imprimantes ; on lit des valeurs identifiées par des OID dans des MIB. Les applications modernes exposent elles-mêmes leurs métriques sur une page HTTP (/metrics).

SNMPv1 et v2c transmettent la communauté (le « mot de passe ») en clair ; on utilise SNMPv3, avec authentification et chiffrement, ou à défaut on limite v2c en lecture seule à l’adresse du serveur de supervision.' }, + { t: 'Prometheus et Grafana', d: '', html: 'Prometheus vient chercher (pull) les métriques des cibles à intervalle régulier et les stocke en séries temporelles, chaque série étant identifiée par un nom et des labels (instance, job, mountpoint). Le langage PromQL interroge ces séries : taux, moyennes, prédictions. Les règles d’alerte sont évaluées par Prometheus et envoyées à Alertmanager, qui regroupe, déduplique et notifie par courriel, messagerie ou SMS. Grafana affiche les tableaux de bord.', exemple: '# Espace disque libre en %\n100 * node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}\n\n# Règle : disque plein dans moins de 24 h au rythme actuel\n- alert: DisquePleinSous24h\n expr: predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 24*3600) < 0\n for: 30m\n labels: { severity: warning }\n annotations: { summary: "{{ $labels.instance }} : disque plein sous 24 h" }' }, + { t: 'Zabbix', d: '', html: 'Zabbix est une solution intégrée très répandue dans les collectivités et les établissements : serveur, base de données, interface web et agents. On y définit des hôtes, auxquels on applique des modèles (templates) qui regroupent éléments (items, les valeurs collectées), déclencheurs (triggers, les conditions d’alerte) et graphiques. Les modèles fournis couvrent Linux, Windows, les principaux switchs et les services courants. La découverte automatique (LLD) crée les éléments pour chaque disque ou chaque interface détectée.' }, + { t: 'Bien alerter', d: '', html: 'Une alerte doit être actionnable : si personne ne doit rien faire en la recevant, ce n’est pas une alerte mais une information pour un tableau de bord. Trop d’alertes produisent la fatigue d’alerte : l’équipe ne les lit plus, et la vraie panne passe inaperçue.

Les bonnes pratiques : alerter sur les symptômes vus par l’utilisateur plutôt que sur chaque cause possible ; ajouter une durée (for: 5m) pour ignorer les pics d’une seconde ; utiliser des seuils prédictifs (plein dans 24 h) plutôt que fixes ; hiérarchiser (info, avertissement, critique) ; et regrouper les alertes dépendantes : si le routeur d’un site tombe, on veut une alerte, pas cinquante.' }, + { t: 'Journaux centralisés', d: '', html: 'Les métriques disent qu’il se passe quelque chose ; les journaux disent quoi. On les centralise pour les consulter en un seul endroit et les conserver même si un serveur est compromis : rsyslog ou journald vers un serveur central, ou des piles comme Loki avec Grafana, ou Elasticsearch. La centralisation est aussi une exigence de sécurité : l’ANSSI recommande de conserver et d’horodater les journaux d’authentification et d’administration.' } + ], + synthese: ['Disponibilité, performance, capacité, erreurs ; latence, trafic, erreurs, saturation', 'Agent, SNMP (v3), métriques exposées', 'Prometheus : pull, séries à labels, PromQL, Alertmanager ; Grafana pour l’affichage', 'Zabbix : hôtes, modèles, éléments, déclencheurs', 'Alertes actionnables, durée, seuils prédictifs, dépendances', 'Journaux centralisés et conservés'], + qcm: [ + ['Quel protocole pour superviser un switch sans agent ?', ['SSH', 'SNMP', 'SMTP', 'DHCP'], 1, 'Lecture d’OID.'], + ['Pourquoi préférer SNMPv3 ?', ['Plus rapide', 'Authentification et chiffrement', 'Plus de métriques', 'Obligatoire'], 1, 'v2c envoie la communauté en clair.'], + ['Prometheus collecte les métriques en mode :', ['push', 'pull', 'SNMP trap', 'courriel'], 1, 'Il interroge les cibles.'], + ['Que fait for: 5m dans une règle d’alerte ?', ['Répète l’alerte', 'N’alerte que si la condition dure 5 minutes', 'Supprime l’alerte', 'Envoie 5 messages'], 1, 'Ignore les pics.'], + ['Qu’est-ce qu’une alerte actionnable ?', ['Une alerte rouge', 'Une alerte qui demande une action précise', 'Une alerte fréquente', 'Une alerte par SMS'], 1, 'Sinon c’est du bruit.'], + ['Dans Zabbix, un déclencheur est :', ['une valeur collectée', 'une condition qui génère un problème', 'un graphique', 'un agent'], 1, 'Trigger.'] + ], + td: [ + { n: 'Application', c: 'Lister ce qu’il faut superviser pour l’ENT d’un lycée, du matériel à l’expérience utilisateur, avec un seuil pour chaque élément.', r: 'Disque < 15 % · mémoire > 90 % pendant 10 min · charge > nb de cœurs · port 443 fermé · certificat expirant < 14 j · page de connexion > 2 s · taux de 5xx > 2 % · sauvegarde de la nuit en échec.' }, + { n: 'Application', c: 'Écrire la requête PromQL du taux d’erreurs HTTP 5xx sur 5 minutes, et la règle d’alerte associée.', r: 'sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.02 avec for: 10m.' }, + { n: 'Approfondissement', c: 'Le routeur du site B tombe : 40 alertes arrivent en même temps. Proposer la configuration pour n’en recevoir qu’une.', r: 'Zabbix : dépendance des déclencheurs des hôtes du site B envers celui du routeur. Alertmanager : inhibit_rules qui masquent les alertes d’un site quand l’alerte « routeur injoignable » du même site est active.' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Prometheus, node_exporter, Alertmanager et Grafana sur deux VM.', etapes: ['Installer node_exporter', 'Déclarer la cible dans Prometheus', 'Créer un tableau de bord Grafana', 'Écrire une alerte disque prédictive', 'Remplir le disque et recevoir l’alerte'] }, + plusLoin: [['Documentation Prometheus', 'https://prometheus.io/docs/'], ['Documentation Zabbix', 'https://www.zabbix.com/documentation/current/fr/manual'], ['Google SRE Book · Monitoring Distributed Systems', 'https://sre.google/sre-book/monitoring-distributed-systems/']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Lundi 7 h 50 : l’alerte « DisquePleinSous24h » est arrivée à 2 h sur le serveur de fichiers, sans que personne ne réagisse. À 7 h 45 le disque est plein et les enseignants ne peuvent plus enregistrer. Rétablir puis fiabiliser la chaîne d’alerte.', etapes: [ + ['Rétablir le service', 'df -h /srv\n/dev/sdb1 500G 500G 0 100% /srv\nsudo du -xh --max-depth=2 /srv | sort -h | tail -3\n180G /srv/partage/tmp/exports-pronote\n→ Un script d’export mal configuré écrit un fichier toutes les minutes depuis vendredi.'], + ['Arrêter la cause et libérer de la place', 'sudo systemctl stop export-pronote.timer\nsudo find /srv/partage/tmp/exports-pronote -mtime +0 -delete\ndf -h /srv\n/dev/sdb1 500G 322G 178G 65% /srv\n→ Les enseignants peuvent de nouveau enregistrer.'], + ['Comprendre pourquoi l’alerte n’a servi à rien', 'amtool alert query DisquePleinSous24h\nalertname=DisquePleinSous24h severity=warning instance=fichiers:9100\n→ L’alerte a bien été émise, mais en severity warning, routée vers une adresse de courriel lue seulement en journée.'], + ['Corriger le routage', 'route:\n routes:\n - matchers: [severity="critical"]\n receiver: astreinte-sms\n# nouvelle règle : disque plein sous 6 h → critical\n→ L’alerte à 24 h reste un avertissement par courriel ; une seconde règle à 6 h devient critique et part vers l’astreinte.'], + ['Superviser la cause', '→ Ajout d’une alerte sur le nombre de fichiers créés par heure dans /srv/partage/tmp, et correction du script d’export (rotation des anciens fichiers). Compte rendu : l’outil avait détecté le problème six heures à l’avance ; le défaut était dans l’organisation de la réponse.'] + ] }, + erreurs: ['Alerter sur tout, sans hiérarchie', 'Seuils fixes sans durée', 'Aucune supervision de l’expérience utilisateur', 'SNMP v2c en écriture avec la communauté public', 'Alertes envoyées à une boîte que personne ne lit', 'Oublier de superviser les sauvegardes et les certificats'], + memo: [['Outil / commande', 'Usage'], ['curl -s localhost:9100/metrics | head', 'vérifier un node_exporter'], ['promtool check config prometheus.yml', 'valider la configuration'], ['promtool check rules regles.yml', 'valider les règles d’alerte'], ['amtool alert query', 'alertes actives dans Alertmanager'], ['snmpwalk -v3 -l authPriv … IF-MIB::ifOperStatus', 'lire l’état des ports d’un switch'], ['zabbix_get -s -k system.cpu.load', 'tester un élément Zabbix'], ['systemctl status zabbix-agent2', 'état de l’agent'], ['journalctl -u prometheus', 'journaux du serveur'], ['rate(), increase(), predict_linear()', 'fonctions PromQL essentielles']], + examen: { titre: 'Sujet type · 20 points', q: ['Proposer un plan de supervision pour un service donné. (6 pts)', 'Comparer agent, SNMP et métriques exposées. (4 pts)', 'Écrire deux règles d’alerte avec seuils et durées. (5 pts)', 'Analyser un incident de fatigue d’alerte. (5 pts)'], r: ['matériel → expérience utilisateur', 'voir cours', 'expr + for + severity', 'actionnable, hiérarchie, dépendances'] }, + prof: { deroule: [['Ouverture', 'Quelle panne avez-vous apprise par un utilisateur ?'], ['Cours', 'Quoi superviser, comment collecter'], ['Pratique', 'Lab Prometheus et Grafana'], ['Incident', 'Alerte émise mais ignorée'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : tableau de bord importé, une seule alerte. Élèves avancés : SNMPv3, Loki, inhibitions.', eval: 'Alerte reçue pendant le lab, sujet type.' } }, +exemples: [ + ['Vérifier une cible Prometheus', 'Un serveur n’apparaît plus dans Grafana.', 'curl -s http://prometheus:9090/api/v1/targets | jq -r \'.data.activeTargets[] | "\\(.labels.instance) \\(.health) \\(.lastError)"\'', 'fichiers:9100 up \nweb1:9100 down Get "http://web1:9100/metrics": dial tcp 10.62.40.21:9100: connect: connection refused', 'node_exporter ne tourne plus sur web1 (ou le pare-feu bloque 9100). systemctl status prometheus-node-exporter sur web1.'], + ['Lire l’état des ports d’un switch', 'Un bâtiment signale des coupures.', 'snmpwalk -v3 -l authPriv -u supervision -a SHA -A "…" -x AES -X "…" sw-bat-b IF-MIB::ifOperStatus | grep -v up', 'IF-MIB::ifOperStatus.10017 = INTEGER: down(2)', 'Le port 17 est tombé. Avec ifAlias, on retrouve qu’il s’agit de la liaison montante vers le bâtiment B.'], + ['Prédire la saturation', 'Planifier l’achat de disques.', 'predict_linear(node_filesystem_avail_bytes{instance="fichiers:9100",mountpoint="/srv"}[7d], 30*24*3600)', '-4.2e+10', 'Au rythme des sept derniers jours, il manquera environ 42 Go dans 30 jours : on commande avant l’incident.'], + ['Tester un élément Zabbix', 'Un élément reste vide dans l’interface.', 'zabbix_get -s 10.62.40.10 -k vfs.fs.size[/srv,pfree]', 'ZBX_NOTSUPPORTED: Cannot obtain filesystem information: [2] No such file or directory', 'Le point de montage n’existe pas sur cet hôte : le modèle appliqué ne correspond pas à la machine. On utilise la découverte automatique des systèmes de fichiers.'], + ['Voir les alertes actives', 'Point d’astreinte du matin.', 'amtool alert query --active', 'Alertname Starts At Summary\nCertificatExpire 2026-09-28 06:00 ent.lycee.fr expire dans 9 jours\nSauvegardeEchec 2026-09-30 03:12 sauvegarde nocturne de db1 en échec', 'Deux actions claires pour la journée : renouveler le certificat (vérifier certbot) et relancer la sauvegarde de db1.'], + ['Valider les règles avant rechargement', 'Nouvelle règle d’alerte.', 'promtool check rules /etc/prometheus/regles.yml', 'Checking /etc/prometheus/regles.yml\n FAILED:\n group "systeme", rule 3: could not parse expression: unexpected "}"', 'Une accolade en trop dans la règle 3. Sans ce contrôle, Prometheus aurait refusé de recharger toute la configuration.'] +], +termes: ['supervision', 'métrique', 'alerte', 'seuil', 'SNMP', 'SNMPv3', 'OID', 'MIB', 'agent', 'Prometheus', 'PromQL', 'node_exporter', 'Alertmanager', 'Grafana', 'Zabbix', 'déclencheur', 'modèle', 'predict_linear', 'rate', 'fatigue d’alerte', 'Loki', 'rsyslog'] +}); + +reg({ +cours: { id: 'annuaire', match: /LDAP|Active Directory|annuaire|AD DS|GPO|Samba/i, + titre: 'Annuaires : LDAP, Active Directory, GPO et Samba', duree: '', + niveau: 'BTS SIO SISR · BTS CIEL IR · BUT R&T · TSSR', + prerequis: ['DNS : résolution et zones', 'Droits et utilisateurs'], + competences: ['BTS SIO SISR · Bloc 2 · Administrer les services d’annuaire', 'BTS SIO · Bloc 3 · Gérer les identités et les accès', 'TSSR · Exploiter un annuaire'], + intro: 'Dans un établissement de 1 500 comptes, créer chaque utilisateur sur chaque serveur est impossible. L’annuaire centralise les identités, les groupes et les droits : un élève se connecte avec le même identifiant sur n’importe quel poste, au Wi-Fi, à l’imprimante et aux applications. Active Directory domine dans les établissements et les entreprises ; il repose sur des standards ouverts (LDAP, Kerberos, DNS) que l’on retrouve dans Samba et OpenLDAP. Ce module en décrit le fonctionnement, l’administration et la sécurisation.', + cm: [ + { t: 'LDAP : un arbre d’objets', d: '', html: 'LDAP (Lightweight Directory Access Protocol) est le protocole d’interrogation des annuaires. Les données forment un arbre (DIT) : chaque entrée a un DN (Distinguished Name) unique, lu de la feuille vers la racine, par exemple CN=Léa M.,OU=SIO1,OU=Eleves,DC=lycee,DC=local. Les entrées appartiennent à des classes d’objets (user, group, organizationalUnit) qui définissent leurs attributs (sAMAccountName, mail, memberOf). LDAP écoute sur le port 389, LDAPS (chiffré) sur 636.' }, + { t: 'Active Directory : domaine, forêt, contrôleurs', d: '', html: 'Un domaine AD regroupe comptes, groupes et ordinateurs sous une même autorité, avec un nom DNS (lycee.local, ou mieux un sous-domaine d’un domaine possédé : ad.lycee-exemple.fr). Les contrôleurs de domaine (DC) hébergent l’annuaire et le répliquent entre eux ; on en installe au moins deux. Plusieurs domaines peuvent former une forêt.

AD dépend entièrement du DNS : les clients trouvent les DC grâce aux enregistrements SRV (_ldap._tcp.dc._msdcs.lycee.local). Un poste qui n’utilise pas le DNS du domaine ne peut pas le rejoindre. L’authentification repose sur Kerberos, qui impose des horloges synchronisées à cinq minutes près.' }, + { t: 'Organiser : OU, groupes et délégation', d: '', html: 'Les unités d’organisation (OU) rangent les objets pour appliquer des stratégies et déléguer l’administration : OU Eleves, Professeurs, Administration, Ordinateurs\\Salles. Les groupes portent les droits. La méthode AGDLP résume la bonne pratique : les comptes (A) vont dans des groupes globaux par fonction (G, « Profs-SIO »), placés dans des groupes locaux de domaine par ressource (DL, « Partage-SIO-Modification »), auxquels on donne les permissions (P). On ne donne jamais un droit directement à un utilisateur.' }, + { t: 'Stratégies de groupe (GPO)', d: '', html: 'Une GPO applique des paramètres aux ordinateurs et aux utilisateurs d’une OU : fond d’écran, lecteurs réseau, imprimantes, mises à jour, pare-feu, restrictions, déploiement de logiciels. L’ordre d’application est LSDOU : local, site, domaine, OU (de la plus haute à la plus basse) ; la dernière appliquée l’emporte, sauf blocage ou application forcée. gpupdate /force applique immédiatement, gpresult /r montre les GPO effectivement reçues.' }, + { t: 'Samba et intégration Linux', d: '', html: 'Samba partage des fichiers et des imprimantes avec les postes Windows (protocole SMB) et peut même servir de contrôleur de domaine compatible AD, solution répandue dans les établissements attachés aux logiciels libres. Un serveur Linux rejoint un domaine AD avec realmd et SSSD (realm join) : les comptes du domaine s’y connectent en SSH et les groupes AD pilotent les droits sudo.', exemple: 'sudo realm discover lycee.local\nsudo realm join -U administrateur lycee.local\nid lea.martin@lycee.local\nuid=1854201103(lea.martin@lycee.local) … groupes=…(eleves-sio1@lycee.local)' }, + { t: 'Sécurité de l’annuaire', d: '', html: 'L’annuaire est la cible principale des attaquants : compromettre un compte d’administrateur du domaine, c’est posséder tout le réseau. Les recommandations de l’ANSSI s’organisent en niveaux (tiering) : les comptes d’administration du domaine ne se connectent qu’aux DC, jamais à un poste utilisateur. Autres mesures : comptes d’administration séparés des comptes du quotidien, mots de passe longs, LAPS pour des mots de passe administrateur local différents sur chaque poste, désactivation des protocoles anciens (NTLMv1, SMBv1), LDAPS ou signature LDAP, et audit régulier avec des outils comme PingCastle.' } + ], + synthese: ['LDAP : arbre, DN, classes et attributs ; 389 et 636', 'AD : domaine, forêt, au moins deux DC ; dépend du DNS et de Kerberos', 'OU pour organiser et appliquer, groupes pour les droits (AGDLP)', 'GPO appliquées en LSDOU ; gpresult pour vérifier', 'Samba, realmd et SSSD pour Linux', 'Tiering, comptes séparés, LAPS, protocoles anciens désactivés'], + qcm: [ + ['Que désigne un DN ?', ['Un nom DNS', 'Le chemin unique d’une entrée dans l’arbre LDAP', 'Un groupe', 'Un port'], 1, 'Distinguished Name.'], + ['Pourquoi un poste ne trouve-t-il pas le domaine ?', ['Pare-feu Windows', 'Il n’utilise pas le DNS du domaine', 'Mauvais fond d’écran', 'LAPS'], 1, 'Enregistrements SRV.'], + ['Quel écart d’horloge maximal tolère Kerberos par défaut ?', ['1 s', '5 min', '1 h', 'Aucun'], 1, 'D’où NTP obligatoire.'], + ['Où donner une permission sur un partage selon AGDLP ?', ['À l’utilisateur', 'À un groupe local de domaine', 'À l’OU', 'Au DC'], 1, 'Comptes → global → local → permission.'], + ['Quelle commande montre les GPO reçues par un poste ?', ['gpupdate', 'gpresult /r', 'ipconfig', 'netstat'], 1, 'Résultat de la stratégie.'], + ['Quel outil donne un mot de passe administrateur local différent à chaque poste ?', ['GPO', 'LAPS', 'SSSD', 'Kerberos'], 1, 'Local Administrator Password Solution.'] + ], + td: [ + { n: 'Application', c: 'Proposer l’arborescence d’OU et les groupes d’un lycée avec BTS SIO, sa salle de TP et le partage « Sujets » accessible en modification aux profs de SIO, en lecture aux élèves de SIO.', r: 'OU Utilisateurs\\Eleves\\SIO1, SIO2 · OU Utilisateurs\\Profs · OU Ordinateurs\\Salles\\B204 · groupes globaux G-Eleves-SIO, G-Profs-SIO · groupes locaux DL-Sujets-Lecture, DL-Sujets-Modif · permissions NTFS et partage sur ces DL.' }, + { n: 'Application', c: 'Écrire la requête ldapsearch qui liste les comptes de l’OU SIO1 avec leur adresse mail.', r: 'ldapsearch -H ldaps://dc1.lycee.local -D "cn=lecture,…" -W -b "OU=SIO1,OU=Eleves,DC=lycee,DC=local" "(objectClass=user)" sAMAccountName mail' }, + { n: 'Approfondissement', c: 'Expliquer pourquoi un administrateur du domaine ne doit pas ouvrir de session sur un poste d’élève.', r: 'Son jeton ou ses identifiants restent en mémoire du poste ; si celui-ci est compromis, l’attaquant les récupère et devient administrateur du domaine (modèle de tiering de l’ANSSI).' } + ], + tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un contrôleur Samba AD, un client Windows et un serveur Linux.', etapes: ['Provisionner le domaine Samba AD', 'Vérifier les enregistrements SRV', 'Créer OU, comptes et groupes', 'Joindre le serveur Linux avec realmd', 'Appliquer une GPO de lecteur réseau'] }, + plusLoin: [['ANSSI · Recommandations de sécurité relatives à Active Directory', 'https://cyber.gouv.fr/publications/recommandations-de-securite-relatives-active-directory'], ['Samba Wiki · Setting up Samba as an AD DC', 'https://wiki.samba.org/index.php/Setting_up_Samba_as_an_Active_Directory_Domain_Controller'], ['PingCastle', 'https://www.pingcastle.com/']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Ce matin, aucun élève de la salle B204 ne parvient à ouvrir de session (« La relation d’approbation entre cette station de travail et le domaine principal a échoué » sur certains postes, « L’heure du serveur diffère » sur d’autres). Les autres salles fonctionnent.', etapes: [ + ['Vérifier l’heure', 'Sur un poste de B204 (session locale) : w32tm /stripchart /computer:dc1.lycee.local /samples:1\n11:47:02, d:+00.0012s o:-00:07:41.3\n→ Le poste a 7 min 41 s de retard sur le DC : Kerberos refuse l’authentification au-delà de 5 minutes.'], + ['Trouver pourquoi', 'w32tm /query /source\nLocal CMOS Clock\n→ Les postes de B204, réinstallés pendant les vacances à partir d’une image, ne prennent plus l’heure du domaine.'], + ['Corriger l’heure', 'w32tm /config /syncfromflags:domhier /update\nnet stop w32time && net start w32time\nw32tm /resync\n→ Le poste se synchronise sur la hiérarchie du domaine.'], + ['Réparer la relation d’approbation', 'Sur les postes au message « relation d’approbation » :\nTest-ComputerSecureChannel -Repair -Credential lycee\\technicien\nTrue\n→ Le mot de passe du compte ordinateur, restauré depuis une image ancienne, ne correspondait plus à celui de l’annuaire ; le canal sécurisé est réinitialisé.'], + ['Généraliser et prévenir', '→ La correction est déployée par GPO sur l’OU Salles (source de temps du domaine) ; les images de poste sont préparées avec Sysprep et jointes au domaine après déploiement. Compte rendu rédigé.'] + ] }, + erreurs: ['Postes qui n’utilisent pas le DNS du domaine', 'Horloges non synchronisées', 'Un seul contrôleur de domaine', 'Droits donnés directement aux utilisateurs', 'Administrateur du domaine utilisé sur les postes', 'Même mot de passe administrateur local partout', 'LDAP simple non chiffré'], + memo: [['Commande', 'Usage'], ['nslookup -type=SRV _ldap._tcp.dc._msdcs.lycee.local', 'trouver les contrôleurs de domaine'], ['ldapsearch -H ldaps://… -b … "(filtre)"', 'interroger l’annuaire'], ['gpupdate /force', 'appliquer les GPO'], ['gpresult /r', 'GPO effectivement appliquées'], ['w32tm /query /status', 'source et écart de temps'], ['Test-ComputerSecureChannel -Repair', 'réparer la relation avec le domaine'], ['samba-tool user create|list', 'gérer les comptes Samba AD'], ['samba-tool drs showrepl', 'état de la réplication'], ['realm join -U admin domaine', 'joindre un Linux au domaine'], ['id utilisateur@domaine', 'vérifier un compte depuis Linux']], + examen: { titre: 'Sujet type · 20 points', q: ['Décrire le rôle de DNS et Kerberos dans AD. (4 pts)', 'Proposer OU et groupes pour un cas donné selon AGDLP. (6 pts)', 'Expliquer l’ordre d’application des GPO et diagnostiquer une GPO non appliquée. (5 pts)', 'Citer cinq mesures de sécurité de l’annuaire. (5 pts)'], r: ['SRV, tickets, horloge', 'voir TD', 'LSDOU, gpresult', 'tiering, LAPS, comptes séparés, NTLMv1/SMBv1, audit'] }, + prof: { deroule: [['Ouverture', 'Combien de mots de passe auriez-vous sans annuaire ?'], ['Cours', 'LDAP, AD, DNS, Kerberos'], ['Pratique', 'Lab Samba AD'], ['Incident', 'Heure et relation d’approbation'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : arborescence fournie. Élèves avancés : audit PingCastle, tiering.', eval: 'Lab validé, sujet type.' } }, +exemples: [ + ['Trouver les contrôleurs de domaine', 'Un poste ne rejoint pas le domaine.', 'nslookup -type=SRV _ldap._tcp.dc._msdcs.lycee.local', '_ldap._tcp.dc._msdcs.lycee.local SRV service location:\n priority = 0, weight = 100, port = 389, svr hostname = dc1.lycee.local\n priority = 0, weight = 100, port = 389, svr hostname = dc2.lycee.local', 'Les deux DC sont annoncés. Si la commande échoue, le poste n’interroge pas le DNS du domaine : on vérifie sa configuration DNS avant tout le reste.'], + ['Chercher un compte', 'Retrouver les groupes d’un élève.', 'ldapsearch -LLL -H ldaps://dc1.lycee.local -D "CN=lecture,OU=Services,DC=lycee,DC=local" -W -b "DC=lycee,DC=local" "(sAMAccountName=lea.martin)" memberOf', 'dn: CN=Léa M.,OU=SIO1,OU=Eleves,DC=lycee,DC=local\nmemberOf: CN=G-Eleves-SIO,OU=Groupes,DC=lycee,DC=local', 'LDAPS chiffre l’échange ; le compte de lecture dédié n’a aucun autre droit.'], + ['Vérifier les GPO d’un poste', 'Le lecteur réseau n’apparaît pas.', 'gpresult /r /scope:user', 'Objets Stratégie de groupe appliqués\n GPO-Eleves-Fond\nLes GPO suivants n’ont pas été appliqués car ils ont été filtrés\n GPO-Lecteurs-SIO\n Filtrage : Refusé (Sécurité)', 'La GPO des lecteurs est filtrée par sécurité : le groupe de l’élève n’a pas le droit « Appliquer la stratégie de groupe ».'], + ['Contrôler la réplication', 'Un compte créé sur dc1 n’existe pas sur dc2.', 'sudo samba-tool drs showrepl | grep -A3 "INBOUND" | tail -3', 'DC=lycee,DC=local\n Default-First-Site-Name\\DC1 via RPC\n Last attempt @ … failed, result 1256 (WERR_NO_SUCH_SERVICE)', 'La réplication de dc1 vers dc2 échoue. On vérifie la résolution de dc1 depuis dc2 et l’ouverture des ports RPC.'], + ['Joindre un Linux au domaine', 'Nouveau serveur Debian.', 'sudo realm join -U technicien lycee.local && realm list | grep -E "realm-name|configured"', 'realm-name: LYCEE.LOCAL\nconfigured: kerberos-member', 'Le serveur est membre ; les comptes du domaine peuvent s’y connecter selon les règles de realm permit.'], + ['Repérer les mots de passe qui n’expirent pas', 'Audit des comptes.', 'Get-ADUser -Filter {PasswordNeverExpires -eq $true -and Enabled -eq $true} | Select Name', 'Name\n----\nsvc-sauvegarde\nancien.stagiaire', 'Un compte de stagiaire parti est encore actif avec un mot de passe éternel : on le désactive. Le compte de service doit devenir un compte de service géré (gMSA).'] +], +termes: ['LDAP', 'LDAPS', 'DN', 'OU', 'Active Directory', 'domaine', 'forêt', 'contrôleur de domaine', 'Kerberos', 'SRV', 'GPO', 'LSDOU', 'AGDLP', 'gpupdate', 'gpresult', 'Samba', 'SSSD', 'realmd', 'LAPS', 'tiering', 'NTLM', 'SMB', 'ldapsearch', 'samba-tool'] +}); +})(); diff --git a/portail/public/js/contenus-long.js b/portail/public/js/contenus-long.js new file mode 100644 index 0000000..d1e2790 --- /dev/null +++ b/portail/public/js/contenus-long.js @@ -0,0 +1,64 @@ +// Lecture longue, schémas et emplacements photo pour chaque cours complet +(function () { +var K = 'stroke="#000" stroke-width="3"'; +var box = function (x, y, w, h, fill, txt, fs) { return '' + txt + ''; }; +var arrow = function (x1, y1, x2, y2) { return ''; }; +var svg = function (w, h, body) { return '' + body + ''; }; +var bits = [128, 64, 32, 16, 8, 4, 2, 1], v = [1, 0, 1, 1, 0, 0, 1, 0]; +window.CONTENUS_LONG = Object.assign(window.CONTENUS_LONG || {}, { +binaire: { figure: svg(720, 200, bits.map(function (b, i) { return box(20 + i * 85, 20, 80, 50, '#F6F5F1', b, 16) + box(20 + i * 85, 80, 80, 60, v[i] ? '#FFD23F' : '#fff', v[i], 26); }).join('') + '1011 0010₂ = 128 + 32 + 16 + 2 = 178'), legende: 'Chaque case d’un octet a un poids. On additionne les poids des bits à 1.', + photo: 'Photo : barrette de mémoire vive ou vue au microscope d’une puce', + lecture: ['Imaginez une rangée de huit interrupteurs. Chacun peut être allumé ou éteint. Si l’on convient que le premier vaut 1, le deuxième 2, le troisième 4, et ainsi de suite en doublant à chaque fois, on peut représenter n’importe quel nombre de 0 à 255 en choisissant quels interrupteurs allumer. C’est exactement ce que fait un octet dans la mémoire d’un ordinateur.', + 'Pourquoi doubler ? Parce que chaque interrupteur n’a que deux positions. En base 10, on a dix chiffres, donc chaque position vaut dix fois la précédente : unités, dizaines, centaines. En base 2, on n’a que deux chiffres, donc chaque position vaut deux fois la précédente. La logique est la même, seule la base change.', + 'L’hexadécimal n’est qu’une écriture plus compacte du binaire. Lire 32 zéros et uns d’affilée est pénible et source d’erreurs ; en les regroupant par quatre, on obtient huit symboles faciles à recopier. C’est pour cela que les adresses MAC, les couleurs web et les adresses IPv6 s’écrivent en hexadécimal : l’humain lit vite, et la machine retrouve les bits sans calcul.', + 'Dans la pratique, un administrateur réseau manipule ces conversions tous les jours sans même y penser : un masque /26, c’est 26 bits à 1, donc un dernier octet à 1100 0000, soit 192. Ceux qui maîtrisent ce réflexe découpent un réseau en sous-réseaux de tête ; les autres sortent la calculatrice. L’objectif de ce cours est de vous faire passer dans la première catégorie.'] }, +terminal: { figure: svg(720, 260, box(310, 10, 100, 40, '#000', '/', 20) + [['etc', 20], ['home', 160], ['var', 300], ['usr', 440], ['tmp', 580]].map(function (d) { return arrow(360, 50, d[1] + 60, 95) + box(d[1], 100, 120, 40, '#fff', '/' + d[0], 16); }).join('') + arrow(220, 140, 220, 185) + box(160, 190, 120, 40, '#FFD23F', 'eleve', 16) + arrow(360, 140, 360, 185) + box(300, 190, 120, 40, '#7CF2C0', 'log', 16)), legende: 'L’arborescence Linux part d’une seule racine, /. Votre dossier personnel est /home/eleve.', + photo: 'Photo : une salle serveur ou un administrateur devant un terminal', + lecture: ['Quand on découvre le terminal, l’écran noir impressionne. Pourtant, il ne fait rien d’autre que ce que fait l’explorateur de fichiers : afficher des dossiers, en ouvrir, copier, déplacer. La différence, c’est qu’on écrit ce qu’on veut au lieu de cliquer, et que ce qu’on écrit peut être rejoué, partagé et automatisé.', + 'Sur un serveur, il n’y a souvent pas d’interface graphique du tout. On s’y connecte à distance par SSH depuis un autre ordinateur, parfois depuis l’autre bout du monde, et le terminal est la seule porte d’entrée. Un technicien qui sait s’en servir intervient en quelques secondes là où un autre devrait se déplacer.', + 'La première compétence est de toujours savoir où l’on est. L’invite vous le dit, pwd le confirme. La plupart des erreurs de débutant viennent d’une commande lancée dans le mauvais dossier : un fichier créé « nulle part », un rm qui supprime autre chose que prévu. Prenez l’habitude de regarder l’invite avant d’appuyer sur Entrée.', + 'La deuxième compétence est de ne pas tout retenir. Personne ne connaît toutes les options de toutes les commandes. Les professionnels utilisent man, --help, la touche Tab et l’historique en permanence. Savoir chercher l’information est plus important que la connaître par cœur.'] }, +droits: { figure: svg(720, 190, box(20, 20, 60, 60, '#F6F5F1', '-', 26) + [['r', 'w', 'x', '#FFD23F', 'propriétaire (u)'], ['r', '-', 'x', '#7CF2C0', 'groupe (g)'], ['-', '-', '-', '#FF6FB5', 'autres (o)']].map(function (g, i) { var x = 100 + i * 205; return box(x, 20, 60, 60, g[3], g[0], 26) + box(x + 62, 20, 60, 60, g[3], g[1], 26) + box(x + 124, 20, 60, 60, g[3], g[2], 26) + '' + g[4] + '' + [7, 5, 0][i] + ''; }).join('')), legende: 'rwxr-x--- : trois triplets, un par catégorie. En octal, 750.', + photo: 'Photo : badge d’accès ou porte de salle serveur sécurisée', + lecture: ['Un serveur de lycée héberge à la fois les sujets d’examen des enseignants, les travaux des élèves et les fichiers de configuration du système. Sans règles, n’importe qui pourrait lire les sujets la veille de l’épreuve ou modifier la configuration. Les droits Unix sont ces règles, et elles s’appliquent à chaque fichier.', + 'Le modèle est volontairement simple : trois catégories de personnes, trois actions possibles. Cette simplicité est une force, car elle se vérifie d’un coup d’œil avec ls -l. Elle a aussi des limites, que les ACL viennent combler quand il faut donner un accès à une seule personne sans créer de groupe.', + 'Le piège classique est de « tout ouvrir » pour que ça marche : chmod 777. Le service redémarre, l’utilisateur est content, et le serveur vient de perdre toute protection. Un bon technicien cherche au contraire le droit minimum qui suffit. C’est le principe du moindre privilège, au cœur des recommandations de l’ANSSI.', + 'Retenez enfin que sur un dossier, x ne veut pas dire « exécuter » mais « traverser ». Un dossier en r-- permet de voir les noms des fichiers sans pouvoir les ouvrir, ce qui surprend toujours la première fois.'] }, +tubes: { figure: svg(720, 130, [['auth.log', '#F6F5F1'], ['grep', '#FFD23F'], ['awk', '#7CF2C0'], ['sort | uniq -c', '#C4B5FD'], ['head', '#FF6FB5']].map(function (s, i) { return box(10 + i * 142, 40, 120, 50, s[1], s[0], 14) + (i < 4 ? arrow(130 + i * 142, 65, 150 + i * 142, 65) : ''); }).join('')), legende: 'Chaque commande transforme le flux et passe le résultat à la suivante.', + photo: 'Photo : un écran de supervision avec des journaux qui défilent', + lecture: ['Un journal système, c’est un fichier texte où chaque ligne raconte un événement : une connexion, une erreur, un démarrage de service. Sur un serveur exposé à Internet, auth.log peut grossir de plusieurs milliers de lignes par jour. Personne ne les lit une par une.', + 'La philosophie Unix propose une réponse élégante : de petits outils qui font chacun une seule chose, et un moyen de les brancher entre eux. grep sait filtrer, sort sait trier, uniq sait compter les doublons. Aucun ne sait analyser un journal seul ; ensemble, ils répondent en une ligne à des questions complexes.', + 'Construisez toujours une chaîne pas à pas. Lancez la première commande, regardez le résultat, ajoutez un tube et la commande suivante, regardez encore. Une chaîne de six commandes écrite d’un coup ne marche presque jamais du premier essai ; construite progressivement, elle se met au point en deux minutes.', + 'Quand une chaîne est utile, on la sauvegarde dans un script et on la planifie. C’est le premier pas vers l’automatisation, compétence très recherchée chez les techniciens et les administrateurs.'] }, +osi: { figure: svg(720, 330, ['7 Application · HTTP, DNS', '6 Présentation · TLS', '5 Session', '4 Transport · TCP, UDP', '3 Réseau · IP', '2 Liaison · Ethernet', '1 Physique · câble, radio'].map(function (t, i) { return box(20, 10 + i * 44, 420, 40, ['#FF6FB5', '#FF6FB5', '#FF6FB5', '#C4B5FD', '#7CF2C0', '#FFD23F', '#F6F5F1'][i], t, 15); }).join('') + [['Application', 10, 130], ['Transport', 142, 40], ['Internet', 186, 40], ['Accès réseau', 230, 84]].map(function (c) { return box(470, c[1], 230, c[2], '#fff', c[0], 15); }).join('')), legende: 'À gauche les 7 couches OSI, à droite leurs équivalents dans le modèle TCP/IP.', + photo: 'Photo : baie de brassage avec switchs et câbles RJ45', + lecture: ['Quand vous ouvrez une page web, des dizaines d’équipements différents collaborent : votre carte Wi-Fi, la box, les routeurs de votre opérateur, ceux d’Internet, le serveur. Ils ont été fabriqués par des entreprises différentes, à des époques différentes. Ils se comprennent parce que chacun respecte des règles communes, rangées par couches.', + 'L’analogie du courrier aide à fixer les idées. Vous écrivez une lettre (l’application), vous la mettez dans une enveloppe avec l’adresse (le réseau), la poste la place dans un sac à destination d’un centre de tri (la liaison), et un camion transporte le sac (le physique). À chaque étape, on ajoute un emballage sans ouvrir le précédent. À l’arrivée, on déballe dans l’ordre inverse.', + 'Le modèle OSI à 7 couches est un modèle de référence, très utile pour raisonner et pour dialoguer entre techniciens (« c’est un problème de couche 2 »). Le modèle TCP/IP à 4 couches est celui qui est réellement implémenté. Vous devez connaître les deux et savoir passer de l’un à l’autre.', + 'Le vrai intérêt du modèle en couches se révèle en panne. Plutôt que de tout tester au hasard, on part du bas : le lien, l’adresse, la passerelle, le nom, le service. La première couche qui échoue désigne le coupable. Cette méthode fait gagner un temps considérable.'] }, +sql: { figure: svg(720, 200, box(20, 20, 280, 40, '#FFD23F', 'ELEVE', 16) + box(20, 60, 280, 34, '#fff', 'id · nom · prenom · #classe_id', 14) + box(420, 20, 280, 40, '#7CF2C0', 'CLASSE', 16) + box(420, 60, 280, 34, '#fff', 'id · libelle · annee', 14) + arrow(300, 77, 418, 77) + 'classe_id → idSELECT e.nom, c.libelle FROM eleve e JOIN classe c ON e.classe_id = c.id;'), legende: 'La clé étrangère classe_id relie chaque élève à sa classe. La jointure suit ce lien.', + photo: 'Photo : tableau de bord ou écran d’application de vie scolaire', + lecture: ['Un tableur suffit pour une classe. Pour un établissement entier, avec des milliers d’élèves, des centaines de devoirs et des notes qui arrivent en continu, il faut une base de données. Elle garantit que les données restent cohérentes, que plusieurs personnes peuvent écrire en même temps et qu’on retrouve n’importe quelle information en une fraction de seconde.', + 'L’idée centrale du modèle relationnel est de ne jamais recopier une information. Le libellé d’une classe n’est écrit qu’une fois, dans la table des classes ; chaque élève ne contient que le numéro de sa classe. Si la classe change de nom, on ne modifie qu’une ligne. Les jointures reconstruisent l’information complète au moment de la requête.', + 'SQL est un langage déclaratif : on décrit ce qu’on veut obtenir, pas comment le calculer. C’est la base de données qui choisit la meilleure façon d’y arriver. Cela rend les requêtes courtes et lisibles, presque comme des phrases, mais demande de bien comprendre l’ordre logique : FROM et JOIN d’abord, puis WHERE, GROUP BY, HAVING, SELECT et enfin ORDER BY.', + 'Enfin, une base contient souvent des données personnelles. Le RGPD impose de les protéger : droits d’accès limités, requêtes préparées contre les injections, sauvegardes, et suppression des données qui ne sont plus nécessaires.'] }, +python: { figure: svg(720, 250, box(260, 10, 200, 44, '#F6F5F1', 'note = saisie', 16) + arrow(360, 54, 360, 84) + 'note ≥ 10 ?' + arrow(470, 126, 560, 126) + box(560, 104, 140, 44, '#7CF2C0', 'Admis', 16) + arrow(250, 126, 160, 126) + box(20, 104, 140, 44, '#FF6FB5', 'À revoir', 16) + 'ouinonif note >= 10: print("Admis") else: print("À revoir")'), legende: 'Une condition est un embranchement : le programme suit une seule des deux branches.', + photo: 'Photo : élèves en séance de programmation', + lecture: ['Programmer, ce n’est pas taper du code : c’est découper un problème en étapes assez simples pour qu’une machine les exécute sans réfléchir. Le langage n’est qu’un moyen d’écrire ces étapes. Python est choisi dans l’enseignement parce que sa syntaxe gêne le moins possible cette réflexion.', + 'Trois structures suffisent à écrire presque tous les programmes : la séquence (une instruction après l’autre), la condition (faire ceci ou cela) et la répétition (recommencer). Le reste, fonctions, listes, fichiers, sert à organiser ces trois briques pour que le programme reste lisible quand il grandit.', + 'Un bon réflexe est d’écrire le programme en français avant de l’écrire en Python : « demander la note ; si elle est au moins 10, afficher admis ; sinon, afficher à revoir ». La traduction devient alors presque mécanique, et les erreurs de logique apparaissent avant la première ligne de code.', + 'Enfin, testez tout le temps. Une fonction de trois lignes accompagnée de deux assert est plus précieuse qu’une fonction de trente lignes que personne n’ose modifier. C’est ce que font les développeurs professionnels, et c’est ce que vérifient les tests automatiques de l’IDE.'] }, +web: { figure: svg(720, 200, box(20, 60, 180, 70, '#C4B5FD', 'Navigateur', 18) + box(520, 60, 180, 70, '#7CF2C0', 'Serveur Apache', 18) + arrow(200, 80, 518, 80) + arrow(520, 115, 202, 115) + 'GET /index.html200 OK + HTMLLe navigateur affiche le HTML, applique le CSS, exécute le JavaScript.'), legende: 'Un échange HTTP : une requête du client, une réponse du serveur.', + photo: 'Photo : outils de développement du navigateur ouverts sur une page', + lecture: ['Le Web repose sur une idée simple inventée au CERN en 1989 : des documents reliés par des liens, que l’on demande à un serveur avec un protocole commun. Trente ans plus tard, la même idée fait fonctionner les réseaux sociaux, les ENT et les applications de banque.', + 'Ouvrez les outils de développement de votre navigateur (F12), onglet Réseau, et rechargez une page : vous verrez des dizaines de requêtes, chacune avec sa méthode, son code et sa durée. C’est le meilleur moyen de comprendre HTTP, et c’est l’outil numéro un d’un développeur web pour dépanner.', + 'Séparer la structure (HTML) de la présentation (CSS) n’est pas un caprice : le même contenu peut ainsi s’afficher sur un téléphone, un grand écran ou être lu à voix haute par un lecteur d’écran pour une personne malvoyante. Une page bien structurée est aussi mieux référencée par les moteurs de recherche.', + 'Côté serveur, héberger un site, c’est installer un logiciel qui écoute sur le port 80 ou 443 et renvoie des fichiers. Apache et Nginx font tourner une grande partie des sites du monde. Savoir les installer, les configurer et lire leurs journaux fait partie du socle du technicien.'] }, +crypto: { figure: svg(720, 210, box(20, 70, 150, 60, '#fff', 'Message', 18) + arrow(170, 100, 268, 100) + box(270, 60, 180, 80, '#FFD23F', 'Chiffrement', 18) + arrow(450, 100, 548, 100) + box(550, 70, 150, 60, '#13233A', 'x9#Qa…', 18) + 'clé publique d’AliceSeule la clé privée d’Alice peut déchiffrer'), legende: 'Chiffrement asymétrique : on chiffre avec la clé publique du destinataire.', + photo: 'Photo : cadenas HTTPS dans la barre d’adresse ou clé de sécurité matérielle', + lecture: ['La cryptographie a longtemps été réservée aux militaires et aux diplomates. Aujourd’hui, elle protège chaque connexion HTTPS, chaque message sur une messagerie moderne, chaque paiement par carte. Vous l’utilisez des centaines de fois par jour sans le savoir.', + 'Il faut d’abord bien distinguer les outils. Le hachage ne cache rien : il produit une empreinte qui permet de vérifier qu’un fichier ou un mot de passe n’a pas changé. Le chiffrement, lui, rend un message illisible sans la clé, et il est réversible. Confondre les deux est l’erreur la plus fréquente, en cours comme en entreprise.', + 'Le chiffrement asymétrique a résolu un problème vieux de plusieurs siècles : comment se mettre d’accord sur une clé secrète sans jamais se rencontrer ? La réponse, publiée dans les années 1970, est d’utiliser deux clés liées mathématiquement. On peut diffuser l’une à tout le monde sans jamais compromettre l’autre.', + 'En pratique, un technicien ne réinvente jamais un algorithme. Il choisit des outils éprouvés, suit les recommandations de l’ANSSI, active HTTPS partout, remplace les mots de passe SSH par des clés et vérifie que l’application stocke les mots de passe avec Argon2 ou bcrypt. La sécurité vient d’abord de la rigueur.'] } +}); +})(); diff --git a/portail/public/js/contenus-lot-10.js b/portail/public/js/contenus-lot-10.js new file mode 100644 index 0000000..92c0f6c --- /dev/null +++ b/portail/public/js/contenus-lot-10.js @@ -0,0 +1,227 @@ +// Cours complets · lot 10 : algorithmique et structures, développement web back-end, JavaScript et TypeScript, langage C +(function () { +var reg = function (c) { + (window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours); + (window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus; + (window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples; + (window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] }; +}; + +reg({ +cours: { id: 'algo', match: /Tris|dichotomique|complexité|Complexité et notation O|Récursivité|Piles, files|listes chaînées|Graphes : parcours|Dijkstra|Listes, tuples|Fonctions, portée|Tests : assert/i, + titre: 'Algorithmique : complexité, tris, récursivité, structures de données et graphes', duree: '', + niveau: 'NSI Première et Terminale · BTS SIO SLAM · BUT Informatique · classes préparatoires', + prerequis: ['Python : variables, boucles, fonctions'], + competences: ['NSI · Algorithmique et structures de données', 'BTS SIO SLAM · Concevoir une solution applicative', 'BUT Info · Compétence « Optimiser »'], + intro: 'Deux programmes corrects peuvent avoir des comportements radicalement différents : l’un trie un million de lignes en une seconde, l’autre en plusieurs heures. L’algorithmique donne les outils pour choisir la bonne méthode et la bonne structure de données, et pour prévoir comment un programme se comportera quand les données grandiront. Ce module couvre la complexité, les tris et la recherche, la récursivité, les structures linéaires et les graphes, avec leurs applications concrètes en réseau et en informatique de gestion.', + cm: [ + { t: 'Mesurer un algorithme : la complexité', d: '', html: 'La complexité en temps compte le nombre d’opérations élémentaires en fonction de la taille n des données, dans le pire cas. On ne garde que l’ordre de grandeur, noté O : une boucle simple est en O(n), deux boucles imbriquées en O(n²), une recherche qui divise l’espace par deux à chaque étape en O(log n). Pour n = 1 000 000, O(n log n) représente environ 20 millions d’opérations, O(n²) mille milliards : la différence entre une seconde et plusieurs jours. La complexité en espace mesure la mémoire utilisée.', table: [['Complexité', 'Exemple', 'n = 10⁶'], ['O(1)', 'accès à un élément d’un tableau, d’un dictionnaire', '1'], ['O(log n)', 'recherche dichotomique', '≈ 20'], ['O(n)', 'parcours, recherche linéaire', '10⁶'], ['O(n log n)', 'tri fusion, tri de Python', '≈ 2 × 10⁷'], ['O(n²)', 'tri par sélection, double boucle', '10¹²'], ['O(2ⁿ)', 'tester tous les sous-ensembles', 'inenvisageable']] }, + { t: 'Recherche et tris', d: '', html: 'La recherche dichotomique dans un tableau trié compare la valeur cherchée au milieu, puis continue dans la moitié qui convient : O(log n). Les tris simples, par sélection et par insertion, sont en O(n²) ; le tri par insertion est très efficace sur des données presque triées. Le tri fusion découpe le tableau en deux, trie chaque moitié récursivement et fusionne : O(n log n) garanti. Le tri rapide est en moyenne en O(n log n). En pratique, on utilise le tri de la bibliothèque (sorted, Timsort), mais comprendre ces algorithmes permet de raisonner sur les coûts et de réussir les épreuves.', exemple: 'def dichotomie(t, x):\n g, d = 0, len(t) - 1\n while g <= d:\n m = (g + d) // 2\n if t[m] == x:\n return m\n if t[m] < x:\n g = m + 1\n else:\n d = m - 1\n return -1' }, + { t: 'Récursivité', d: '', html: 'Une fonction récursive s’appelle elle-même sur un problème plus petit, jusqu’à un cas de base qui s’arrête. Elle exprime naturellement les problèmes à structure récursive : parcours d’arborescence de fichiers, tri fusion, calculs sur des arbres. Chaque appel occupe une place sur la pile d’appels : sans cas de base atteint, on obtient un dépassement (RecursionError en Python, limite par défaut de 1 000 appels). Certaines récursions recalculent plusieurs fois les mêmes valeurs (Fibonacci naïf en O(2ⁿ)) : la mémoïsation (functools.cache) ou une version itérative corrige le problème.' }, + { t: 'Structures de données linéaires', d: '', html: 'Une pile (LIFO, dernier entré premier sorti) sert à l’historique d’annulation, à la pile d’appels, à la vérification des parenthèses. Une file (FIFO) sert aux files d’attente : impressions, requêtes, mains levées dans l’ordre d’arrivée. En Python, une liste fait une bonne pile (append, pop), mais une mauvaise file (pop(0) en O(n)) : on utilise collections.deque. Une liste chaînée relie des maillons par des références : insertion en tête en O(1), accès au k-ième élément en O(k). Le dictionnaire (table de hachage) donne un accès moyen en O(1) par clé : c’est souvent la structure qui transforme un O(n²) en O(n).' }, + { t: 'Graphes et parcours', d: '', html: 'Un graphe est un ensemble de sommets reliés par des arêtes, éventuellement orientées et pondérées : routeurs et liens, pages web, dépendances entre tâches. On le représente par une matrice d’adjacence ou, plus économiquement, par des listes d’adjacence (un dictionnaire de voisins). Le parcours en largeur (BFS, avec une file) visite par distance croissante et trouve le plus court chemin en nombre d’arêtes ; le parcours en profondeur (DFS, avec une pile ou la récursivité) sert à détecter des cycles ou des composantes connexes.' }, + { t: 'Plus courts chemins : Dijkstra', d: '', html: 'L’algorithme de Dijkstra calcule les plus courts chemins depuis un sommet dans un graphe à poids positifs : il sélectionne à chaque étape le sommet non traité le plus proche et met à jour les distances de ses voisins. Avec une file de priorité (heapq), il s’exécute en O((V + E) log V). C’est exactement ce que fait chaque routeur OSPF pour construire sa table de routage à partir de la carte du réseau, avec un coût par lien inversement proportionnel au débit.', exemple: 'import heapq\ndef dijkstra(g, s):\n dist = {s: 0}\n file = [(0, s)]\n while file:\n d, u = heapq.heappop(file)\n if d > dist[u]:\n continue\n for v, w in g[u].items():\n if d + w < dist.get(v, float("inf")):\n dist[v] = d + w\n heapq.heappush(file, (d + w, v))\n return dist' } + ], + synthese: ['Complexité O : O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)', 'Dichotomie sur tableau trié en O(log n) ; tri fusion en O(n log n)', 'Récursivité : cas de base, pile d’appels, mémoïsation', 'Pile LIFO, file FIFO avec deque, dictionnaire en O(1) moyen', 'Graphes : listes d’adjacence ; BFS et DFS', 'Dijkstra et OSPF'], + qcm: [ + ['Complexité de la recherche dichotomique ?', ['O(1)', 'O(log n)', 'O(n)', 'O(n²)'], 1, 'On divise par deux à chaque étape.'], + ['Condition pour utiliser la dichotomie ?', ['Tableau trié', 'Tableau court', 'Nombres entiers', 'Aucune'], 0, 'Sinon on ne sait pas quelle moitié choisir.'], + ['Quelle structure pour une file d’attente en Python ?', ['list avec pop(0)', 'collections.deque', 'set', 'tuple'], 1, 'popleft en O(1).'], + ['Que manque-t-il à une récursion qui provoque RecursionError ?', ['Un return', 'Un cas de base atteint', 'Une boucle', 'Un print'], 1, 'Elle ne s’arrête jamais.'], + ['Quel parcours trouve le plus court chemin en nombre d’arêtes ?', ['DFS', 'BFS', 'Tri fusion', 'Dichotomie'], 1, 'Par distance croissante.'], + ['Quel protocole de routage utilise Dijkstra ?', ['RIP', 'OSPF', 'DHCP', 'ARP'], 1, 'État de liens.'], + ['Deux boucles imbriquées sur n éléments :', ['O(n)', 'O(n²)', 'O(log n)', 'O(2n)'], 1, 'n × n.'] + ], + td: [ + { n: 'Échauffement', c: 'Donner la complexité de : parcourir une liste ; vérifier si deux listes ont un élément commun avec deux boucles ; même chose en transformant la seconde en ensemble.', r: 'O(n) · O(n × m) · O(n + m)' }, + { n: 'Application', c: 'Écrire un tri par insertion, puis compter le nombre de comparaisons sur une liste déjà triée et sur une liste triée à l’envers de 1 000 éléments.', r: 'Déjà triée : environ n comparaisons (O(n)) · à l’envers : environ n²/2 = 500 000 (O(n²)).' }, + { n: 'Application', c: 'Vérifier le bon parenthésage d’une expression avec une pile.', r: 'Empiler chaque ouvrante ; pour chaque fermante, la pile ne doit pas être vide et son sommet doit correspondre ; à la fin, la pile doit être vide.' }, + { n: 'Approfondissement', c: 'Appliquer Dijkstra à la main sur un réseau de cinq routeurs fourni, puis en déduire la table de routage du routeur A.', r: 'Tableau des distances étape par étape ; la table associe à chaque destination le premier saut du plus court chemin.' } + ], + tp: { type: 'IDE Python', url: 'apercu-ide.html', desc: 'Exercices avec tests et mesure du temps d’exécution.', etapes: ['Dichotomie et tests', 'Tri fusion', 'Comparaison des temps selon n', 'File des mains levées avec deque', 'BFS sur un plan de salles', 'Dijkstra sur un réseau de routeurs'] }, + plusLoin: [['France-IOI · parcours algorithmique', 'https://www.france-ioi.org/'], ['HackerRank · Data Structures et Algorithms', 'https://www.hackerrank.com/domains/algorithms'], ['VisuAlgo (visualisation)', 'https://visualgo.net/fr']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Le calcul des doublons dans l’export des 12 000 élèves de l’académie prend 40 minutes. Rendre le traitement quasi instantané.', etapes: [ + ['Lire le code', 'for a in eleves:\n for b in eleves:\n if a is not b and a["ine"] == b["ine"]: doublons.append(a)\n→ Deux boucles imbriquées : 144 millions de comparaisons, O(n²).'], + ['Choisir la structure', '→ Un dictionnaire (ou un Counter) compte les occurrences de chaque identifiant en un seul parcours.'], + ['Réécrire', 'from collections import Counter\nc = Counter(e["ine"] for e in eleves)\ndoublons = [e for e in eleves if c[e["ine"]] > 1]\n→ Deux parcours : O(n).'], + ['Mesurer', 'Temps : 40 min → 0,02 s\n→ Même résultat vérifié par un test sur un petit jeu de données connu.'], + ['Retenir', '→ Devant une double boucle de recherche, se demander quelle clé permettrait un accès direct par dictionnaire ou ensemble.'] + ] }, + erreurs: ['Dichotomie sur une liste non triée', 'Oublier le cas de base', 'list.pop(0) pour une file', 'Double boucle là où un dictionnaire suffit', 'Recalculer sans mémoïsation', 'Dijkstra avec des poids négatifs'], + memo: [['Outil', 'Usage'], ['len, in sur set/dict', 'O(1) en moyenne'], ['sorted(t, key=…)', 'tri O(n log n)'], ['bisect', 'dichotomie de la bibliothèque'], ['collections.deque', 'file'], ['collections.Counter', 'compter'], ['heapq', 'file de priorité'], ['functools.cache', 'mémoïsation'], ['timeit', 'mesurer le temps'], ['assert f(x) == y', 'tests simples'], ['sys.setrecursionlimit', 'limite de récursion (avec prudence)']], + examen: { titre: 'Sujet type · 20 points', q: ['Déterminer la complexité de trois fonctions. (4 pts)', 'Écrire la recherche dichotomique et ses tests. (4 pts)', 'Écrire une fonction récursive et justifier sa terminaison. (4 pts)', 'Implémenter une file avec deque. (3 pts)', 'Appliquer Dijkstra à la main. (5 pts)'], r: ['O(n), O(n²), O(log n)', 'voir cours', 'cas de base, taille décroissante', 'append / popleft', 'tableau des distances'] }, + prof: { deroule: [['Ouverture', 'Chronométrer une double boucle puis un dictionnaire'], ['Cours', 'Complexité'], ['Pratique', 'Tris et dichotomie'], ['Cours', 'Récursivité et structures'], ['Pratique', 'Graphes et Dijkstra'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : visualisations VisuAlgo. Élèves avancés : HackerRank Medium, A*.', eval: 'Tests IDE, sujet type.' } }, +exemples: [ + ['Comparer deux complexités', 'Justifier un choix.', 'python3 -m timeit -s "l=list(range(10**5)); s=set(l)" "99999 in l"\npython3 -m timeit -s "l=list(range(10**5)); s=set(l)" "99999 in s"', '500 loops, best of 5: 612 usec per loop\n5000000 loops, best of 5: 42.1 nsec per loop', 'La recherche dans un ensemble est environ 15 000 fois plus rapide que dans une liste.'], + ['Dichotomie de la bibliothèque', 'Retrouver un seuil de note.', 'import bisect; bisect.bisect_left([8, 10, 12, 14, 16], 13)', '3', 'Position d’insertion de 13 dans la liste triée, en O(log n).'], + ['Limite de récursion', 'Fonction récursive sur une longue liste.', 'def somme(l): return 0 if not l else l[0] + somme(l[1:])\nsomme(list(range(5000)))', 'RecursionError: maximum recursion depth exceeded', 'Au-delà de 1 000 appels : on écrit une version itérative, plus adaptée ici.'], + ['Mémoïsation', 'Fibonacci lent.', 'from functools import cache\n@cache\ndef fib(n): return n if n < 2 else fib(n-1) + fib(n-2)\nfib(200)', '280571172992510140037611932413038677189525', 'Instantané grâce au cache ; la version naïve ne terminerait pas.'], + ['File des mains levées', 'Fonction Lever la main.', 'from collections import deque\nq = deque(["Nora", "Yanis"]); q.append("Léa"); q.popleft(), list(q)', "('Nora', ['Yanis', 'Léa'])", 'Premier arrivé, premier servi, en O(1).'], + ['Plus courts chemins', 'Réseau de quatre routeurs.', 'g = {"A":{"B":10,"C":1},"B":{"D":1},"C":{"B":1,"D":20},"D":{}}\ndijkstra(g, "A")', "{'A': 0, 'C': 1, 'B': 2, 'D': 3}", 'Le chemin vers D passe par C puis B (coût 3) plutôt que par le lien direct C–D (21).'] +], +termes: ['complexité', 'O(n)', 'O(log n)', 'O(n²)', 'O(n log n)', 'recherche dichotomique', 'tri par insertion', 'tri fusion', 'récursivité', 'cas de base', 'pile d’appels', 'mémoïsation', 'pile', 'file', 'deque', 'liste chaînée', 'dictionnaire', 'graphe', 'BFS', 'DFS', 'Dijkstra', 'heapq'] +}); + +reg({ +cours: { id: 'backend', match: /Laravel|MVC|API REST|OpenAPI|Authentification : sessions|OAuth|PHP : bases|Framework back|Déployer une application web|Sécurité web|CSP|CORS|Tests : unitaires, intégration/i, + titre: 'Développement web back-end : MVC avec Laravel, API REST, authentification et sécurité', duree: '', + niveau: 'BTS SIO SLAM · BUT Informatique · licence pro · Master', + prerequis: ['Le Web : HTTP et HTML', 'SQL', 'Git'], + competences: ['BTS SIO SLAM · Bloc 2 · Concevoir et développer une solution applicative', 'BTS SIO · Bloc 3 · Cybersécurité des services', 'BUT Info · Réaliser et déployer'], + intro: 'Une application web professionnelle se compose d’une interface, d’une logique métier, d’une base de données et d’une API, le tout sécurisé et testé. Les frameworks comme Laravel, sur lequel est construit Syllabus, apportent une organisation éprouvée et des protections intégrées. Ce module présente l’architecture MVC, le routage, l’ORM, la conception d’une API REST, l’authentification, les protections contre les failles courantes et le déploiement.', + cm: [ + { t: 'Architecture MVC', d: '', html: 'Le motif Modèle-Vue-Contrôleur sépare les responsabilités. Le modèle représente les données et les règles métier (un élève, une classe, un devoir). La vue présente les données (templates Blade). Le contrôleur reçoit la requête, appelle le modèle et choisit la vue ou la réponse. Le routeur associe chaque URL et méthode HTTP à une action. Cette séparation rend le code testable et permet à plusieurs développeurs de travailler en parallèle.', exemple: '// routes/web.php\nRoute::middleware("auth")->group(function () {\n Route::get("/classes/{classe}", [ClasseController::class, "show"])->name("classes.show");\n});\n\n// app/Http/Controllers/ClasseController.php\npublic function show(Classe $classe) {\n $this->authorize("view", $classe);\n return view("classes.show", [\n "classe" => $classe->load("eleves"),\n ]);\n}' }, + { t: 'Modèles, ORM et migrations', d: '', html: 'L’ORM Eloquent associe chaque table à une classe : Eleve::where("classe_id", 3)->orderBy("nom")->get() produit une requête SQL préparée. Les relations (hasMany, belongsTo, belongsToMany) décrivent les clés étrangères. Les migrations versionnent le schéma de la base avec le code : chaque modification est un fichier, appliqué dans le même ordre partout. Piège de performance classique : le problème N+1, une requête par élément d’une liste, évité par le chargement anticipé (with("eleves")).' }, + { t: 'Concevoir une API REST', d: '', html: 'Une API REST expose des ressources identifiées par des URL et manipulées par les méthodes HTTP : GET /api/devoirs liste, POST /api/devoirs crée, GET /api/devoirs/12 lit, PUT ou PATCH modifie, DELETE supprime. Les réponses sont en JSON avec des codes cohérents : 200, 201 Created, 204 No Content, 400 et 422 pour une requête invalide, 401 non authentifié, 403 interdit, 404 introuvable. On versionne l’API (/api/v1), on pagine les listes, et on la documente avec OpenAPI, qui génère une documentation interactive et des clients.' }, + { t: 'Authentification et autorisation', d: '', html: 'L’authentification établit qui est l’utilisateur ; l’autorisation décide de ce qu’il peut faire. Pour une application web, une session côté serveur, identifiée par un cookie HttpOnly, Secure et SameSite, reste la solution la plus sûre. Pour une API consommée par d’autres applications, on utilise des jetons (tokens d’API, Laravel Sanctum) ou OAuth 2 / OpenID Connect pour déléguer l’authentification à un fournisseur d’identité (ENT, EduConnect). Les mots de passe sont hachés avec bcrypt ou Argon2. L’autorisation se centralise dans des politiques (policies) : un professeur ne voit que ses classes.' }, + { t: 'Sécurité : les failles courantes', d: '', html: 'L’OWASP Top 10 recense les failles les plus fréquentes. Contrôle d’accès défaillant : changer un identifiant dans l’URL donne accès aux données d’un autre (IDOR) ; on vérifie l’autorisation à chaque action. Injection : requêtes préparées systématiques. XSS : Blade échappe automatiquement {{ }} ; {!! !!} ne s’utilise que sur du contenu maîtrisé. CSRF : un jeton dans chaque formulaire (@csrf). Assignation de masse : déclarer les champs modifiables ($fillable). À quoi s’ajoutent les en-têtes : Content-Security-Policy contre l’injection de scripts, CORS limité aux origines autorisées, HSTS.', attention: 'Le mode débogage (APP_DEBUG=true) en production affiche les traces d’erreur, les variables d’environnement et parfois les mots de passe de la base. Il doit être désactivé, et le fichier .env ne doit jamais être accessible par le web ni versionné.' }, + { t: 'Tester et déployer', d: '', html: 'Les tests unitaires vérifient une classe isolée, les tests de fonctionnalité envoient de vraies requêtes à l’application ($this->actingAs($prof)->get("/classes/3")->assertOk()), les tests de bout en bout pilotent un navigateur. Le déploiement suit toujours les mêmes étapes, automatisées par la CI : récupération du code, composer install --no-dev, migrations, mise en cache de la configuration et des routes, redémarrage des workers de file d’attente, derrière Nginx et PHP-FPM, avec un retour arrière possible.' } + ], + synthese: ['MVC : modèle, vue, contrôleur, routeur', 'ORM et migrations ; éviter le N+1 avec with()', 'REST : ressources, méthodes, codes HTTP, JSON, versionnement, OpenAPI', 'Session sécurisée ou jetons ; OAuth 2 / OIDC ; policies pour l’autorisation', 'OWASP : contrôle d’accès, injection, XSS, CSRF, assignation de masse ; en-têtes', 'APP_DEBUG=false, .env protégé ; tests et déploiement automatisés'], + qcm: [ + ['Dans MVC, qui reçoit la requête et choisit la réponse ?', ['Le modèle', 'Le contrôleur', 'La vue', 'La base'], 1, 'Il orchestre.'], + ['Quelle méthode pour créer une ressource en REST ?', ['GET', 'POST', 'DELETE', 'HEAD'], 1, 'Réponse 201.'], + ['Code HTTP pour « authentifié mais pas autorisé » ?', ['401', '403', '404', '500'], 1, '401 = non authentifié.'], + ['Qu’est-ce que le problème N+1 ?', ['Une faille', 'Une requête SQL par élément d’une liste', 'Un code HTTP', 'Une erreur de migration'], 1, 'Chargement anticipé.'], + ['Protection contre le CSRF ?', ['Un jeton dans chaque formulaire', 'HTTPS seul', 'Un captcha', 'Le hachage'], 0, '@csrf.'], + ['Pourquoi désactiver APP_DEBUG en production ?', ['Performance', 'Il expose des informations sensibles', 'Obligation légale', 'Pour le cache'], 1, 'Traces, variables, secrets.'] + ], + td: [ + { n: 'Application', c: 'Concevoir les routes REST de la ressource « devoirs » d’une classe, avec les codes de réponse attendus.', r: 'GET /api/v1/classes/{c}/devoirs 200 · POST 201 · GET /devoirs/{d} 200/404 · PATCH 200/422 · DELETE 204 · 401 sans jeton, 403 si pas professeur de la classe.' }, + { n: 'Application', c: 'Corriger la faille : « public function show($id) { return Eleve::find($id); } ».', r: 'Liaison de modèle + autorisation : show(Eleve $eleve) { $this->authorize("view", $eleve); return $eleve; } avec une policy vérifiant que l’élève appartient à une classe du professeur.' }, + { n: 'Approfondissement', c: 'Écrire un test de fonctionnalité qui vérifie qu’un professeur ne peut pas voir la fiche d’un élève d’une autre classe.', r: '$prof = User::factory()->prof()->create(); $eleve = Eleve::factory()->create(); $this->actingAs($prof)->get(route("eleves.show", $eleve))->assertForbidden();' } + ], + tp: { type: 'IDE', url: 'apercu-ide.html', desc: 'Mini-application Laravel de gestion de devoirs.', etapes: ['Migration et modèle', 'Contrôleur et vues', 'API REST', 'Policy d’autorisation', 'Tests de fonctionnalité', 'Déploiement par CI'] }, + plusLoin: [['Documentation Laravel', 'https://laravel.com/docs'], ['OWASP Top 10', 'https://owasp.org/Top10/fr/'], ['OpenAPI Specification', 'https://spec.openapis.org/oas/latest.html'], ['ANSSI · Recommandations pour la mise en œuvre d’un site web', 'https://cyber.gouv.fr/']] }, +plus: { cas: { titre: '', texte: '' }, + resolu: { enonce: 'Un élève signale qu’en modifiant le numéro dans l’adresse de sa page de résultats, il voit les notes d’un camarade.', etapes: [ + ['Reproduire', 'Connecté comme élève 1204 : GET /resultats/1205 → 200 OK, notes de l’élève 1205.\n→ Faille de contrôle d’accès (IDOR).'], + ['Lire le code', 'public function show($id) { return view("resultats", ["eleve" => Eleve::findOrFail($id)]); }\n→ Aucune vérification que l’élève connecté est celui demandé.'], + ['Corriger', 'Policy EleveResultatsPolicy : un élève voit ses résultats, un professeur ceux de ses classes, la vie scolaire tout l’établissement.\npublic function show(Eleve $eleve) { $this->authorize("voirResultats", $eleve); … }\n→ Réponse 403 pour un autre élève.'], + ['Tester et vérifier les autres routes', '→ Test automatisé ajouté ; revue de toutes les routes prenant un identifiant : deux autres corrigées (exports, devoirs rendus).'], + ['Traiter les conséquences', '→ Analyse des journaux d’accès : consultations anormales par deux comptes. Information du DPO, qui évalue la notification à la CNIL et l’information des familles concernées.'] + ] }, + erreurs: ['Contrôler l’accès seulement dans l’interface', 'Requêtes SQL construites par concaténation', 'Utiliser {!! !!} sur une saisie utilisateur', 'APP_DEBUG=true en production', 'Pas de tests d’autorisation', 'Codes HTTP incohérents dans l’API'], + memo: [['Commande / élément', 'Usage'], ['php artisan route:list', 'lister les routes'], ['php artisan make:model -mcr Devoir', 'modèle, migration, contrôleur'], ['php artisan migrate', 'appliquer les migrations'], ['php artisan make:policy', 'politique d’autorisation'], ['$this->authorize()', 'vérifier un droit'], ['with("relation")', 'éviter le N+1'], ['@csrf', 'jeton de formulaire'], ['php artisan test', 'lancer les tests'], ['php artisan config:cache route:cache', 'optimiser en production'], ['curl -H "Authorization: Bearer …" /api/v1/…', 'tester une API']], + examen: { titre: 'Sujet type · 20 points', q: ['Expliquer MVC sur un exemple. (3 pts)', 'Concevoir une API REST. (5 pts)', 'Écrire un contrôleur avec autorisation. (5 pts)', 'Identifier et corriger trois failles dans un code fourni. (5 pts)', 'Décrire un déploiement. (2 pts)'], r: ['voir cours', 'ressources et codes', 'authorize + policy', 'IDOR, XSS, injection', 'composer, migrate, cache'] }, + prof: { deroule: [['Ouverture', 'Démonstration d’une faille IDOR sur une application de lab'], ['Cours', 'MVC et routage'], ['Pratique', 'Modèle et contrôleur'], ['Cours', 'API et sécurité'], ['Pratique', 'Policies et tests'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : squelette fourni. Élèves avancés : OAuth 2, files d’attente, OpenAPI.', eval: 'Application testée, sujet type.' } }, +exemples: [ + ['Lister les routes', 'Audit d’une application.', 'php artisan route:list --path=api', 'GET|HEAD api/v1/devoirs ...... devoirs.index › DevoirController@index\nPOST api/v1/devoirs ...... devoirs.store › DevoirController@store\nDELETE api/v1/devoirs/{devoir} devoirs.destroy', 'On vérifie que chaque route sensible a un middleware d’authentification.'], + ['Détecter un N+1', 'Page lente.', 'DB::enableQueryLog(); $c = Classe::all(); foreach ($c as $x) $x->eleves->count(); count(DB::getQueryLog())', '31', '31 requêtes pour 30 classes. Avec Classe::withCount("eleves")->get(), une seule.'], + ['Tester une API', 'Création d’un devoir.', 'curl -s -X POST https://syllabus.local/api/v1/devoirs -H "Authorization: Bearer $T" -H "Content-Type: application/json" -d \'{"titre":"DM 3"}\'', '{"message":"The date limite field is required.","errors":{"date_limite":["…"]}} (HTTP 422)', 'La validation renvoie 422 avec le détail des champs manquants.'], + ['Test d’autorisation', 'Non-régression.', 'php artisan test --filter=ResultatsTest', 'PASS Tests\\Feature\\ResultatsTest\n✓ un élève voit ses résultats\n✓ un élève ne voit pas ceux d’un camarade', 'Le test échouerait si la faille réapparaissait.'], + ['Vérifier les en-têtes', 'Audit de sécurité.', 'curl -sI https://syllabus.local | grep -iE "content-security|set-cookie"', 'content-security-policy: default-src \'self\'; frame-ancestors \'none\'\nset-cookie: syllabus_session=…; secure; httponly; samesite=lax', 'CSP restrictive et cookie de session protégé.'], + ['Déploiement', 'Mise en production.', 'composer install --no-dev -o && php artisan migrate --force && php artisan optimize && php artisan queue:restart', 'INFO Running migrations. 2026_10_01_create_rendus_table ....... DONE\nINFO Caching framework bootstrap, configuration, and metadata.', 'Chaque étape est dans le pipeline CI, avec retour arrière en cas d’échec.'] +], +termes: ['MVC', 'modèle', 'vue', 'contrôleur', 'routeur', 'Laravel', 'Blade', 'Eloquent', 'ORM', 'migration', 'N+1', 'API REST', 'JSON', 'OpenAPI', 'session', 'jeton', 'OAuth 2', 'OpenID Connect', 'policy', 'OWASP', 'IDOR', 'XSS', 'CSRF', 'CSP', 'CORS', 'APP_DEBUG'] +}); + +reg({ +cours: { id: 'js', match: /JavaScript|TypeScript|DOM|fetch|Accessibilité web|RGAA|WCAG|Performance web/i, + titre: 'JavaScript et TypeScript côté navigateur : DOM, asynchrone, fetch, accessibilité', duree: '', + niveau: 'NSI Première · BTS SIO SLAM · BUT Informatique · Titre pro DWWM', + prerequis: ['Le Web : HTML et CSS', 'Python : bases de la programmation'], + competences: ['NSI · Interactions dans le navigateur', 'BTS SIO SLAM · Développer une interface', 'DWWM · Développer la partie front-end'], + intro: 'Tout ce qui réagit dans une page web, du bouton « Lever la main » au terminal xterm.js des labs, est écrit en JavaScript. Le langage s’exécute dans le navigateur, manipule la page, dialogue avec le serveur sans la recharger et doit rester utilisable par tous, y compris au clavier et avec un lecteur d’écran. Ce module présente le langage moderne, le DOM, la programmation asynchrone avec fetch, TypeScript et les exigences d’accessibilité et de performance.', + cm: [ + { t: 'Le langage moderne', d: '', html: 'On déclare avec const (par défaut) et let ; var est à éviter. Les types de base : nombres, chaînes, booléens, null, undefined, objets et tableaux. Les fonctions fléchées (x => x * 2), la déstructuration (const { nom, classe } = eleve), les gabarits de chaînes (`Bonjour ${nom}`) et les méthodes de tableaux (map, filter, reduce, find) forment le socle quotidien. On compare avec ===, qui ne convertit pas les types ("1" == 1 est vrai, "1" === 1 est faux). Les modules (import / export) organisent le code.' }, + { t: 'Le DOM et les événements', d: '', html: 'Le DOM est la représentation de la page sous forme d’arbre d’objets. On sélectionne un élément (document.querySelector("#liste")), on modifie son contenu (textContent), ses classes (classList.add) ou ses attributs, on en crée (createElement). On réagit aux actions de l’utilisateur avec addEventListener("click", …). La délégation d’événements place un seul écouteur sur un parent pour gérer de nombreux enfants, y compris ceux ajoutés plus tard.', attention: 'innerHTML avec une donnée saisie par un utilisateur ouvre une faille XSS : le texte peut contenir un script. On utilise textContent pour du texte, ou on échappe explicitement.' }, + { t: 'Asynchrone : promesses et async/await', d: '', html: 'Le navigateur n’exécute qu’un seul fil de JavaScript ; une opération longue (requête réseau, minuterie) ne doit pas le bloquer. Elle renvoie une promesse, résolue plus tard. La syntaxe async / await permet d’écrire ce code de façon lisible, et try / catch gère les erreurs. Promise.all lance plusieurs requêtes en parallèle. La boucle d’événements exécute les rappels quand le fil principal est libre.' }, + { t: 'Dialoguer avec le serveur : fetch', d: '', html: 'fetch envoie une requête HTTP et renvoie une promesse de réponse. Il ne rejette la promesse qu’en cas d’erreur réseau : une réponse 404 ou 500 est une réponse « réussie », il faut tester response.ok. On envoie du JSON avec l’en-tête Content-Type et on joint le jeton CSRF ou d’authentification. Pour des mises à jour en temps réel (mains levées, présences du quiz), on utilise les WebSockets ou les Server-Sent Events plutôt qu’interroger le serveur toutes les secondes.', exemple: 'async function leverLaMain(labId) {\n const r = await fetch(`/api/v1/labs/${labId}/mains`, {\n method: "POST",\n headers: { "Content-Type": "application/json", "X-CSRF-TOKEN": jeton },\n body: JSON.stringify({ etape: 4 }),\n });\n if (!r.ok) throw new Error(`Erreur ${r.status}`);\n const { position } = await r.json();\n document.querySelector("#position").textContent = `Vous êtes ${position}e dans la file.`;\n}' }, + { t: 'TypeScript', d: '', html: 'TypeScript ajoute un typage statique à JavaScript : on déclare la forme des données (interface Eleve { id: number; nom: string; classe?: string }) et le compilateur signale les erreurs avant l’exécution : propriété mal orthographiée, valeur possiblement undefined, mauvais type d’argument. Le code est compilé en JavaScript standard. Sur un projet de plusieurs milliers de lignes, il réduit fortement les régressions et rend l’éditeur beaucoup plus utile (complétion, renommage sûr).' }, + { t: 'Accessibilité et performance', d: '', html: 'Un site public en France doit respecter le RGAA, fondé sur les normes WCAG. Les règles de base : utiliser les bons éléments HTML (un <button> plutôt qu’un <div> cliquable, qui ne se commande pas au clavier), des étiquettes de formulaire liées, des textes alternatifs, un contraste suffisant (4,5:1 pour le texte), un focus visible, et aria-live pour annoncer les mises à jour dynamiques. Côté performance : réduire le JavaScript chargé, différer ce qui n’est pas indispensable (defer, chargement à la demande), optimiser les images, mettre en cache. Les outils de développement du navigateur et Lighthouse mesurent les deux.' } + ], + synthese: ['const/let, fonctions fléchées, déstructuration, ===, modules', 'DOM : querySelector, textContent, addEventListener, délégation', 'Jamais innerHTML avec une saisie utilisateur', 'Promesses, async/await, try/catch, Promise.all', 'fetch : tester response.ok ; WebSockets pour le temps réel', 'TypeScript : interfaces et vérification avant exécution', 'RGAA/WCAG : éléments natifs, clavier, contraste, aria-live ; performance'], + qcm: [ + ['Que vaut "1" === 1 ?', ['true', 'false', 'undefined', 'erreur'], 1, 'Pas de conversion de type.'], + ['Quelle propriété insérer pour afficher un texte saisi par l’utilisateur ?', ['innerHTML', 'textContent', 'outerHTML', 'eval'], 1, 'Pas d’interprétation HTML.'], + ['Quand fetch rejette-t-il sa promesse ?', ['Sur un 404', 'Sur un 500', 'En cas d’erreur réseau', 'Jamais'], 2, 'Tester response.ok.'], + ['À quoi sert await ?', ['Bloquer le navigateur', 'Attendre le résultat d’une promesse dans une fonction async', 'Créer une boucle', 'Déclarer une variable'], 1, 'Sans bloquer le fil principal.'], + ['Pourquoi préférer

  • Élèves actifs

    608

    +86 ce mois

  • Taux d’activité

    91 %

    élèves connectés / semaine

  • Labs réalisés

    4 210

    depuis la rentrée

  • Heures de VM

    2 940 h

    sur 9 120 h incluses

  • Élèves accompagnés

    47

    suite à une alerte

+

Établissements

ÉtablissementClassesÉlèvesActivitéCompétences validéesStatut

Lycée Jean-Jaurès

Montreuil

611894 %72 %Actif

Lycée Louis-Armand

Paris 15e

920490 %68 %Actif

Lycée Saint-Exupéry

Marseille

48688 %61 %Actif

IUT de Villetaneuse

Villetaneuse

716092 %70 %Actif

Lycée Condorcet

Saint-Priest

24071 %55 %Démarrage

Lycée Vauban

Brest

00——Invité
+
+

Compétences les moins acquises

BTS SIO et CIEL
  • Services réseau (DHCP, DNS)38 %
  • Scripts Bash46 %
  • VLAN et commutation52 %
  • Sécurité des accès58 %
  • Adressage IPv466 %

Piste : proposer une formation académique sur les services réseau.

+

Rapports

  • Bilan de rentrée 2026

    PDF · 12 pages · 01/10

  • Usage par établissement

    CSV · 29/09

  • Compétences par filière

    PDF · 6 pages · 29/09

+
+ + + + + + + diff --git a/portail/resources/views/accueil.blade.php b/portail/resources/views/accueil.blade.php new file mode 100644 index 0000000..bead766 --- /dev/null +++ b/portail/resources/views/accueil.blade.php @@ -0,0 +1,75 @@ +{{-- resources/views/accueil.blade.php --}} + + + + + + Syllabus — Une vraie machine Linux pour chaque élève + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + + + + + +
+ +
+ +

Cours · TP sur machine · suivi des élèves

Enseigner l’informatique, du lycée au master.

Des cours gratuits, un vrai Linux pour chaque élève et toute la classe sur un écran. Dans le navigateur, rien à installer.

Créer une classe gratuite

Sans carte bancaire · élève ? rejoindre ma classe

  • Hébergé en France

    Paris

  • RGPD

    données minimisées

  • 20 cursus

    du lycée au master

  • 443 seulement

    aucun port à ouvrir

  • 0 €

    sans carte bancaire

Pour l’élève

Un terminal Linux en 5 secondes.

Chaque élève a sa propre machine Debian. Il clique, la console s’ouvre. Aucun poste du lycée à configurer.

Essayer un lab sans compte →

eleve@lab-07 · Debian 12
eleve@lab-07:~$ ip -br addr
+ens18  UP  10.20.7.14/24
+eleve@lab-07:~$ ping -c1 10.20.7.1
+64 bytes from 10.20.7.1: time=0.4 ms
+eleve@lab-07:~$  

Correction

Chaque étape se vérifie toute seule.

Le lab contrôle directement la machine. L’élève sait s’il a réussi, et reçoit un indice s’il bloque.

  1. 1. Afficher l’adresse IP✓
  2. 2. Joindre la passerelle✓
  3. 3. Configurer le DNSÀ vérifier
  4. 4. Tester la résolution
  5. Indice 1 : le fichier se trouve dans /etc. Regardez son nom avec ls.

Pour l’enseignant

Le roi du silence : la fonction « Lever la main ».

Vos élèves n’auront plus que le droit de tousser ou de respirer ! Les demandes arrivent dans l’ordre. Vous ouvrez la console de l’élève sans vous déplacer.

Voir le tableau de bord prof →

Mains levées

3 en attente

  1. 1Noraétape 3 · depuis 2 minOuvrir sa console
  2. 2Samiétape 2 · depuis 4 minOuvrir sa console
  3. 3Jadeétape 1 · depuis 6 minOuvrir sa console

Questions fréquentes.

Combien ça coûte ?

Les cours sont gratuits. Les TP sur VM : 29 € par mois pour une classe, ou 24 € par élève et par an pour un établissement.

Faut-il installer quelque chose ?

Non. Un navigateur suffit, même sur un vieux PC ou un Chromebook. Tout passe par HTTPS, port 443.

Comment inscrire mes élèves ?

Par code de classe, import CSV ou saisie rapide. Chaque élève reçoit une fiche de connexion, sans e-mail.

Les cours se téléchargent-ils ?

Oui. CM, TD avec cases à remplir, correction et QCM papier, en PDF.

Où sont les données ?

Sur un cloud hébergé en France. Les données des élèves sont supprimées 90 jours après l’archivage de la classe.

Voir tous les tarifs →

Votre prochain TP commence dans 5 secondes.

Créer une classe gratuite

Sans carte bancaire

+ + + + + + + diff --git a/portail/resources/views/aide.blade.php b/portail/resources/views/aide.blade.php new file mode 100644 index 0000000..f8e0ddb --- /dev/null +++ b/portail/resources/views/aide.blade.php @@ -0,0 +1,68 @@ + + + + + + Aide et tutoriels · Syllabus + + + + + + + + + + + + + +
+ +
+ +
+

Centre d’aide

Aide et tutoriels

Des vidéos courtes et des guides pour prendre Syllabus en main.

Tutoriels vidéo

Vidéos en cours de tournage : les vignettes sont des emplacements.

+

Guides pas à pas

+

Vous ne trouvez pas ?

Écrivez à adam@belkacemi.me. Chaque question aide à améliorer l’outil.

+
+
+ + + + + + diff --git a/portail/resources/views/auth/connexion.blade.php b/portail/resources/views/auth/connexion.blade.php new file mode 100644 index 0000000..285dddc --- /dev/null +++ b/portail/resources/views/auth/connexion.blade.php @@ -0,0 +1,31 @@ +@extends('layouts.vitrine') + +@section('titre', 'Se connecter') + +@section('contenu') +
+ +
+

Se connecter.

+
+
+ @csrf +

Je suis élève

+ @error('identifiant')

{{ $message }}

@enderror + + +

Ils sont sur ta fiche de connexion. Tu peux aussi scanner le QR.

+
+
+
+ @csrf +

Je suis enseignant

+ @error('email')

{{ $message }}

@enderror + + +
Créer une classe
+
+
+
+
+@endsection diff --git a/portail/resources/views/auth/inscription.blade.php b/portail/resources/views/auth/inscription.blade.php new file mode 100644 index 0000000..1fbb85c --- /dev/null +++ b/portail/resources/views/auth/inscription.blade.php @@ -0,0 +1,68 @@ +@extends('layouts.vitrine') + +@section('titre', 'Créer une classe') + +@section('contenu') +
+ +
+
+

Enseignants

+

Créer une classe.

+

Gratuit · sans CB

+

Ensuite, ajoutez vos élèves par code de classe, import CSV ou saisie rapide. Pour les mineurs, on ne demande que le nom, le prénom et le groupe.

+
+
+ @csrf + @if ($errors->any()) +
+
    @foreach ($errors->all() as $erreur)
  • {{ $erreur }}
  • @endforeach
+
+ @endif + + + + + +
+ + + +
+ + +
+

Aucune carte bancaire demandée.

+
+
+
+ +@endsection diff --git a/portail/resources/views/banque.blade.php b/portail/resources/views/banque.blade.php new file mode 100644 index 0000000..e986f67 --- /dev/null +++ b/portail/resources/views/banque.blade.php @@ -0,0 +1,185 @@ +@extends('layouts.vitrine') + +@section('titre', 'Banque de sujets') + +@section('contenu') +
+
+
+

Espace enseignant · Banque

+

Pour les profs

+

Banque de sujets et de questions

+

1 240 questions classées par cursus et par notion. Modifiez-les, ajoutez les vôtres, composez vos sujets.

+
+ + Nouvelle question +
+
+ +
+
+
+

Début d’heure

+

Un quiz flash pour lancer le cours.

+

Choisissez les outils à réviser. Syllabus tire les questions dans la banque. Un diaporama s’ouvre au tableau. Les élèves se connectent avec leur identifiant depuis leur téléphone ou leur PC, et vous voyez tout de suite qui manque.

+ Voir le diaporama →Voir la vue élève → +
+
+

Outils à réviser

+
+ + + + + + + + + + + + + + + +
+
+ + + +
+
+ + +
+
+
+
+ +
+ + +
+

Questions · 7 affichées sur 1 240

+
    +
  • +
    Choix uniqueBTS SIO · Bloc 2Adressage IPv4Moyen
    +

    Quelle est l’adresse réseau de 10.0.3.200/22 ?

    +

    2 pts · utilisée 34 fois · réussite 38 %

    +
    Modifier
    +
  • +
  • +
    Choix uniqueBTS CIEL · RéseauxAdressage IPv4Facile
    +

    Combien d’hôtes utilisables dans un /27 ?

    +

    1 pt · utilisée 51 fois · réussite 71 %

    +
    Modifier
    +
  • +
  • +
    Choix multipleNSI · PremièreCommandes LinuxFacile
    +

    Quelles commandes affichent les interfaces réseau sous Debian ?

    +

    1 pt · utilisée 22 fois · réussite 64 %

    +
    Modifier
    +
  • +
  • +
    Vrai / fauxBTS SIO · Bloc 2VLANFacile
    +

    Un VLAN isole le trafic au niveau 2 du modèle OSI.

    +

    1 pt · utilisée 40 fois · réussite 82 %

    +
    Modifier
    +
  • +
  • +
    Réponse courteBTS SIO · Bloc 1Droits LinuxMoyen
    +

    Écrivez la commande qui donne les droits rwxr-x--- au fichier notes.txt.

    +

    2 pts · utilisée 18 fois · réussite 47 %

    +
    Modifier
    +
  • +
  • +
    Réponse numériqueBUT R&T · S1Services réseauFacile
    +

    Quel port utilise par défaut le protocole SSH ?

    +

    1 pt · utilisée 29 fois · réussite 88 %

    +
    Modifier
    +
  • +
  • +
    Texte à trousBTS CIEL · RéseauxRoutageDifficile
    +

    Complétez : la route par défaut s’écrit ___ via 192.168.1.1.

    +

    3 pts · utilisée 9 fois · réussite 29 %

    +
    Modifier
    +
  • +
+
+ + +
+ +
+
+

Créer ou modifier une question

+
+ + +
+
Réponses (cochez la bonne) + + + + +
+ + + +
+
+
+
+ +@endsection diff --git a/portail/resources/views/bibliotheque.blade.php b/portail/resources/views/bibliotheque.blade.php new file mode 100644 index 0000000..ed32d02 --- /dev/null +++ b/portail/resources/views/bibliotheque.blade.php @@ -0,0 +1,121 @@ +@extends('layouts.vitrine') + +@section('titre', 'Bibliothèque') + +@section('contenu') +
+
+

Bibliothèque du prof

+

Pour les profs

+

Tout le référentiel, fiche par fiche.

+

Du SNT au master, en passant par l’école d’ingénieurs. Chaque fiche regroupe cours, quiz, TD, labs et examens. Un même chapitre sert plusieurs formations : OSPF compte pour la NSI, le BTS SIO, le BTS CIEL, le BUT R&T et les titres pro.

+
+ +
+
+
+
+
+ + + +
+ + + +

Ma séquence : 0

+
+
+ + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +@endsection diff --git a/portail/resources/views/classes/suivi.blade.php b/portail/resources/views/classes/suivi.blade.php new file mode 100644 index 0000000..a192cbe --- /dev/null +++ b/portail/resources/views/classes/suivi.blade.php @@ -0,0 +1,157 @@ +@extends('layouts.vitrine') + +@section('titre', 'Suivi de classe') + +@section('contenu') +
+
+
+

Espace enseignant · Suivi

+

Pour les profs

+

BTS SIO 1 · Groupe A

+

Bloc 2 · Administration des systèmes et des réseaux · 24 élèves

+
+ +
+
+
+
+
Progression moyenne
52 %
+
Moyenne aux QCM
13,4
+
Actifs cette semaine
21 / 24
+
À aider maintenant
2
+
+ +
+
+

Élèves

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ÉlèveAdressage IPv4VLANInter-VLANDHCPDNS BINDQCMTemps VM
LLéa M.Terminé18/2012 h 40
YYanis B.14/209 h 10
NNora KaciMain levée11/208 h 05
TTom R.16/2010 h 30
IInès D.9/205 h 20
SSami L.Bloqué 9 min7/206 h 45
CChloé P.En avance19/2013 h 15
AAdam F.13/208 h 50
+
+

8 élèves sur 24 affichés · triés par besoin d’aide

+
+ + +
+
+@endsection diff --git a/portail/resources/views/cockpit.blade.php b/portail/resources/views/cockpit.blade.php new file mode 100644 index 0000000..5c23a45 --- /dev/null +++ b/portail/resources/views/cockpit.blade.php @@ -0,0 +1,185 @@ + + + + + + Cockpit prof · Syllabus + + + + + + + + + + + + + +
+ +
+ +
+
+
+ +
+
+

Cockpit prof

Pour les profs

+

BTS SIO 1 · Groupe A

16 élèves · mardi 29 septembre · 10:22

+
+ Quiz flash + Programmer un DM + +
+
+
    +
  • Présents

    15/16

    1 absent signalé

  • +
  • Moyenne QCM

    13,4

    +0,8 sur 4 semaines

  • +
  • À accompagner

    3

    +1 cette semaine

  • +
  • Mains levées

    3

    attente moy. 2 min

  • +
  • DM rendus

    13/16

    échéance vendredi

  • +
+
+

Avancement du lab « VLAN »

6 étapes · médiane : étape 3

+ +

Largeur = nombre d’élèves à chaque étape. 2 élèves ont terminé.

+
+
+
+

Élèves

+
+ +
+
+
+
+ + +
Tendance
+
+
+
+

Surveillance d’examen

Inactif
+

E6 blanc SISR · 1 h 30

00:00

  1. Lancez l’examen pour suivre les sorties d’onglet et de fenêtre.
+
+
+

Devoirs maison

Tout voir
+
  • DM 3 · Plan d’adressage

    Rendu ven. 03/10

    13/16

  • TP maison · Scripts Bash

    Rendu lun. 06/10

    6/16

  • QCM · Lecture CM VLAN

    Rendu ce soir

    15/16

+
+
+
+
+
+
+
+

Mains levées

ordre d’arrivée
+
    +
    +
    +

    Priorités du jour

    +
    • Léa M. Accompagner

      3 QCM sous 8/20. Proposer le premier rang et réexpliquer les VLAN.

    • Lucas M. Accompagner

      Cours lu en 2 min au lieu de 15. QCM de lecture à refaire.

    • Hugo P. Relancer

      DM 3 non rendu. Relance automatique jeudi 18 h.

    • Inès K. Stimuler

      Tous les labs réussis. Défi connexe proposé : QoS et priorisation.

    +
    +
    +

    Observation rapide

    Classée par élève pour le conseil de classe.

    +
    +
    +
    + + +
    + + + + + + diff --git a/portail/resources/views/cohortes.blade.php b/portail/resources/views/cohortes.blade.php new file mode 100644 index 0000000..f6df979 --- /dev/null +++ b/portail/resources/views/cohortes.blade.php @@ -0,0 +1,72 @@ + + + + + + Suivi des cohortes · Syllabus + + + + + + + + + + + + + +
    + +
    + +
    +

    Scolarité

    Pour les profs

    +

    Suivi des cohortes

    Assiduité, résultats et alertes par classe. Cliquez sur une classe pour voir ses étudiants.

    • Étudiants

      113

      6 classes

    • Assiduité moyenne

      89 %

      séances planifiées

    • Compétences acquises

      64 %

      référentiels BTS / BUT

    • Alertes ouvertes

      15

      dont 4 nouvelles

    +

    Cohortes

    ClasseEffectifAssiduitéMoyenneCompétencesAlertes
    BTS SIO 1 A1693 %12,468 %1 ouverteVoir la classe →
    BTS SIO 1 B1588 %11,161 %3 ouvertesVoir la classe →
    BTS SIO 2 A1895 %13,279 %AucuneVoir la classe →
    BTS SIO 2 B1484 %10,657 %4 ouvertesVoir la classe →
    BTS CIEL 12290 %11,863 %2 ouvertesVoir la classe →
    BUT R&T 12886 %11,359 %5 ouvertesVoir la classe →
    +
    +

    Assiduité · BTS SIO 2 B

    ÉtudiantPrésentAbsentJustifiéTaux
    Petit Hugo186172 %
    Leroy Tom213388 %
    Diallo Adam231196 %

    Absence = séance planifiée sans connexion ni présence pointée. Une connexion hors séance ne compte pas comme présence.

    +

    Règles d’alerte

    +
    +

    Rôles et droits

    DonnéeEnseignantProf principalCPEDirectionIntendance
    Notes de ses classesLire, écrireLireLireLire—
    AssiduitéPointerLireLire, justifierLire—
    ObservationsÉcrireLire, écrireLire, écrireLire—
    Fiche élève complèteSes élèvesSa classeTousTous—
    ExportsSes classesSa classeTousTous—
    Licences et factures———LireLire, gérer

    Chaque consultation de fiche et chaque export sont journalisés. Le journal est consultable par la direction et le DPO.

    +
    + + + + + + diff --git a/portail/resources/views/correction.blade.php b/portail/resources/views/correction.blade.php new file mode 100644 index 0000000..225ee5c --- /dev/null +++ b/portail/resources/views/correction.blade.php @@ -0,0 +1,75 @@ + + + + + + Correction assistée · Syllabus + + + + + + + + + + + + + +
    + +
    + +
    +

    Correction assistée

    Pour les profs

    +

    Copie de Léa M.

    Syllabus pré-corrige selon votre barème. Vous validez, ajustez et commentez.

    +

    Rendu de l’élève

    Question 2 · masque

    J’emprunte 2 bits car 2² = 4 sous-réseaux. Le masque devient /26 soit 255.255.255.192.

    Question 4 · plan d’adressage

    SRRéseauBroadcast
    SR1.0.63
    SR2.64.127
    SR3.128.190 ✕
    SR4.192.255

    Question 5 · terminal (rejoué)

    eleve@srv:~$ sudo ip addr add 192.168.10.65/26 dev eth0
    +eleve@srv:~$ ip a show eth0
    +    inet 192.168.10.65/26 scope global eth0
    +

    Barème et pré-correction

    pré-correction · à valider
    • ✓

      Masque choisi et justifié

      Suggestion : /26 correct, justification 2² = 4

    • ✕

      Plan d’adressage complet

      Suggestion : SR3 : broadcast .190 au lieu de .191

    • ✓

      Commande de configuration

      Suggestion : ip addr add 192.168.10.65/26 dev eth0

    • ✕

      Tests de connectivité

      Suggestion : ping manquant entre SR1 et SR2

    • ?

      Rédaction et clarté

      À évaluer par vous

    +

    Note

    15/20

    +
    +

    La pré-correction vérifie ce qui est vérifiable (calculs, commandes rejouées dans la VM, tests de code). Elle ne décide jamais seule : la note n’est publiée qu’après votre validation.

    +
    +
    + + + + + + diff --git a/portail/resources/views/cours/cm.blade.php b/portail/resources/views/cours/cm.blade.php new file mode 100644 index 0000000..3ba5026 --- /dev/null +++ b/portail/resources/views/cours/cm.blade.php @@ -0,0 +1,243 @@ +@extends('layouts.vitrine') + +@section('titre', 'CM · Adressage IPv4') + +@section('contenu') +
    +
    + CM +

    Adressage IPv4 et sous-réseaux

    +

    Lu : 0 %

    +

    00:00/ 20:00 conseillées

    +
    +
    +
    +
    +
    + +

    Pour les élèves

    +

    Adressage IPv4 et sous-réseaux

    +

    Lis le cours jusqu’au bout. Le QCM de lecture se débloque quand toutes les parties ont été lues et que 5 minutes se sont écoulées.

    +
      +
    1. 1 · CM
    2. 2 · QCM de lecture
    3. 3 · TD optionnel
    4. 4 · TP
    5. 5 · Examen
    6. +
    +
    +
    +
    + +
    +
    +

    1. Pourquoi des adresses ?

    +

    Sur un réseau, chaque machine doit pouvoir être trouvée. Comme une lettre a besoin d’une adresse postale, un paquet a besoin d’une adresse IP de destination.

    +
    +

    La rue

    = le réseau

    Toutes les maisons de la rue partagent le même nom de rue.

    +

    Le numéro

    = l’hôte

    Chaque maison a son numéro, unique dans la rue.

    +
    +

    Une adresse IPv4 contient les deux informations. Le masque indique où s’arrête la rue et où commence le numéro.

    +
    +
    +

    Vidéo de contexte

    +

    Ton prof peut ajouter ici une vidéo YouTube pour illustrer le cours.

    +
    +

    Espace prof · la vidéo s’affiche en mode sans cookie (youtube-nocookie.com).

    +
    +
    +

    2. Une adresse, 32 bits

    +

    Une adresse IPv4 est un nombre de 32 bits. Pour la lire plus facilement, on la découpe en 4 octets de 8 bits, écrits en décimal et séparés par des points.

    +
    +

    192.168.10.77

    +
    11000000.10101000.00001010.01001101
    +
    192 = 11000000 · 168 = 10101000 · 10 = 00001010 · 77 = 01001101
    +
    +

    Chaque octet va de 0 (00000000) à 255 (11111111). Une adresse comme 192.168.10.300 est donc impossible.

    +
    +
    +

    3. Le masque de sous-réseau

    +

    Le masque est lui aussi un nombre de 32 bits. Il commence par une suite de 1 (la partie réseau) suivie d’une suite de 0 (la partie hôte). La notation CIDR écrit simplement le nombre de 1 : /26.

    +
    +

    /26 = 255.255.255.192

    +
    11111111.11111111.11111111.11000000
    +
    26 bits de réseau6 bits d’hôte
    +
    +

    Dans le dernier octet, les 2 premiers bits valent 1 : 128 + 64 = 192. D’où 255.255.255.192.

    +
    +
    +

    4. Trouver l’adresse réseau : le ET logique

    +

    Pour trouver le réseau d’une adresse, on fait un ET logique bit à bit entre l’adresse et le masque. Le résultat vaut 1 seulement si les deux bits valent 1.

    +
    +
    Table du ET logique
    ABA ET B
    000
    010
    100
    111
    +
    +

    Dernier octet seulement

    +

    01001101 (77)

    +

    ET 11000000 (192)

    +

    = 01000000 (64)

    +
    +
    +

    Les trois premiers octets du masque valent 255 : ils recopient l’adresse telle quelle. L’adresse réseau de 192.168.10.77/26 est donc 192.168.10.64.

    +
    +
    +

    5. Diffusion et nombre d’hôtes

    +

    L’adresse de diffusion (broadcast) met tous les bits d’hôte à 1. Un paquet envoyé à cette adresse arrive à toutes les machines du réseau.

    +

    Ici, les 6 bits d’hôte à 1 donnent 01111111 = 127 : la diffusion est 192.168.10.127.

    +
    Hôtes utilisables = 2bits d’hôte − 2
    +

    On retire 2 adresses : celle du réseau et celle de diffusion. Avec 6 bits : 2⁶ − 2 = 62 hôtes, de 192.168.10.65 à 192.168.10.126.

    +
    +
    +

    6. Découper un réseau en sous-réseaux

    +

    Emprunter des bits d’hôte pour le réseau crée des sous-réseaux plus petits. Emprunter 2 bits à un /24 donne 2² = 4 sous-réseaux en /26.

    +
    +

    192.168.10.0/24 découpé en 4 × /26

    +
    +
    .0
    → .63
    .64
    → .127
    .128
    → .191
    .192
    → .255
    +
    +
    Chaque bloc : 64 adresses, 62 hôtes. L’adresse .77 tombe dans le 2ᵉ bloc.
    +
    +

    Astuce : la taille d’un bloc vaut 256 moins la valeur du dernier octet du masque. Pour 192 : 256 − 192 = 64.

    +
    +
    +

    7. Adresses privées et adresses particulières

    +

    Certaines plages sont réservées aux réseaux internes (RFC 1918). Elles ne sont pas routées sur Internet.

    +
    • 10.0.0.0/8
    • 172.16.0.0/12
    • 192.168.0.0/16
    +
    • 127.0.0.1la boucle locale : la machine elle-même
    • 169.254.x.xadresse automatique quand aucun DHCP ne répond
    • 0.0.0.0« n’importe quelle adresse » ou « pas encore d’adresse »
    • 255.255.255.255diffusion limitée au réseau local
    +
    +
    +

    8. À retenir

    +
    • Une adresse IPv4 = 32 bits = 4 octets de 0 à 255.
    • Le masque sépare la partie réseau de la partie hôte. /n = n bits à 1.
    • Adresse réseau = adresse ET masque. Diffusion = bits d’hôte à 1.
    • Hôtes utilisables = 2bits d’hôte − 2.
    • Taille d’un bloc = 256 − dernier octet du masque.
    +
    +
    +

    QCM de lecture

    +

    4 questions sur les notions du texte, pour vérifier que tu as bien lu.

    +
    +

    🔒 Encore un peu de lecture

    +

    Parties lues : 0 / 8 · Temps : 0 / 5 min

    + +
    + +
    +
    +

    Pour aller plus loin

    +

    Des plateformes externes pour s’entraîner encore, à ton rythme. Elles ne font pas partie de Syllabus.

    + +
    + +
    +
    + +@endsection diff --git a/portail/resources/views/cours/fiche.blade.php b/portail/resources/views/cours/fiche.blade.php new file mode 100644 index 0000000..3b89d1a --- /dev/null +++ b/portail/resources/views/cours/fiche.blade.php @@ -0,0 +1,174 @@ + + + + + + Cours · Syllabus + + + + + + + + + + + + + +
    + +
    + +
    +
    +
    + ← Tous les cours +

    +

    Pour les élèves

    +

    Chargement…

    +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + diff --git a/portail/resources/views/cours/index.blade.php b/portail/resources/views/cours/index.blade.php new file mode 100644 index 0000000..4142f2b --- /dev/null +++ b/portail/resources/views/cours/index.blade.php @@ -0,0 +1,41 @@ +@extends('layouts.vitrine') + +@section('titre', 'Cours gratuits') + +@section('contenu') +

    Gratuit, sans compte

    99 cours gratuits, calés sur vos référentiels.

    Chaque cours suit le découpage de l’Éducation nationale : un CM pour comprendre, un TD pour s’exercer sur papier, un TP sur une vraie VM, un QCM corrigé et expliqué.

    99 cours · 45 TD · 59 TP sur VM · 98 QCM corrigés

    +
    +
    +
    +

    {{ $c['nom'] }}

    +

    {{ $c['sous_titre'] }} · {{ collect($c['blocs'])->sum(fn ($b) => count($b['cours'])) }} cours

    +
    +
    + @foreach ($c['blocs'] as $b) +
    +

    {{ $b['titre'] }}

    + +
    + @endforeach +
    +
    +
    + @endforeach + +@endsection diff --git a/portail/resources/views/cours/show.blade.php b/portail/resources/views/cours/show.blade.php new file mode 100644 index 0000000..68ee32f --- /dev/null +++ b/portail/resources/views/cours/show.blade.php @@ -0,0 +1,225 @@ +@extends('layouts.vitrine') + +@section('titre', 'Adressage IPv4 et sous-réseaux') + +@section('contenu') +
    +
    + +

    Pour les élèves

    +

    Adressage IPv4 et sous-réseaux

    +
    + BTS SIO SISR · Bloc 2 + Aussi : BTS CIEL, BUT R&T S1 + 4 h +
    +
    +
    +
    + +
    +
    +

    1 · CM · Lire le cours

    + +
    +

    Adressage IPv4 et sous-réseaux · cours complet

    +

    8 parties illustrées : bits, masque, ET logique, diffusion, découpage, adresses privées. Chronométré, à lire en entier pour débloquer le QCM de lecture.

    +
    +
    ⏱ Lecture chronométréeLire le CM →
    +
    +
    + +
    +
    +

    2 · QCM · Valider les notions

    +

    5 questions · correction expliquée

    +
    +
    + +
    + Question 1 +

    Combien d’adresses d’hôtes utilisables dans un réseau /27 ?

    +
    + + + + +
    + +
    +
    + Question 2 +

    Quelle est l’adresse réseau de 10.0.3.200/22 ?

    +
    + + + + +
    + +
    +
    + Question 3 +

    Quel masque correspond à /26 ?

    +
    + + + + +
    + +
    +
    + Question 4 +

    192.168.1.130/25 et 192.168.1.100/25 sont-ils dans le même réseau ?

    +
    + + +
    + +
    +
    + Question 5 +

    Quelle commande affiche les adresses IP sous Debian ?

    +
    + + + + +
    + +
    +
    + + + +
    +
    +
    + +
    +

    3 · TD · S’exercer optionnel

    +
      +
    1. +

      Exercice 1

      +

      Pour 172.16.5.130/25, donnez l’adresse réseau, la diffusion et le nombre d’hôtes.

      +
      + Voir la correction expliquée +

      Un /25 coupe le dernier octet en blocs de 128. 130 est dans le bloc 128–255. Réseau : 172.16.5.128. Diffusion : 172.16.5.255. Hôtes : 2⁷ − 2 = 126.

      +
      +
    2. +
    3. +

      Exercice 2

      +

      Découpez 192.168.1.0/24 en 4 sous-réseaux de même taille.

      +
      + Voir la correction expliquée +

      4 sous-réseaux = 2 bits empruntés, donc /26. Sous-réseaux : 192.168.1.0, .64, .128 et .192, avec 62 hôtes chacun.

      +
      +
    4. +
    5. +

      Exercice 3

      +

      Le service comptabilité a 50 postes. Quel est le plus petit sous-réseau qui convient ?

      +
      + Voir la correction expliquée +

      Il faut 2ʰ − 2 ≥ 50. Avec h = 6 : 64 − 2 = 62. On garde 6 bits d’hôte, soit un /26 (255.255.255.192). Un /27 ne donnerait que 30 hôtes.

      +
      +
    6. +
    +
    + +
    +

    4 · TP · Pratiquer sur VM

    +
    +
    +

    Deux VM, un routeur

    +
      +
    1. 1. Placez deux VM dans 192.168.10.0/26.
    2. +
    3. 2. Vérifiez qu’elles se joignent avec ping.
    4. +
    5. 3. Passez la seconde en 192.168.10.70/26.
    6. +
    7. 4. Constatez l’échec, puis ajoutez une route.
    8. +
    +

    4 étapes vérifiées automatiquement · 45 min

    + Lancer le TP +
    +
    +

    eleve@vm1:~$ sudo ip addr add 192.168.10.10/26 dev ens3

    +

    eleve@vm1:~$ ip -br addr

    +

    ens3 UP 192.168.10.10/26

    +

    eleve@vm1:~$ ping -c 1 192.168.10.11

    +

    1 packets transmitted, 1 received

    +

    ✓ Étape 2 validée

    +
    +
    +
    + +
    +

    5 · EXAMEN · Quand le prof le décide

    +
    +

    Le prof compose son sujet dans la banque de questions, le programme pour la classe, et suit l’épreuve en direct : sorties d’onglet, copier-coller bloqué, remise automatique.

    + +
    +
    + + + + + + +
    +
    + +@endsection diff --git a/portail/resources/views/devoirs.blade.php b/portail/resources/views/devoirs.blade.php new file mode 100644 index 0000000..578c58e --- /dev/null +++ b/portail/resources/views/devoirs.blade.php @@ -0,0 +1,65 @@ +@extends('layouts.vitrine') + +@section('titre', 'Devoirs maison') + +@section('contenu') +
    +
    +

    Espace enseignant · Devoirs

    Pour les profs

    +

    Devoirs maison

    Donnez un TP, un QCM, un TD ou un exercice de code à faire à la maison. Programmez l’ouverture et la date limite, suivez les rendus, corrigez et notez.

    + + Nouveau devoir +
    +
    +
    +
    +

    Devoirs programmés

    +
      +
    • TP sur VMBTS SIO 1 · A⏰ Ven. 3 oct. · 23:59

      TP Routage inter-VLAN

      18 / 24 rendus · 2 en retard

    • +
    • QCMBTS SIO 1 · A⏰ Lun. 6 oct. · 08:00

      QCM Adressage IPv4 · 2ᵉ essai

      11 / 24 rendus

    • +
    • IDE2nde NSI · B⏰ Mer. 8 oct. · 18:00

      Défi Python : somme d’une liste

      5 / 28 rendus

    • +
    • TDBTS SIO 1 · A⏰ Clos · 29 sept.

      TD Découpage en sous-réseaux

      24 / 24 rendus · 3 en retard

    • +
    +
    +
    +
    +

    Rendus · TD Découpage en sous-réseaux

    + + + + + + +
    ÉlèveÉtatNote
    Léa M.30 sept. · 17:42Rendu18/20
    Yanis B.1 oct. · 22:10Rendu14/20
    Nora Kaci2 oct. · 09:05En retard—
    Sami L.—Non rendu—
    Chloé P.29 sept. · 20:31Rendu19/20
    +
    +
    +
    +
    +
    +
    +

    Programmer un devoir

    +
    + + + + + +
    Options + + + +
    +
    Choisir dans la bibliothèque
    +
    +
    +
    + +@endsection diff --git a/portail/resources/views/eleve.blade.php b/portail/resources/views/eleve.blade.php new file mode 100644 index 0000000..5420fbb --- /dev/null +++ b/portail/resources/views/eleve.blade.php @@ -0,0 +1,65 @@ +@extends('layouts.vitrine') + +@section('titre', 'Espace élève') + +@section('contenu') +
    + +
    +
    +

    Espace élève · {{ $eleve->identifiant }}

    +

    Pour les élèves

    +

    Salut {{ $eleve->prenom }}.

    +

    {{ $eleve->classe->nom }}@if($eleve->classe->formation) · {{ $eleve->classe->formation }}@endif

    +
    + Reprendre mon TP +
    +
    + +@endsection diff --git a/portail/resources/views/espace-eleve.blade.php b/portail/resources/views/espace-eleve.blade.php new file mode 100644 index 0000000..e533953 --- /dev/null +++ b/portail/resources/views/espace-eleve.blade.php @@ -0,0 +1,169 @@ + + + + + + Mon espace élève · Syllabus + + + + + + + + + + + + + +
    + +
    + +
    +
    +
    + +
    +
    +

    Mon espace élève

    Pour les élèves

    +

    Bonjour Sarah !

    Mardi 29 septembre · 2 devoirs cette semaine · 1 examen dans 9 jours

    + Reprendre le lab VLAN · étape 6 +
    +
      +
    • Moyenne QCM

      16,0

      +1,2 sur 4 semaines

    • Labs réussis

      11/14

      dont 3 sans indice

    • Devoirs à rendre

      2

      le prochain vendredi

    • Série

      6 j

      d’entraînement d’affilée

    • +
    +
    +
    +

    Septembre – octobre 2026

    +

    Lun

    Mar

    Mer

    Jeu

    Ven

    Sam

    Dim

    +
    +
    ExamenDevoirQuizCours / TP
    +
    +
    +

    Au programme

    mardi 29/09

    +
      +
      +
      +
      +
      +

      Mes devoirs

      2 à rendre
      +
      • DM 3 · Plan d’adressage

        Réseaux · M. Belkacemi · ven. 03/10 · 23 h 59

        À rendre
        60 %
      • TP maison · Scripts Bash

        Linux · Mme Roche · lun. 06/10 · 18 h

        À rendre
        20 %
      • QCM · Lecture CM VLAN

        Réseaux · hier

        Rendu · 17/20
        100 %
      +
      +
      +

      Examens à venir

      +
      • J-9

        E6 blanc SISR

        Administration systèmes et réseaux · jeu. 08/10 · 8 h

        Réviser
      • J-14

        Contrôle · Adressage IPv4

        Réseaux · 1 h · mar. 13/10 · 10 h

        Réviser
      • —

        QCM flash · Commandes Linux

        Début d’heure · 5 min · chaque lundi

        Réviser
      +

      Pendant un examen, reste sur la page : les changements d’onglet et de fenêtre sont signalés au prof.

      +
      +
      +
      +

      S’entraîner

      +
      +
        +
        +
        +

        Ma progression par compétence

        référentiel BTS SIO

        +
        • Installer et configurer un poste85 %
        • Adressage IPv4 et sous-réseaux72 %
        • VLAN et commutation64 %
        • Scripts Bash48 %
        • Sécurité des accès55 %
        • Services réseau (DHCP, DNS)30 %
        +
        +
        +
        +
        + + +
        + + + + + + diff --git a/portail/resources/views/espace-etablissement.blade.php b/portail/resources/views/espace-etablissement.blade.php new file mode 100644 index 0000000..f14c9fa --- /dev/null +++ b/portail/resources/views/espace-etablissement.blade.php @@ -0,0 +1,75 @@ + + + + + + Espace établissement · Syllabus + + + + + + + + + + + + + +
        + +
        + +
        +

        Gestion administrative

        Pour les profs

        +

        Espace établissement

        Abonnement, licences, documents comptables et contrats. Accès réservé à l’intendance et à la direction.

        • Formule Établissement

          20 classes

          renouvelable chaque année

        • Période

          01/09 → 31/08

          renouvellement le 01/07/2027

        • Licences

          118 / 120

          2 disponibles

        • Heures de VM

          2 940 h

          sur 9 120 h incluses

        +

        Licences

        118 actives · 2 libres
        TitulaireClasse / rôleDébutFinStatut
        Martin LéaBTS SIO 1 A01/09/202630/06/2027Active
        Kaci InèsBTS SIO 1 A01/09/202630/06/2027Active
        Petit HugoBTS SIO 2 B01/09/202515/10/2026Fin programmée
        Roche AnneEnseignante01/09/2026—Active
        Leroy TomBTS SIO 1 B01/09/202629/09/2026Retirée

        Import CSV : colonnes nom;prenom;email;classe;date_fin. Une licence se libère automatiquement à la date de fin ; les données de l’étudiant sont supprimées 12 mois après, sauf export demandé.

        +
        +

        Documents comptables

        DocumentN° engagementHTTTCChorus Pro
        Facture F-2026-0412EJ 2026-01872 400,00 €2 880,00 €Déposée
        Bon de commande BC-2026-031EJ 2026-01872 400,00 €2 880,00 €Validé
        Devis D-2026-118—2 400,00 €2 880,00 €Accepté
        Facture F-2025-0298EJ 2025-01421 800,00 €2 160,00 €Payée

        Fournisseur : Syllabus SAS · SIRET 000 000 000 00000 · TVA FR00000000000 · IBAN FR76 0000 0000 0000 0000 0000 000

        +

        Contrats et conventions

        • Conditions générales de vente

          v3 · 01/09/2026 · Acceptées le 03/09/2026

        • Contrat de service établissement

          Formule Établissement · Signé le 03/09/2026

        • Accord de sous-traitance RGPD (art. 28)

          DPA v2 · Signé le 03/09/2026

        • Fiche fournisseur

          SIRET, RIB, contacts · À jour

        • Modèle de bon de commande

          Format administration · Modèle

        +
        +
        +

        Consommation

        Heures de VM2 940 / 9 120 h
        Stockage41 / 200 Go
        +

        Paramètres de l’établissement

        Configurer le SSO et l’API →
        +
        +
        + + + + + + diff --git a/portail/resources/views/etablissements.blade.php b/portail/resources/views/etablissements.blade.php new file mode 100644 index 0000000..b508da9 --- /dev/null +++ b/portail/resources/views/etablissements.blade.php @@ -0,0 +1,14 @@ +@extends('layouts.vitrine') + +@section('titre', 'Établissements') + +@section('contenu') +

        Pour les établissements

        Un projet par établissement. Un réseau par classe.

        Chaque établissement a son espace isolé sur notre cloud hébergé en France, à Paris. Chaque classe a son propre réseau, sans IP publique.

        Demander un devis

        Réponse sous 48 h · bon de commande et Chorus Pro

        Chacun son rôle.

        • Administrateur

          Gère l’établissement

          Crée les classes, gère les enseignants et les quotas.

        • Enseignant

          Anime la séance

          Réserve une séance, suit la classe en direct, prend la main sur une console.

        • Élève

          Travaille

          Se connecte avec sa fiche, lance le lab, lève la main s’il bloque.

        • Formateur

          Forme des adultes

          Anime des sessions en titre professionnel.

        Ce que votre intendance et votre DSI vérifient.

        • Licences, devis, factures

          Ajout et retrait d’élèves, bons de commande, factures déposées sur Chorus Pro, conventions.

          Espace établissement →

        • Intégration et sécurité

          Hébergement en France, RGPD, prérequis réseau, authentification unique.

          Fiche technique →

        • Une bibliothèque à dupliquer

          Des séquences reliées aux référentiels : cours, TD, quiz et lab. Les quiz s’exportent en GIFT et Moodle XML.

          Voir la bibliothèque →

        Équiper votre établissement.

        Dites-nous combien de classes et quelles formations. On vous répond avec une proposition.

        +@csrf + + + + +
        +
        +@endsection diff --git a/portail/resources/views/examen.blade.php b/portail/resources/views/examen.blade.php new file mode 100644 index 0000000..64c33cc --- /dev/null +++ b/portail/resources/views/examen.blade.php @@ -0,0 +1,116 @@ + + + + + + Mode examen · Syllabus + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + +
        +
        + MODE EXAMEN +

        Adressage et VLAN · E6 blanc SISR · Léa M.

        +

        Temps restant59:59

        +
        +
        +
        +
          +
        • ■ Indices désactivés
        • ■ Copier-coller bloqué
        • ■ Sortie d’onglet signalée à l’enseignant
        • ■ VM sans accès Internet
        • Incidents signalés : 0
        • +
        +
        +
        + +
        +
        + Question 5 · 1 point +

        Un service de 50 postes doit avoir son propre sous-réseau. Quel masque choisissez-vous ?

        +
        + + + + +
        +
        + ← Précédente + + Suivante → +
        +
        +
        +
        +

        Partie TP · commutateur-sw1 · console 1

        + vérification à la remise +
        +
        +

        Créez le VLAN 20 « compta » et placez-y l’interface eth2.

        +

        root@sw1:~# ip link add link eth0 name eth0.20 type vlan id 20

        +

        root@sw1:~#

        +
        +
        +
        + +

        La copie est rendue automatiquement à la fin du temps.

        +
        +
        +
        + + + diff --git a/portail/resources/views/export-ent.blade.php b/portail/resources/views/export-ent.blade.php new file mode 100644 index 0000000..c5f5233 --- /dev/null +++ b/portail/resources/views/export-ent.blade.php @@ -0,0 +1,72 @@ + + + + + + Export Pronote et ENT · Syllabus + + + + + + + + + + + + + +
        + +
        + +
        +

        Exports

        Pour les profs

        +

        Export vers Pronote et l’ENT

        Vos notes, présences et appréciations, prêtes à charger dans votre logiciel de vie scolaire.

        +

        1 · Que voulez-vous exporter ?

        +

        2 · Format

        +
        +

        3 · Aperçu du fichier

        notes-bts-sio-1a-dm3.csv
        NomPrénomClasseDevoirNoteSurCoef.Date
        MartinLéaBTS SIO 1 ADM 31120103/10/2026
        NguyenSarahBTS SIO 1 ADM 31620103/10/2026
        KaciInèsBTS SIO 1 ADM 31920103/10/2026
        PetitHugoBTS SIO 1 ADM 3ABS20103/10/2026

        16 lignes · les devoirs non rendus sont marqués ABS · encodage UTF-8

        +

        Charger le fichier dans votre logiciel

        1. 1Téléchargez le fichier CSV ci-dessus.
        2. 2Dans votre logiciel de vie scolaire, ouvrez l’import de notes du service concerné.
        3. 3Choisissez le fichier, vérifiez la correspondance des colonnes, validez.

        Les menus exacts dépendent de votre logiciel et de ses réglages. Un tutoriel vidéo est disponible dans l’aide.

        +
        +
        + + + + + + diff --git a/portail/resources/views/facturation.blade.php b/portail/resources/views/facturation.blade.php new file mode 100644 index 0000000..3303a3e --- /dev/null +++ b/portail/resources/views/facturation.blade.php @@ -0,0 +1,68 @@ + + + + + + Mon abonnement · Syllabus + + + + + + + + + + + + + +
        + +
        + +
        +

        Facturation

        Pour les profs

        +

        Mon abonnement

        M. Belkacemi · BTS SIO 1 A et B

        Offre actuelle

        Enseignant · mensuel

        Actif

        29,00 € HT · 34,80 € TTC / mois

        Prochain paiement le 29/10/2026 · 2 classes · 33/35 élèves

        VM ce mois : 270 h sur 495 h incluses

        +

        Moyen de paiement

        VISA

        •••• 4242

        expire 08/28

        Vous pouvez aussi passer au prélèvement SEPA ou au bon de commande de votre établissement.

        +

        Factures

        N°DateMontant TTCStatut
        F-2026-10429/09/202634,80 €PayéePDF
        F-2026-08129/08/202634,80 €PayéePDF
        F-2026-06029/07/202634,80 €PayéePDF
        +
        + + + + + + diff --git a/portail/resources/views/fiche-eleve.blade.php b/portail/resources/views/fiche-eleve.blade.php new file mode 100644 index 0000000..55bf658 --- /dev/null +++ b/portail/resources/views/fiche-eleve.blade.php @@ -0,0 +1,74 @@ + + + + + + Fiche élève · Syllabus + + + + + + + + + + + + + +
        + +
        + +
        +

        Fiche élève

        Pour les profs

        +

        Léa M.

        Accès journalisé · visible par l’équipe pédagogique et la vie scolaire

        Écrire
        • Moyenne QCM

          7,5

          −4,5 en 4 semaines

        • Labs réussis

          5/14

          3,1 indices par lab

        • DM rendus

          3/3

          moyenne 11/20

        • Temps de lecture

          −62 %

          vs moyenne de la classe

        +
        +

        Compétences du référentiel

        BTS SIO · bloc 1
        • Installer et configurer un poste

          80 %
        • Adressage IPv4

          55 %
        • VLAN et commutation

          40 %
        • Scripts Bash

          35 %
        • Sécurité des accès

          60 %
        +

        Observations

        1. Difficulté21/09 · M. Belkacemi

          Participe mais se décourage vite.

        2. Progrès14/09 · Mme Roche

          Très bon travail en binôme sur le câblage.

        3. CPE27/09 · Mme Diallo

          Entretien prévu le 02/10 avec la famille.

        +
        +

        Historique

        1. 29/09LabLab VLAN · étape 2 · 3 indices utilisés
        2. 28/09ExamenE6 blanc SISR · 9/20 · 1 sortie (6 s)
        3. 26/09QCMLecture CM VLAN · 7/20 · cours lu en 4 min
        4. 24/09DMDM 2 · Adressage · rendu · 11/20
        5. 22/09Quiz flashCommandes Linux · 3/5
        6. 21/09Observation« Participe mais se décourage vite. » · M. Belkacemi
        +
        + + + + + + diff --git a/portail/resources/views/fonctionnalites.blade.php b/portail/resources/views/fonctionnalites.blade.php new file mode 100644 index 0000000..e5553c3 --- /dev/null +++ b/portail/resources/views/fonctionnalites.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.vitrine') + +@section('titre', 'Fonctionnalités') + +@section('contenu') +

        Fonctionnalités

        Tout ce qu’il faut pour une classe, en ligne.

        Cours, TP sur machine Linux, éditeur de code, évaluations et suivi des élèves, dans un seul site. Choisissez un outil pour voir à quoi il sert, puis essayez sa démo.

        Créer une classe gratuite

        Sans carte bancaire

        + +
        +
        +

        Pourquoi un tel outil ?

        +

        Suivre chaque élève, même dans une classe de 35.

        +
        +

        Avec des classes entières et une charge de travail déjà lourde, suivre chaque élève individuellement devient presque impossible. Un élève en difficulté décroche vite, souvent sans bruit, et le prof, pris par ses nombreuses classes, s’en rend compte trop tard.

        +

        Syllabus surveille tout, sans être intrusif. Le QCM de début d’heure, celui qui suit la lecture du cours, les TP, les devoirs et exercices maison : chaque trace remonte au prof. Il voit rapidement qui accompagner plus régulièrement, qui faire passer au premier rang, à qui demander de réexpliquer une notion.

        +

        C’est une supervision bienveillante et naturelle, qui ne remplace pas le regard du prof mais l’aide à savoir où concentrer ses efforts. Tout remonte dans le cockpit prof, d’où il pilote et supervise examens, devoirs maison, quiz et labs.

        +

        Dans les labs, chaque étape a ses indices, qui se débloquent après un certain nombre de tentatives. Ils ne remplacent pas l’aide d’un prof (ni une recherche Google 😉), mais ils poussent l’élève à réfléchir un tout petit peu plus avant de vous appeler. Et souvent, ils vous évitent les questions triviales.

        +

        Et dans l’autre sens, l’élève en excellence peut s’épanouir. Il aborde des notions plus avancées, avec une difficulté plus forte, sans prendre d’avance sur le programme : il explore des notions connexes qui développent sa culture générale.

        +

        Tout cela dans un unique but : faciliter votre travail.

        +
        +
        +
        +
        + Alerte douce +

        Un élève décroche

        +
        • 3 QCM de début d’heure sous la moyenne
        • Cours lu en 2 minutes au lieu de 15
        • Devoir maison non rendu
        +

        → Premier rang, notion à réexpliquer, suivi plus régulier.

        +
        +
        + Excellence +

        Un élève s’ennuie

        +
        • Tous les TP réussis du premier coup
        • 20/20 au QCM de lecture
        • Finit avant tout le monde
        +

        → Défis plus durs sur des notions connexes, sans prendre d’avance sur le programme.

        +
        + +
        + Labs +

        Un indice se débloque

        +
        1. Tentative 1 · échec
        2. Tentative 3 · 💡 Indice 1 : regarde la commande ip addr
        3. Tentative 5 · 💡 Indice 2 : le masque /26 donne 62 hôtes
        +

        → L’élève cherche d’abord. Vous gardez votre temps pour les vraies questions.

        +
        +
        +
        +

        Piloter

        Suivre la classe, l’établissement et le territoire.

        −40 dB

        Vous devenez le roi du silence grâce à la fonction « Lever la main ».

        Vos élèves n’auront plus que le droit de tousser ou de respirer !

        Tous les cours se téléchargent en PDF.

        CM complet, TD prêt à imprimer avec ses cases à remplir, correction à part, QCM papier.

        +@endsection diff --git a/portail/resources/views/generateur.blade.php b/portail/resources/views/generateur.blade.php new file mode 100644 index 0000000..f30e240 --- /dev/null +++ b/portail/resources/views/generateur.blade.php @@ -0,0 +1,105 @@ +@extends('layouts.vitrine') + +@section('titre', 'Générateur de TD') + +@section('contenu') +
        +
        +

        Générateur de TD

        +

        Pour les profs

        +

        Des exercices à l’infini, corrigés tout de suite.

        +

        Chaque clic tire un nouvel exercice au hasard. La correction montre le calcul, étape par étape. En mode examen : 10 questions, chronométrées, notées sur 20.

        +
        +
        +
        + +
        + +
        +

        +

        +
        +
        + +
        + +
        +
        + +@endsection diff --git a/portail/resources/views/ide.blade.php b/portail/resources/views/ide.blade.php new file mode 100644 index 0000000..225d689 --- /dev/null +++ b/portail/resources/views/ide.blade.php @@ -0,0 +1,262 @@ + + + + + + IDE en ligne · Syllabus + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + + + + + + + + + + + + + + + + + +
        + +
        + +
        +
        +
        +
        +

        IDE en ligne

        +

        Pour les élèves

        +

        Coder, exécuter, vérifier. Dans le navigateur.

        +

        Python, SQL, HTML/CSS/JavaScript et Linux. Le code s’exécute vraiment, les tests disent tout de suite si c’est juste. Ton code est gardé sur cet appareil.

        +
        +
        +
        +
        + +
        +
        + + main.py + + + + + +
        +
        +
        +
        + + + +
        +
        Clique sur ▶ Exécuter pour lancer ton code.
        + + +
        +
        + +
        + +
        + + + + + + diff --git a/portail/resources/views/integration.blade.php b/portail/resources/views/integration.blade.php new file mode 100644 index 0000000..8b244f1 --- /dev/null +++ b/portail/resources/views/integration.blade.php @@ -0,0 +1,80 @@ + + + + + + Intégration et sécurité · Syllabus + + + + + + + + + + + + + +
        + +
        + +
        +

        Documentation technique

        Pour les profs

        +

        Intégration et sécurité

        Ce qu’il faut savoir pour raccorder Syllabus au système d’information de l’établissement.

        +
        +

        Authentification unique (SSO)

        Protocoles pris en charge : SAML 2.0 (Shibboleth, ADFS, fédération Renater), OpenID Connect (Entra ID, Google Workspace, Keycloak), CAS (ENT). EduConnect pour les élèves du secondaire via l’ENT de l’académie.

        ParamètreValeur
        Entity IDhttps://auth.syllabus.fr/saml/metadata
        ACS URLhttps://auth.syllabus.fr/saml/acs
        Attributs requismail, givenName, sn, eduPersonAffiliation
        Redirect URI OIDChttps://auth.syllabus.fr/oidc/callback
        +

        Création et suppression des comptes

        • Import CSV

          Classes et étudiants, depuis l’export de votre logiciel de scolarité

        • Juste à temps (JIT)

          Compte créé à la première connexion SSO, rattaché selon l’affiliation

        • SCIM 2.0

          Synchronisation automatique depuis Entra ID ou Keycloak

        • LDAP / Active Directory

          Synchronisation planifiée des groupes en lecture seule

        • Suppression

          Compte désactivé à la fin de licence, données effacées après 12 mois

        +
        +
        +

        API REST

        API REST en JSON, documentée au format OpenAPI 3.1. Authentification par jeton porteur, une clé par établissement avec des droits limités (lecture des notes, gestion des comptes). Limite : 600 requêtes par minute. Webhooks signés (HMAC-SHA256) pour les événements devoir rendu, examen terminé, alerte ouverte.

        curl -H "Authorization: Bearer $PAILLASSE_TOKEN" \
        +  https://api.syllabus.fr/v1/classes/bts-sio-1a/notes?format=csv
        +

        Prérequis réseau et postes

        À autoriserProtocoleUsage
        syllabus.fr, *.syllabus.frHTTPS 443site, cours, API
        vm.syllabus.frWSS 443 (WebSocket)terminal et IDE
        auth.syllabus.frHTTPS 443connexion SSO

        Aucun port autre que 443, aucun logiciel à installer. Le proxy doit laisser passer les connexions WebSocket (en-tête Upgrade) et ne pas couper les sessions inactives avant 10 minutes. Navigateurs : Firefox, Chrome, Edge et Safari des deux dernières versions majeures. Débit : environ 50 kbit/s par terminal, 300 kbit/s par IDE.

        +
        +
        +

        Isolation des environnements

        • ■Une VM KVM dédiée par étudiant et par séance, jamais partagée
        • ■Trafic entre VM d’étudiants bloqué, y compris dans une même classe
        • ■Sortie Internet filtrée : fermée par défaut, liste blanche de dépôts (Debian, PyPI, npm) activable par le prof
        • ■VM détruite en fin de séance ; seul le dossier de travail est conservé, chiffré
        • ■Quotas CPU, mémoire et disque par VM ; arrêt automatique après 30 min d’inactivité
        • ■Commandes et connexions journalisées, consultables par le prof et la DSI
        +

        Conformité et continuité

        SujetEngagement
        HébergementScaleway, datacenters de Paris, France
        RGPDAccord de sous-traitance art. 28, registre des traitements fourni
        ConservationTraces d’activité 12 mois, notes jusqu’à fin de scolarité + 1 an
        SauvegardesQuotidiennes, chiffrées, conservées 30 jours, second site en France
        Disponibilité99,5 % par mois en période scolaire, maintenance hors cours
        ChiffrementTLS 1.2+ en transit, AES-256 au repos
        DPOdpo@syllabus.fr
        Mentions légales →
        +
        +
        +
        + + + + + + diff --git a/portail/resources/views/labs/essai.blade.php b/portail/resources/views/labs/essai.blade.php new file mode 100644 index 0000000..2806986 --- /dev/null +++ b/portail/resources/views/labs/essai.blade.php @@ -0,0 +1,83 @@ +@extends('layouts.vitrine') + +@section('titre', 'Lab d’essai · Premiers pas dans le shell') + +@push('head') + + + + +@endpush + +@section('contenu') +
        +
        + ← Labs +

        Pour les élèves

        +

        Premiers pas dans le shell

        + Lab d’essai +
        +
        +
        +
        +

        Consignes

        +
          +
        1. 1Affiche le dossier courant avec pwd.
        2. +
        3. 2Liste son contenu avec ls -la.
        4. +
        5. 3Crée un dossier projets et entre dedans.
        6. +
        7. 4Crée un fichier notes.txt.
        8. +
        9. 5Écris ton prénom dedans avec echo.
        10. +
        +
        +
        +
        + + debian-lab · console 1 · xterm.js +
        +
        +
        + +
        + +@endsection diff --git a/portail/resources/views/labs/index.blade.php b/portail/resources/views/labs/index.blade.php new file mode 100644 index 0000000..77d1abe --- /dev/null +++ b/portail/resources/views/labs/index.blade.php @@ -0,0 +1,86 @@ +@extends('layouts.vitrine') + +@section('titre', 'Labs') + +@section('contenu') +
        + +
        +

        Catalogue

        +

        Pour les élèves

        +

        Des labs qui se corrigent tout seuls.

        +

        Chaque lab est une vraie VM Debian, ou plusieurs reliées en réseau. L’élève suit les consignes, la vérification valide chaque étape.

        +
        +
        +
        +
        +

        Filtrer

        + + + + +
        +
          +
        • + Linux + 01 +
          +

          Lycée NSI

          +

          Premiers pas dans le shell

          +

          Se déplacer, créer des dossiers, lire des fichiers.

          + Lancer le lab +
          +
        • +
        • + Linux + 02 +
          +

          BTS SIO · SISR et SLAM

          +

          Utilisateurs et droits

          +

          Créer des comptes, régler chmod et chown, tester les accès.

          + Lancer le lab +
          +
        • +
        • + Réseau + 03 +
          +

          BTS CIEL

          +

          Adressage IPv4

          +

          Configurer deux VM pour qu’elles se voient, puis diagnostiquer.

          + Lancer le lab +
          +
        • +
        • + Réseau + 04 +
          +

          BUT · Licence pro

          +

          Routage statique avec Kathará

          +

          Trois routeurs en conteneurs, une topologie à faire communiquer.

          + Lancer le lab +
          +
        • +
        • + Services + 05 +
          +

          BTS SIO · SISR et SLAM

          +

          Serveur web Apache

          +

          Installer, publier une page, lire les journaux.

          + Lancer le lab +
          +
        • +
        • + Services + 06 +
          +

          Master · École d’ingé

          +

          Annuaire Samba AD

          +

          Monter un contrôleur de domaine et joindre un client.

          + Lancer le lab +
          +
        • +
        +
        +@endsection diff --git a/portail/resources/views/labs/web.blade.php b/portail/resources/views/labs/web.blade.php new file mode 100644 index 0000000..594405a --- /dev/null +++ b/portail/resources/views/labs/web.blade.php @@ -0,0 +1,137 @@ + + + + + + Lab web · Syllabus + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + + + + + +
        + +
        + +
        +
        +
        + ← Labs +

        Pour les élèves

        +

        Lab web · Mon premier serveur Apache

        + SRV-1 · BTS SIO, BUT R&T, NSI +
        +
        +
        + +
        +
        web-01 · 192.168.10.10 · xterm.js
        +
        +
        +
        +
        + +

        http://192.168.10.10/

        +
        + +
        +
        + +
        + + + + + + diff --git a/portail/resources/views/layouts/vitrine.blade.php b/portail/resources/views/layouts/vitrine.blade.php new file mode 100644 index 0000000..d33c972 --- /dev/null +++ b/portail/resources/views/layouts/vitrine.blade.php @@ -0,0 +1,68 @@ + + + + + + @yield('titre') · Syllabus + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + @stack('head') + + + + +
        + +
        + +
        + @yield('contenu') +
        + + + + + diff --git a/portail/resources/views/legal.blade.php b/portail/resources/views/legal.blade.php new file mode 100644 index 0000000..2f2388f --- /dev/null +++ b/portail/resources/views/legal.blade.php @@ -0,0 +1,70 @@ +@extends('layouts.vitrine') + +@section('titre', 'Informations légales') + +@section('contenu') +
        +
        +

        Informations légales

        +

        Le cadre, en clair.

        +

        Dernière mise à jour : 29 septembre 2026

        +
        +
        +
        + +
        + +
        +

        Mentions légales

        +
        +

        Éditeur : Syllabus, édité par Adam Belkacemi, [à compléter] (forme juridique, capital, SIRET, adresse du siège).

        +

        Directeur de la publication : Adam Belkacemi. Contact : adam@belkacemi.me.

        +

        Hébergeur : Scaleway SAS, 8 rue de la Ville-l’Évêque, 75008 Paris. Les serveurs sont situés en France, à Paris.

        +
        + +
        +

        Conditions générales d’utilisation

        +
        +

        1. Objet

        Syllabus met à disposition des cours, des labs Linux et réseau sur machines virtuelles, des QCM et des outils de suivi pour l’enseignement.

        +

        2. Comptes

        Les enseignants créent un compte avec leur e-mail professionnel. Les élèves se connectent avec l’identifiant et le code remis par leur enseignant. Chacun est responsable de la confidentialité de ses accès.

        +

        3. Accès gratuit

        L’accès enseignant est gratuit et sans carte bancaire. Syllabus peut faire évoluer les fonctionnalités et les limites d’usage, en prévenant les utilisateurs.

        +

        4. Usage acceptable

        Les VM servent uniquement à l’apprentissage. Sont interdits : le minage, les attaques vers des tiers, le contournement des quotas ou du mode examen. Le trafic sortant des VM est bloqué par défaut et l’activité est journalisée.

        +

        5. Mode examen

        Pendant une épreuve, les changements d’onglet, sorties de fenêtre et sorties de souris sont enregistrés et transmis à l’enseignant. L’élève en est informé avant de commencer.

        +

        6. Contenus

        Les conditions de réutilisation des contenus pédagogiques seront précisées ultérieurement. Les travaux des élèves leur appartiennent.

        +

        7. Disponibilité et responsabilité

        Syllabus fait ses meilleurs efforts pour assurer la disponibilité du service, sans garantie d’absence d’interruption. [à compléter]

        +

        8. Droit applicable

        Les présentes conditions sont soumises au droit français.

        +
        + +
        +

        Confidentialité et RGPD

        +
        +

        Quand un établissement utilise Syllabus avec ses élèves, l’établissement est responsable du traitement et Syllabus agit comme sous-traitant. Pour les comptes enseignants créés directement, Syllabus est responsable du traitement.

        +

        Données collectées

        +
        • Élèves : nom, prénom, groupe. Aucun e-mail pour les mineurs.
        • Enseignants : nom, e-mail professionnel, établissement.
        • Activité pédagogique : étapes de TP, réponses aux QCM, temps passé, remarques des enseignants.
        • Mode examen : événements de sortie de copie, uniquement pendant l’épreuve.
        • Journaux techniques de sécurité.
        +

        Finalités et base légale

        Fournir le service d’enseignement demandé par l’établissement (mission d’intérêt public de l’établissement) ou par l’enseignant (exécution du contrat).

        +

        Conservation

        Les données d’une classe sont supprimées automatiquement 90 jours après son archivage de fin d’année.

        +

        Hébergement

        Toutes les données sont hébergées en France, chez Scaleway. Elles ne quittent pas l’Union européenne. Aucune donnée n’est vendue ni utilisée à des fins publicitaires.

        +

        Vos droits

        Accès, rectification, effacement, limitation, opposition et portabilité : écrivez à adam@belkacemi.me ou à votre établissement. Vous pouvez aussi adresser une réclamation à la CNIL (cnil.fr).

        +

        Délégué à la protection des données : [à compléter]

        +
        + +
        +

        Cookies

        +
        +

        Syllabus n’utilise que des cookies strictement nécessaires : le cookie de session qui vous garde connecté, et le jeton de sécurité des formulaires. Aucun cookie publicitaire, aucun traceur tiers. Ils ne demandent donc pas de consentement.

        +
        + +
        +

        Accord de sous-traitance (article 28 du RGPD)

        +
        +

        Pour chaque établissement, un accord de sous-traitance précise :

        +
        • l’objet, la durée et la nature des traitements ;
        • les mesures de sécurité : chiffrement en transit, isolement des classes, sauvegardes chiffrées ;
        • la confidentialité des personnes autorisées ;
        • la liste des sous-traitants ultérieurs (hébergement : Scaleway SAS, France) ;
        • l’assistance pour répondre aux demandes de droits ;
        • la notification des violations de données ;
        • la suppression ou la restitution des données en fin de contrat.
        +

        Demander l’accord pour votre établissement

        +
        +
        +
        +@endsection diff --git a/portail/resources/views/maison.blade.php b/portail/resources/views/maison.blade.php new file mode 100644 index 0000000..c6fa08c --- /dev/null +++ b/portail/resources/views/maison.blade.php @@ -0,0 +1,29 @@ +@extends('layouts.vitrine') + +@section('titre', 'Ressources à la maison') + +@section('contenu') +
        +
        +

        Pour les élèves

        +

        Pour les élèves

        +

        Envie d’aller plus loin à la maison ?

        +

        Sur Syllabus, tu retrouves tes cours, tes QCM, l’IDE et le générateur de TD depuis chez toi, sur ton PC ou ton téléphone. Et si tu en veux encore, voici des ressources gratuites, sérieuses et en grande partie en français.

        + +
        +
        +
        +

        Ressources externes gratuites

        +

        Elles ne font pas partie de Syllabus. Certaines demandent de créer un compte : demande l’accord de tes parents si tu es mineur.

        +
          +
          + + +@endsection diff --git a/portail/resources/views/mes-classes/index.blade.php b/portail/resources/views/mes-classes/index.blade.php new file mode 100644 index 0000000..1f4af16 --- /dev/null +++ b/portail/resources/views/mes-classes/index.blade.php @@ -0,0 +1,35 @@ +@extends('layouts.vitrine') + +@section('titre', 'Mes classes') + +@section('contenu') +
          +

          Mes classes

          + + @if (session('session_precedente_fermee')) +

          + Une session précédente sur ce compte a été fermée : une seule session active à la fois. +

          + @endif + + @if ($classes->isEmpty()) +

          Aucune classe pour l'instant.

          + @else + + @endif +
          +@endsection diff --git a/portail/resources/views/mes-classes/show.blade.php b/portail/resources/views/mes-classes/show.blade.php new file mode 100644 index 0000000..b890b28 --- /dev/null +++ b/portail/resources/views/mes-classes/show.blade.php @@ -0,0 +1,113 @@ +@extends('layouts.vitrine') + +@section('titre', $classe->nom) + +@section('contenu') +
          +
          +
          +

          {{ $classe->nom }}

          + @if ($classe->formation) +

          {{ $classe->formation }}

          + @endif +
          +

          Code de classe : {{ $classe->code_classe }}

          +
          + + @if (session('classe_creee')) +

          Classe créée. Ajoute maintenant tes élèves ci-dessous.

          + @endif + + @if (session('fiches')) +
          +
          +

          + Fiches à imprimer + @if (session('nombre_importes')) + ({{ session('nombre_importes') }} élève(s) importé(s)) + @endif +

          + +
          +

          Le code secret ne sera plus jamais affiché après cette page : imprime ou note-le maintenant.

          +
          + @foreach (session('fiches') as $fiche) +
          +
          {!! \App\Support\QrCode::svg(route('connexion.qr', $fiche['jeton_qr'])) !!}
          +
          +

          {{ $fiche['prenom'] }}@if($fiche['nom']) {{ $fiche['nom'] }}@endif

          +

          Identifiant
          {{ $fiche['identifiant'] }}

          +

          Code secret
          {{ $fiche['code_secret'] }}

          +
          +
          + @endforeach +
          +
          + @endif + +
          +
          +

          Ajouter un élève

          + @error('prenom')

          {{ $message }}

          @enderror +
          + @csrf + + + +
          +
          +
          +

          Importer une liste (CSV)

          +

          Une ligne par élève : prenom,nom (le nom peut être vide).

          + @error('fichier')

          {{ $message }}

          @enderror +
          + @csrf + + +
          +
          +
          + +
          +

          Élèves ({{ $classe->eleves->count() }})

          + @if ($classe->eleves->isEmpty()) +

          Aucun élève pour l'instant.

          + @else +
          + + + + @foreach ($classe->eleves as $eleve) + + + + + + + @endforeach + +
          ÉlèveIdentifiantDernière connexion
          {{ $eleve->nomAffiche() }}{{ $eleve->identifiant }}{{ $eleve->derniere_connexion_a?->diffForHumans() ?? 'jamais' }} +
          + @csrf + +
          +
          +
          + @endif +
          + +
          +

          Lancer une séance

          +

          Les élèves qui rejoignent patientent dans la salle d'attente jusqu'à ce que tu les admettes.

          +
          + @csrf + + +
          +
          +
          +@endsection diff --git a/portail/resources/views/messagerie.blade.php b/portail/resources/views/messagerie.blade.php new file mode 100644 index 0000000..00489d7 --- /dev/null +++ b/portail/resources/views/messagerie.blade.php @@ -0,0 +1,67 @@ + + + + + + Messagerie · Syllabus + + + + + + + + + + + + + +
          + +
          + +
          +

          Messagerie

          Pour les profs

          +

          Yanis B.

          Élève · BTS SIO 1 A · lab VLAN en cours

          Fiche élève
          Échanges visibles par l’élève et son équipe pédagogique. Pas de messages entre élèves. Horaires de réponse conseillés : 8 h – 19 h.
          1. Bonjour Monsieur, je ne comprends pas l’étape 4 du lab VLAN, la commande ne marche pas.

            10:12

          2. J’ai déjà utilisé les 2 indices 😅

            10:14

          3. Bonjour Yanis. Regarde bien le nom de l’interface avec ip a : ce n’est pas eth0 sur cette VM.

            10:16

          +
          +
          + + + + + + diff --git a/portail/resources/views/paiement-ok.blade.php b/portail/resources/views/paiement-ok.blade.php new file mode 100644 index 0000000..8423032 --- /dev/null +++ b/portail/resources/views/paiement-ok.blade.php @@ -0,0 +1,72 @@ + + + + + + Paiement confirmé · Syllabus + + + + + + + + + + + + + +
          + +
          + +
          +
          + +

          Pour les profs

          +

          Merci ! C’est activé.

          +

          Votre offre Enseignant est active. Le reçu et la facture sont envoyés par e-mail.

          +
          Offre
          Enseignant · mensuel
          Montant
          34,80 € TTC / mois
          Prochain paiement
          29/10/2026
          + +

          Un coup de pouce en plus ? Parlez de Syllabus à votre établissement, ou écrivez vos remarques à adam@belkacemi.me.

          +
          + + + + + + diff --git a/portail/resources/views/paiement.blade.php b/portail/resources/views/paiement.blade.php new file mode 100644 index 0000000..25bc51b --- /dev/null +++ b/portail/resources/views/paiement.blade.php @@ -0,0 +1,103 @@ + + + + + + Paiement · Syllabus + + + + + + + + + + + + + +
          + +
          + +
          +
          ← Retour aux tarifs

          Pour les profs

          +

          Passer à l’offre payante

          Les cours restent gratuits pour toujours. Vous payez les VM, le suivi et les outils prof.

          +
          +
          +

          1 · Votre offre

          +
          + +
          +

          2 · Facturation

          +
          +
          +

          3 · Moyen de paiement

          +
          +

          Le numéro de carte est saisi dans un champ sécurisé fourni par Stripe. Syllabus ne le voit ni ne le stocke.

          + + +
          +
          + +
          + +
          + + + + + + diff --git a/portail/resources/views/quiz/eleve.blade.php b/portail/resources/views/quiz/eleve.blade.php new file mode 100644 index 0000000..8e994d9 --- /dev/null +++ b/portail/resources/views/quiz/eleve.blade.php @@ -0,0 +1,63 @@ + + + + + + Quiz flash · Syllabus + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + +
          +

          Vue élève · téléphone ou PC

          +
          +

          Pour les élèves

          +

          Quiz flash

          +
          + + + + +
          +
          + + + +
          + + + diff --git a/portail/resources/views/quiz/projection.blade.php b/portail/resources/views/quiz/projection.blade.php new file mode 100644 index 0000000..6b7f489 --- /dev/null +++ b/portail/resources/views/quiz/projection.blade.php @@ -0,0 +1,162 @@ + + + + + + Quiz flash · projection · Syllabus + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + +
          +
          + QUIZ FLASH +

          BTS SIO 1 · Groupe A · ls, chmod, grep, ip, ping

          +

          0 / 24 connectés

          + +
          + +
          +
          +

          Sur ton téléphone ou ton PC, va sur

          +

          syllabus.fr/quiz

          +

          et entre le code

          +

          K7Q4

          +

          Connecte-toi avec ton identifiant Syllabus.

          +
          +
          +

          Présences

          0 / 24

          +
            +
          • Léa M.
          • +
          • Yanis B.
          • +
          • Nora Kaci
          • +
          • Tom R.
          • +
          • Inès D.
          • +
          • Sami L.
          • +
          • Chloé P.
          • +
          • Adam F.
          • +
          • Maël G.
          • +
          • Jade T.
          • +
          • Hugo M.
          • +
          • Lina G.
          • +
          • Rayan F.
          • +
          • Emma B.
          • +
          • Noah H.
          • +
          • Sarah R.
          • +
          • Ilyes M.
          • +
          • Manon L.
          • +
          • Lucas M.
          • +
          • Zoé D.
          • +
          • Enzo L.
          • +
          • Camille B.
          • +
          • Nathan F.
          • +
          • Anaïs L.
          • +
          +
          +

          En attente des connexions…

          + +
          +
          +
          + + + + +
          + + + diff --git a/portail/resources/views/ressource.blade.php b/portail/resources/views/ressource.blade.php new file mode 100644 index 0000000..f9e5df1 --- /dev/null +++ b/portail/resources/views/ressource.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.vitrine') + +@section('titre', 'Ressources') + +@section('contenu') +
          +
          +
          +

          Outil

          +

          Outil

          +

          +
          + Voir la démo +

          Interactive, sans inscription

          +
          +
          +
          +
          +
          +
          +

          +
          +
          +
          +
            +
            +
            +
            +

            En trois étapes

            +
              +
              +
              +
              +
              +

              +

              + Voir la démo +
              +
              + + + +
              +@endsection diff --git a/portail/resources/views/seances/attente-eleve.blade.php b/portail/resources/views/seances/attente-eleve.blade.php new file mode 100644 index 0000000..77441c5 --- /dev/null +++ b/portail/resources/views/seances/attente-eleve.blade.php @@ -0,0 +1,19 @@ +@extends('layouts.vitrine') + +@section('titre', "Salle d'attente") + +@section('contenu') + @if ($labSession->etat === \App\Models\LabSession::EN_ATTENTE) + + @endif +
              +

              {{ $seance->lab->titre }}

              + @if ($labSession->etat === \App\Models\LabSession::EN_ATTENTE) +

              En attente que le prof t'admette…

              +

              Cette page se rafraîchit toute seule.

              + @else +

              Tu es admis·e !

              +

              Le lancement de ta VM arrive dans une prochaine étape du déploiement.

              + @endif +
              +@endsection diff --git a/portail/resources/views/seances/attente.blade.php b/portail/resources/views/seances/attente.blade.php new file mode 100644 index 0000000..bfb6e8e --- /dev/null +++ b/portail/resources/views/seances/attente.blade.php @@ -0,0 +1,32 @@ +@extends('layouts.vitrine') + +@section('titre', "Salle d'attente") + +@section('contenu') + +
              +

              {{ $seance->classe->nom }} · {{ $seance->lab->titre }}

              +

              Salle d'attente

              +

              La page se rafraîchit toutes les 10 secondes.

              + + @if ($seance->labSessions->isEmpty()) +

              Personne n'a encore rejoint la séance.

              + @else +
                + @foreach ($seance->labSessions as $labSession) +
              • + {{ $labSession->eleve->nomAffiche() }} + @if ($labSession->etat === \App\Models\LabSession::EN_ATTENTE) +
                + @csrf + +
                + @else + Admis + @endif +
              • + @endforeach +
              + @endif +
              +@endsection diff --git a/portail/resources/views/sujet-creer.blade.php b/portail/resources/views/sujet-creer.blade.php new file mode 100644 index 0000000..842bac1 --- /dev/null +++ b/portail/resources/views/sujet-creer.blade.php @@ -0,0 +1,66 @@ + + + + + + Créer un sujet d’examen · Syllabus + + + + + + + + + + + + + +
              + +
              + +
              +

              Sujet · étape par étape

              Pour les profs

              +

              Créer un sujet d’examen

              Composez un sujet en 5 étapes, à partir de la banque ou de vos questions.

              1 · Cadre de l’épreuve

              +
              + + + + + + diff --git a/portail/resources/views/tarifs.blade.php b/portail/resources/views/tarifs.blade.php new file mode 100644 index 0000000..81cbe5b --- /dev/null +++ b/portail/resources/views/tarifs.blade.php @@ -0,0 +1,20 @@ +@extends('layouts.vitrine') + +@section('titre', 'Tarifs') + +@section('contenu') +

              Tarifs HT

              Les cours sont gratuits. Les VM coûtent moins qu’un manuel.

              Une licence Syllabus pour un élève coûte 24 € par an : QCM, TP sur VM, suivi et mode examen compris.

              • Découverte

                Élèves, curieux, enseignants

                0 €

                pour toujours

                • ✓Les 99 cours : CM, TD, QCM
                • ✓Corrections expliquées
                • ✓1 lab d’essai de 30 min par jour

                Lire les cours →

              • Enseignant

                Une classe, sans passer par l’intendance

                29 € / mois

                ou 290 € / an · jusqu’à 35 élèves

                • ✓Les 59 TP sur VM
                • ✓Correction automatique
                • ✓Suivi des élèves et mode examen
                • ✓Cockpit prof, messagerie, correction assistée
                • ✓15 h de VM par élève et par mois

                Souscrire →

              • Le plus choisi

                Établissement

                Lycée, BTS, IUT, université, école

                24 € / élève / an

                à partir de 60 élèves · 1 440 € / an

                • ✓Tout Enseignant, classes illimitées
                • ✓Espace vie scolaire : CPE, direction, intendance
                • ✓Export Pronote / ENT
                • ✓Espace administrateur et bibliothèque partagée
                • ✓Séances réservées, VM prêtes 10 min avant
                • ✓Paiement par mandat administratif

                Demander un devis

              • Région et OF

                Académies, régions, organismes de formation

                Sur devis

                à partir de 500 élèves ou stagiaires

                • ✓Tout Établissement
                • ✓Capacité réservée
                • ✓Suivi par centre ou par site
                • ✓Tableau de bord académie
                • ✓Accompagnement au déploiement

                Nous contacter →

              API et MCP pour développeurs : 19 € HT par mois avec 100 h de VM incluses, puis 0,09 € HT l’heure. Les classes restent prioritaires sur la capacité.

              +
              +
              + +

              Vos questions, nos réponses.

              +

              Intendance, données, pédagogie, technique. Une question manque ? adam@belkacemi.me

              +
              +

              Achat et intendance

              • Bon de commande et Chorus Pro ?

                Oui. Nous envoyons un devis, vous émettez un bon de commande, la facture arrive sur Chorus Pro. Paiement par mandat administratif à 30 jours.

              • Faut-il une carte bancaire pour essayer ?

                Non. Un enseignant crée sa classe gratuitement, sans CB. En échange, on vous demande juste un coup de pouce : vos remarques, ou votre aide pour faire souscrire votre établissement.

              • Une licence couvre quelle période ?

                Une licence suit l’année scolaire, de septembre à août. Les comptes archivés sont supprimés 90 jours après la fin d’année.

              • Les élèves qui redoublent ?

                Ils repartent sur la licence de l’année suivante. Leur historique (notes, observations, progression) peut être conservé si l’établissement le demande.

              • Peut-on payer par région ou par académie ?

                Oui, un devis groupé est possible pour plusieurs établissements. Écrivez-nous pour en parler.

              +

              Élèves et données

              • Où sont hébergées les données ?

                En France, chez Scaleway, sur des serveurs situés à Paris. Rien ne sort de l’Union européenne.

              • Et le RGPD ?

                Nous collectons le minimum : nom, prénom, classe, résultats. Pas de publicité, pas de revente, pas de traceurs tiers. Un contrat de sous-traitance est fourni à l’établissement.

              • Les élèves mineurs peuvent-ils s’inscrire seuls ?

                Non. C’est l’enseignant qui crée la classe et distribue les identifiants. L’élève n’a pas besoin d’adresse e-mail.

              • Un élève peut-il travailler chez lui ?

                Oui, depuis n’importe quel navigateur, sur PC ou téléphone. Cours, QCM, IDE et devoirs maison restent accessibles toute l’année.

              +

              Pédagogie

              • Les cours suivent-ils les programmes officiels ?

                Oui. Ils sont construits à partir des référentiels officiels (BO, arrêtés, fiches RNCP), du SNT au master. Chaque CM, TD et TP indique les compétences visées.

              • Puis-je modifier un cours ou un sujet ?

                Oui. Tout se duplique et se modifie : CM, questions, TD, sujets d’examen. Vos versions restent dans votre bibliothèque.

              • Comment marche le mode examen ?

                L’élève passe en plein écran. Si sa souris sort de la fenêtre ou s’il change d’onglet, vous le voyez en direct, avec l’heure et la durée.

              • Les TD s’impriment-ils ?

                Oui. Chaque TD se télécharge en PDF avec ses étapes et ses cases à remplir. La correction s’imprime à part, pour vous.

              • Et la fonction « Lever la main » ?

                L’élève lève la main depuis son écran, vous voyez la file d’attente. Fini les « Madame ! Monsieur ! » en boucle.

              +

              Technique

              • Faut-il installer quelque chose ?

                Non. Tout se passe dans le navigateur : terminal Debian, IDE, navigateur relié à la VM. Rien à installer sur les postes du lycée.

              • Et si un élève dépasse 15 h de VM ?

                Le compteur est partagé par la classe : un élève peut consommer plus si d’autres consomment moins. Au-delà, les TP restent ouverts en lecture.

              • Le réseau du lycée bloque souvent tout…

                Syllabus passe par une connexion web chiffrée classique, comme n’importe quel site. En cas de souci, on vous aide à le régler avec votre administrateur réseau.

              • Une VM peut-elle casser le réseau du lycée ?

                Non. Chaque VM est isolée chez nous. L’élève peut tout casser dedans, il la réinitialise en un clic.

              +
              +
              +
              +@endsection diff --git a/portail/resources/views/td/imprimable.blade.php b/portail/resources/views/td/imprimable.blade.php new file mode 100644 index 0000000..356e0e8 --- /dev/null +++ b/portail/resources/views/td/imprimable.blade.php @@ -0,0 +1,165 @@ + + + + + + TD à imprimer · Syllabus + + + + + + + + + + + + + +
              + +
              + +
              +
              +
              +
              +

              TD prêt à imprimer

              +

              Découper un réseau IPv4.

              +

              Étapes numérotées, cases à remplir, auto-évaluation. La correction s’imprime à part, pour le prof.

              +
              +
              + + +
              +
              +
              +
              +
              +
              +

              BTS SIO · BTS CIEL · BUT R&T 1re année

              TD · Découper un réseau IPv4

              Durée : 1 h · Calculatrice interdite

              +

              Nom :

              Classe :

              Date :

              +
              +

              Rappel : un masque /n laisse 32 − n bits pour les hôtes. Nombre d’hôtes utilisables = 2(32 − n) − 2.

              +
              + +
              +
              1

              Lire l’adresse

              L’établissement dispose du réseau 192.168.10.0/24. Complète les cases.

              +
              +
              + +
              +
              2

              Choisir le masque

              Il faut 4 sous-réseaux de même taille. Combien de bits emprunter ? Coche puis justifie.

              +
              1 bit2 bits3 bits4 bits

              Justification :

              +
              + +
              +
              3

              Calculer la taille d’un sous-réseau

              Avec ton masque, complète le tableau.

              +
              Adresse réseau
              Masque (décimal)
              Nombre d’adresses par sous-réseau
              Hôtes utilisables
              +
              + +
              +
              4

              Découper le réseau

              Remplis le plan d’adressage des 4 sous-réseaux.

              +
              Sous-réseauAdresse réseauPlage d’hôtesBroadcast
              SR1 · Administration
              SR2 · Élèves
              SR3 · Serveurs
              SR4 · Wi-Fi invités
              +
              + +
              +
              5

              Vérifier sur le lab

              Connecte-toi au lab « Adressage IPv4 » et configure le poste du SR2. Recopie la commande utilisée.

              +
              +
              + +
              +
              6

              Auto-évaluation

              Coche ce que tu sais faire.

              +
              • Je sais lire une notation CIDR
              • Je sais calculer un masque
              • Je sais trouver une adresse de broadcast
              +
              +
              +
              +

              Correction

              Version prof
              +
              + +
              +
              1

              Lire l’adresse

              L’établissement dispose du réseau 192.168.10.0/24. Complète les cases.

              +
              +
              + +
              +
              2

              Choisir le masque

              Il faut 4 sous-réseaux de même taille. Combien de bits emprunter ? Coche puis justifie.

              +
              1 bit✕2 bits3 bits4 bits

              Justification :

              2² = 4 sous-réseaux. Le masque passe de /24 à /26.

              +
              + +
              +
              3

              Calculer la taille d’un sous-réseau

              Avec ton masque, complète le tableau.

              +
              Adresse réseau192.168.10.0
              Masque (décimal)255.255.255.192
              Nombre d’adresses par sous-réseau64
              Hôtes utilisables62
              +
              + +
              +
              4

              Découper le réseau

              Remplis le plan d’adressage des 4 sous-réseaux.

              +
              Sous-réseauAdresse réseauPlage d’hôtesBroadcast
              SR1 · Administration192.168.10.0192.168.10.1 – .62192.168.10.63
              SR2 · Élèves192.168.10.64192.168.10.65 – .126192.168.10.127
              SR3 · Serveurs192.168.10.128192.168.10.129 – .190192.168.10.191
              SR4 · Wi-Fi invités192.168.10.192192.168.10.193 – .254192.168.10.255
              +
              + +
              +
              5

              Vérifier sur le lab

              Connecte-toi au lab « Adressage IPv4 » et configure le poste du SR2. Recopie la commande utilisée.

              +

              sudo ip addr add 192.168.10.65/26 dev eth0

              +
              + +
              +
              6

              Auto-évaluation

              Coche ce que tu sais faire.

              +
              • Je sais lire une notation CIDR
              • Je sais calculer un masque
              • Je sais trouver une adresse de broadcast
              +
              +
              +
              + +
              +
              +
              + +
              +

              Les élèves signalent un blocage avec la fonction « Lever la main », sans interrompre la classe.« Lever la main ».

              +

              Chaque demande arrive dans votre tableau de bord, dans l’ordre. !

              +
              +
              +
              +
              +
              + + + + + + + diff --git a/portail/resources/views/vie-scolaire.blade.php b/portail/resources/views/vie-scolaire.blade.php new file mode 100644 index 0000000..ad39511 --- /dev/null +++ b/portail/resources/views/vie-scolaire.blade.php @@ -0,0 +1,139 @@ + + + + + + Vie scolaire et intendance · Syllabus + + + + + + + + + + + + + +
              + +
              + +
              +
              +
              + +
              +
              +

              Vie scolaire et intendance

              Pour les profs

              +

              Tableau de bord CPE

              6 classes · 118 élèves · semaine du 28 septembre

              +
              + + +
              +
              +
                +
              • Alertes ouvertes

                5

                2 priorité haute

              • Présence moyenne

                92 %

                sur les séances en ligne

              • DM rendus

                75 %

                toutes classes

              • Examens surveillés

                4

                cette semaine · 7 sorties

              • Observations

                38

                prêtes pour les conseils

              • +
              • Licences

                118/120

                renouvellement 31/08/2027

              • Coût annuel

                2 832 €

                HT · 24 € / élève

              • Heures de VM

                612 h

                sur 1 770 h incluses ce mois

              • Factures

                3

                1 en attente Chorus Pro

              • Classes actives

                6

                6 enseignants

              • +
              +
              +

              Alertes élèves, toutes classes

              remontées par les profs et par Syllabus

              +
              + +
              PrioritéÉlèveSignalProf référentAction
              Haute

              Lucas M.

              BTS SIO 1 A

              Décrochage dans 3 matières

              QCM < 8 en réseaux, maths et CEJM

              M. Belkacemi
              Haute

              Léa M.

              BTS SIO 1 A

              Baisse continue sur 4 semaines

              12 → 7,5 de moyenne QCM

              M. Belkacemi
              Moyenne

              Enzo B.

              Terminale NSI

              5 DM non rendus

              Depuis la rentrée

              Mme Roche
              Moyenne

              Camille D.

              BTS SIO 1 A

              3 sorties pendant un examen

              E6 blanc · 12 s au total

              M. Belkacemi
              Moyenne

              Mathis C.

              BTS CIEL 2

              Absences répétées

              4 séances de TP manquées

              M. Arnaud
              Positif

              Inès K.

              BTS SIO 1 A

              Excellence dans toutes les matières

              Proposée pour le tutorat

              M. Belkacemi
              +
              +
              +

              Toutes les classes

              +
              + +
              ClasseEnseignantÉlèvesMoy. QCMAlertesDM rendusPrésence
              BTS SIO 1 · AM. Belkacemi1613,4381 %94 %
              BTS SIO 1 · BM. Belkacemi1712,8276 %91 %
              BTS SIO 2 · SISRMme Roche1413,9188 %96 %
              BTS CIEL 2M. Arnaud1511,6464 %85 %
              Terminale NSIMme Roche2412,1370 %93 %
              Seconde SNT 4M. Faure3211,8273 %95 %
              +
              +
              +
              +

              Conseils de classe · 1er trimestre

              +
              • BTS SIO 1 · A

                lun. 07/12 · observations saisies

                Prêt
                100 %
              • BTS CIEL 2

                mar. 08/12 · observations saisies

                2 profs à relancer
                70 %
              • Terminale NSI

                jeu. 10/12 · observations saisies

                En cours
                45 %
              • Seconde SNT 4

                ven. 11/12 · observations saisies

                En cours
                20 %
              +
              +
              +

              Journal des examens

              +
              1. lun. 28/09 · 09:14 · BTS SIO 1 A · E6 blanc

                Hugo P. · changement d’onglet · 3 s

              2. lun. 28/09 · 09:21 · BTS SIO 1 A · E6 blanc

                Lucas M. · souris hors fenêtre · 6 s

              3. lun. 28/09 · 09:40 · BTS SIO 1 A · E6 blanc

                Camille D. · fenêtre réduite · 2 s

              4. mar. 29/09 · 14:00 · Terminale NSI · Contrôle SQL

                24/24 connectés · aucune sortie

              +
              +
              +
              +
              +

              Licences et factures

              paiement par mandat administratif
              +
              Licences utilisées118 / 120

              Il reste 2 licences. Au-delà, chaque élève ajouté est facturé 24 € HT au prorata.

              +
              PièceDateMontant HTStatut
              Devis D-2026-04102/09/20262 880,00 €Accepté
              Bon de commande BC-118705/09/20262 880,00 €Reçu
              Facture F-2026-09310/09/20262 832,00 €Déposée sur Chorus Pro
              +
              +
              +

              Consommation de VM par classe

              15 h incluses / élève / mois
              +
              • BTS SIO 1 · A142 h / 240 h
              • BTS SIO 1 · B128 h / 255 h
              • BTS SIO 2 · SISR160 h / 210 h
              • BTS CIEL 296 h / 225 h
              • Terminale NSI58 h / 360 h
              • Seconde SNT 428 h / 480 h
              +

              Le compteur est partagé par classe. Aucun dépassement prévu d’ici la fin de l’année au rythme actuel.

              +
              +
              +
              +
              +
              + + +
              + + + + + + diff --git a/portail/resources/views/visio.blade.php b/portail/resources/views/visio.blade.php new file mode 100644 index 0000000..145a51c --- /dev/null +++ b/portail/resources/views/visio.blade.php @@ -0,0 +1,190 @@ + + + + + + Visio de classe · Syllabus + + + + + + + + + + + + + +
              + +
              + +
              +
              +

              Visio de classe

              +

              La classe à distance, sans brouhaha.

              +

              Tous les élèves arrivent en sourdine. Ils lèvent la main, vous donnez la parole, un à la fois.

              +
              + + + +
              +

              Les vues sont reliées : levez la main côté élève (Léa M.), puis donnez-lui la parole côté prof.

              +
              + +
              +
              +
              +
              En direct · 00:24:12BTS SIO 1 · Groupe A
              +
              +
              + +
                +
                +
                +

                Mains levées

                  Premier arrivé en haut.

                  +

                  Salle d’attente

                    +

                    Chat des élèves

                    +
                    +
                    + +
                    +
                    BTS SIO 1 · Groupe A · VLAN et routage
                    +
                    +
                    MB

                    M. B. · votre professeur

                    + M. B. +
                    LMVous
                    +
                    +
                    + + + + Quitter +
                    +

                    +
                    + +
                    +
                    +
                    BTS SIO 1 · AEn direct
                    +
                    +
                    MB

                    M. B.

                    +
                    LM
                    +
                    +
                    +
                    + +
                    + + + +
                    +
                    +
                    +
                    + +
                    + + + + + + diff --git a/portail/routes/console.php b/portail/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/portail/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/portail/routes/web.php b/portail/routes/web.php new file mode 100644 index 0000000..2695dac --- /dev/null +++ b/portail/routes/web.php @@ -0,0 +1,82 @@ +name('accueil'); +Route::view('/labs', 'labs.index')->name('labs.index'); +Route::view('/labs/essai', 'labs.essai')->name('labs.essai'); +Route::view('/cours', 'cours.index')->name('cours.index'); +Route::view('/cours/adressage-ipv4', 'cours.show')->name('cours.show'); +Route::view('/etablissements', 'etablissements')->name('etablissements'); +Route::view('/tarifs', 'tarifs')->name('tarifs'); +Route::view('/connexion', 'auth.connexion')->name('login'); +Route::view('/inscription', 'auth.inscription')->name('register'); +// Démo marketing (données fictives) : la vraie gestion de classe est sous /mes-classes. +Route::view('/classes/suivi', 'classes.suivi')->name('suivi'); +Route::view('/examen', 'examen')->name('examen'); +// À brancher sur l'envoi d'e-mail (Mail::to('adam@belkacemi.me')->send(...)). +Route::post('/contact', fn () => back()->with('contact_ok', true))->name('contact'); +Route::view('/fonctionnalites', 'fonctionnalites')->name('fonctionnalites'); +Route::view('/legal', 'legal')->name('legal'); +Route::view('/cours/adressage-ipv4/cm', 'cours.cm')->name('cours.cm'); +Route::view('/banque', 'banque')->name('banque'); +Route::view('/quiz/projection', 'quiz.projection')->name('quiz.projection'); +Route::view('/quiz', 'quiz.eleve')->name('quiz.eleve'); +Route::view('/ide', 'ide')->name('ide'); +Route::view('/bibliotheque', 'bibliotheque')->name('bibliotheque'); +Route::view('/generateur', 'generateur')->name('generateur'); +Route::view('/labs/web', 'labs.web')->name('labs.web'); +Route::view('/maison', 'maison')->name('maison'); +Route::view('/devoirs', 'devoirs')->name('devoirs'); +Route::view('/td/imprimer', 'td.imprimable')->name('td.imprimable'); +Route::view('/cockpit', 'cockpit')->name('cockpit'); +// Démo marketing (données fictives), distincte de /eleve (vrai tableau de bord). +Route::view('/mon-espace', 'espace-eleve')->name('espace.eleve'); +Route::view('/vie-scolaire', 'vie-scolaire')->name('vie.scolaire'); +Route::view('/eleves/fiche', 'fiche-eleve')->name('fiche.eleve'); +Route::view('/correction', 'correction')->name('correction'); +Route::view('/sujets/creer', 'sujet-creer')->name('sujet.creer'); +Route::view('/messagerie', 'messagerie')->name('messagerie'); +Route::view('/exports', 'export-ent')->name('export.ent'); +Route::view('/aide', 'aide')->name('aide'); +Route::view('/academie', 'academie')->name('academie'); +Route::view('/paiement', 'paiement')->name('paiement'); +Route::view('/paiement/confirme', 'paiement-ok')->name('paiement.ok'); +Route::view('/compte/facturation', 'facturation')->name('facturation'); +Route::view('/cours/fiche', 'cours.fiche')->name('cours.fiche'); +Route::view('/etablissement', 'espace-etablissement')->name('espace.etablissement'); +Route::view('/integration', 'integration')->name('integration'); +Route::view('/cohortes', 'cohortes')->name('cohortes'); +Route::view('/ressources/outil', 'ressource')->name('ressource'); +Route::view('/visio', 'visio')->name('visio'); + +// --- Authentification (étape 8) --- +Route::post('/connexion/prof', [ConnexionController::class, 'connexionProf'])->name('connexion.prof'); +Route::post('/connexion/eleve', [ConnexionController::class, 'connexionEleve'])->name('connexion.eleve'); +Route::get('/connexion/qr/{jeton}', [ConnexionController::class, 'connexionParQr'])->name('connexion.qr'); +Route::post('/inscription', [InscriptionController::class, 'store'])->name('inscription.store'); +Route::post('/deconnexion', [ConnexionController::class, 'deconnexion'])->name('deconnexion'); + +// --- Espace élève (protégé, session unique) --- +Route::middleware(['auth:eleve', 'session.unique:eleve'])->group(function () { + Route::get('/eleve', [EleveTableauDeBordController::class, 'show'])->name('eleve'); + Route::post('/seances/{seance}/rejoindre', [SeanceController::class, 'rejoindre'])->name('seances.rejoindre'); + Route::get('/seances/{seance}/attente-eleve', [SeanceController::class, 'attenteEleve'])->name('seances.eleve.attente'); +}); + +// --- Espace prof (protégé, session unique) --- +Route::middleware(['auth:web', 'session.unique:web'])->group(function () { + Route::get('/mes-classes', [ClasseController::class, 'index'])->name('mes-classes.index'); + Route::get('/mes-classes/{classe}', [ClasseController::class, 'show'])->name('mes-classes.show'); + Route::post('/mes-classes/{classe}/eleves', [ClasseController::class, 'storeEleve'])->name('mes-classes.eleves.store'); + Route::post('/mes-classes/{classe}/eleves/import', [ClasseController::class, 'importerCsv'])->name('mes-classes.eleves.importer'); + Route::post('/mes-classes/{classe}/eleves/{eleve}/regenerer-code', [ClasseController::class, 'regenererCode'])->name('mes-classes.eleves.regenererCode'); + Route::post('/mes-classes/{classe}/seances', [SeanceController::class, 'store'])->name('mes-classes.seances.store'); + Route::get('/seances/{seance}/attente', [SeanceController::class, 'attente'])->name('seances.attente'); + Route::post('/seances/{seance}/admettre/{labSession}', [SeanceController::class, 'admettre'])->name('seances.admettre'); +}); diff --git a/portail/storage/app/.gitignore b/portail/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/portail/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/portail/storage/app/private/.gitignore b/portail/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/storage/app/public/.gitignore b/portail/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/storage/framework/.gitignore b/portail/storage/framework/.gitignore new file mode 100644 index 0000000..7a0b408 --- /dev/null +++ b/portail/storage/framework/.gitignore @@ -0,0 +1,10 @@ +compiled.php +config.php +down +events.scanned.php +lsp-*.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/portail/storage/framework/cache/.gitignore b/portail/storage/framework/cache/.gitignore new file mode 100644 index 0000000..d7852b8 --- /dev/null +++ b/portail/storage/framework/cache/.gitignore @@ -0,0 +1,4 @@ +* +!data/ +!locks/ +!.gitignore diff --git a/portail/storage/framework/cache/data/.gitignore b/portail/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/storage/framework/cache/locks/.gitignore b/portail/storage/framework/cache/locks/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/framework/cache/locks/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/storage/framework/sessions/.gitignore b/portail/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/storage/framework/testing/.gitignore b/portail/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/storage/framework/views/.gitignore b/portail/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/storage/logs/.gitignore b/portail/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/portail/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/portail/tests/Feature/ConnexionEleveTest.php b/portail/tests/Feature/ConnexionEleveTest.php new file mode 100644 index 0000000..892b8e2 --- /dev/null +++ b/portail/tests/Feature/ConnexionEleveTest.php @@ -0,0 +1,42 @@ +create([ + 'prenom' => 'Léa', + 'identifiant' => 'lea.m', + 'code_secret' => Hash::make('ABC123'), + ]); + + $this->post(route('connexion.eleve'), [ + 'identifiant' => 'lea.m', + 'code' => 'ABC123', + ])->assertRedirect(route('eleve')); + + $this->assertAuthenticatedAs($eleve, 'eleve'); + + $this->get(route('eleve')) + ->assertOk() + ->assertSee('Salut Léa') + ->assertSee($eleve->classe->nom); +}); + +test('un mauvais code secret refuse la connexion élève', function () { + $eleve = Eleve::factory()->create([ + 'identifiant' => 'lea.m', + 'code_secret' => Hash::make('ABC123'), + ]); + + $this->post(route('connexion.eleve'), [ + 'identifiant' => 'lea.m', + 'code' => 'FAUXCODE', + ])->assertSessionHasErrors('identifiant'); + + $this->assertGuest('eleve'); +}); + +test('un élève non connecté est renvoyé vers la connexion', function () { + $this->get(route('eleve'))->assertRedirect(route('login')); +}); diff --git a/portail/tests/Feature/ConnexionProfTest.php b/portail/tests/Feature/ConnexionProfTest.php new file mode 100644 index 0000000..58936ad --- /dev/null +++ b/portail/tests/Feature/ConnexionProfTest.php @@ -0,0 +1,33 @@ +create(['password' => Hash::make('mot-de-passe-prof')]); + + $this->post(route('connexion.prof'), [ + 'email' => $prof->email, + 'password' => 'mot-de-passe-prof', + ])->assertRedirect(route('mes-classes.index')); + + $this->assertAuthenticatedAs($prof, 'web'); +}); + +test('un mauvais mot de passe refuse la connexion', function () { + $prof = User::factory()->create(['password' => Hash::make('mot-de-passe-prof')]); + + $this->post(route('connexion.prof'), [ + 'email' => $prof->email, + 'password' => 'mauvais-mot-de-passe', + ])->assertSessionHasErrors('email'); + + $this->assertGuest('web'); +}); + +test('la page de connexion affiche le formulaire prof et élève', function () { + $this->get(route('login')) + ->assertOk() + ->assertSee('Je suis élève') + ->assertSee('Je suis enseignant'); +}); diff --git a/portail/tests/Feature/CreationClasseTest.php b/portail/tests/Feature/CreationClasseTest.php new file mode 100644 index 0000000..b915de9 --- /dev/null +++ b/portail/tests/Feature/CreationClasseTest.php @@ -0,0 +1,67 @@ +post(route('inscription.store'), [ + 'prenom' => 'Chloé', + 'nom' => 'Roux', + 'email' => 'chloe.roux@exemple.test', + 'password' => 'mot-de-passe-sur', + 'password_confirmation' => 'mot-de-passe-sur', + 'etablissement_nom_liste' => 'autre', + 'etablissement_nom' => 'Lycée Marie-Curie', + 'etablissement_ville' => 'Nancy', + 'etablissement_uai' => '', + 'formation' => 'BTS SIO SISR', + 'classe' => '2nde NSI · Groupe B', + ]); + + $etablissement = Etablissement::where('nom', 'Lycée Marie-Curie')->first(); + expect($etablissement)->not->toBeNull(); + + $prof = User::where('email', 'chloe.roux@exemple.test')->first(); + expect($prof)->not->toBeNull(); + expect($prof->etablissement_id)->toBe($etablissement->id); + + $classe = Classe::where('nom', '2nde NSI · Groupe B')->first(); + expect($classe)->not->toBeNull(); + expect($classe->user_id)->toBe($prof->id); + expect($classe->code_classe)->toHaveLength(6); + + $reponse->assertRedirect(route('mes-classes.show', $classe)); + $this->assertAuthenticatedAs($prof, 'web'); +}); + +test('une classe existante peut réutiliser le même établissement', function () { + $this->post(route('inscription.store'), [ + 'prenom' => 'A', 'nom' => 'B', 'email' => 'a@exemple.test', + 'password' => 'mot-de-passe-sur', 'password_confirmation' => 'mot-de-passe-sur', + 'etablissement_nom_liste' => 'Lycée Jean-Jaurès · Montreuil', + 'formation' => 'BTS SIO SISR', 'classe' => 'Classe 1', + ]); + + $this->post(route('inscription.store'), [ + 'prenom' => 'C', 'nom' => 'D', 'email' => 'c@exemple.test', + 'password' => 'mot-de-passe-sur', 'password_confirmation' => 'mot-de-passe-sur', + 'etablissement_nom_liste' => 'Lycée Jean-Jaurès · Montreuil', + 'formation' => 'BTS SIO SISR', 'classe' => 'Classe 2', + ]); + + expect(Etablissement::where('nom', 'Lycée Jean-Jaurès · Montreuil')->count())->toBe(1); + expect(Classe::count())->toBe(2); +}); + +test("l'inscription échoue si les mots de passe ne correspondent pas", function () { + $this->post(route('inscription.store'), [ + 'prenom' => 'Chloé', 'nom' => 'Roux', 'email' => 'chloe@exemple.test', + 'password' => 'mot-de-passe-sur', 'password_confirmation' => 'autre-chose', + 'etablissement_nom_liste' => 'autre', + 'etablissement_nom' => 'Lycée Marie-Curie', 'etablissement_ville' => 'Nancy', + 'formation' => 'BTS SIO SISR', 'classe' => '2nde NSI', + ])->assertSessionHasErrors('password'); + + expect(User::count())->toBe(0); +}); diff --git a/portail/tests/Feature/SessionUniqueTest.php b/portail/tests/Feature/SessionUniqueTest.php new file mode 100644 index 0000000..c50fb90 --- /dev/null +++ b/portail/tests/Feature/SessionUniqueTest.php @@ -0,0 +1,60 @@ +create(['password' => Hash::make('mot-de-passe-prof')]); + + // "Navigateur A" se connecte. + $this->post(route('connexion.prof'), [ + 'email' => $prof->email, + 'password' => 'mot-de-passe-prof', + ]); + $jetonA = $prof->refresh()->jeton_session_actif; + expect($jetonA)->not->toBeNull(); + + // "Navigateur B" se connecte avec le même compte. + $this->post(route('connexion.prof'), [ + 'email' => $prof->email, + 'password' => 'mot-de-passe-prof', + ])->assertSessionHas('session_precedente_fermee', true); + $jetonB = $prof->refresh()->jeton_session_actif; + expect($jetonB)->not->toBe($jetonA); + + // "Navigateur A", qui ne sait pas qu'il a été coupé, retente une page protégée. + $this->withSession(['jeton_session_web' => $jetonA]) + ->actingAs($prof, 'web') + ->get(route('mes-classes.index')) + ->assertRedirect(route('login')) + ->assertSessionHas('session_coupee'); + + $this->assertGuest('web'); + + // "Navigateur B" continue de fonctionner normalement. + $this->withSession(['jeton_session_web' => $jetonB]) + ->actingAs($prof, 'web') + ->get(route('mes-classes.index')) + ->assertOk(); +}); + +test('une nouvelle connexion élève coupe la précédente', function () { + $eleve = Eleve::factory()->create([ + 'identifiant' => 'lea.m', + 'code_secret' => Hash::make('ABC123'), + ]); + + $this->post(route('connexion.eleve'), ['identifiant' => 'lea.m', 'code' => 'ABC123']); + $jetonA = $eleve->refresh()->jeton_session_actif; + + $this->post(route('connexion.eleve'), ['identifiant' => 'lea.m', 'code' => 'ABC123']); + $jetonB = $eleve->refresh()->jeton_session_actif; + expect($jetonB)->not->toBe($jetonA); + + $this->withSession(['jeton_session_eleve' => $jetonA]) + ->actingAs($eleve, 'eleve') + ->get(route('eleve')) + ->assertRedirect(route('login')) + ->assertSessionHas('session_coupee'); +}); diff --git a/portail/tests/Pest.php b/portail/tests/Pest.php new file mode 100644 index 0000000..941e024 --- /dev/null +++ b/portail/tests/Pest.php @@ -0,0 +1,50 @@ +extend(TestCase::class) + ->use(RefreshDatabase::class) + ->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/portail/tests/TestCase.php b/portail/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/portail/tests/TestCase.php @@ -0,0 +1,10 @@ +