From 61a3a453be708f1dc6bbf07445b72757a938fa44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Sun, 20 Sep 2026 09:52:02 +0200 Subject: [PATCH] =?UTF-8?q?Vider=20le=20monorepo=20:=20chaque=20domaine=20?= =?UTF-8?q?a=20son=20d=C3=A9p=C3=B4t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dernier acte de la migration commencée le 2026-05-03. home/ part dans alpinux-www, le dernier à être détaché ; dynamic/ et admin/ n'étaient plus que des copies périmées de alpinux-dynamic et alpinux-admin, où le code vit et se déploie ; wiki/, static/, feedback/ et portail/ ne contenaient plus qu'un écriteau. Le README devient ce qu'il reste d'utile : la table des domaines et de leurs dépôts. Seul scripts/owni-certs-sync.sh reste ici — outil du serveur, rattaché à aucun domaine. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ae3qPEPRrNkEY5LwcTTKc1 --- README.md | 118 +-- admin/.env.example | 13 - admin/app.py | 120 --- admin/builds.py | 103 --- admin/requirements.txt | 4 - admin/static/admin.css | 71 -- admin/templates/index.html | 215 ----- dynamic/.env.example | 15 - dynamic/app.py | 46 - dynamic/auth_utils.py | 16 - dynamic/data/quizzes.json | 679 --------------- dynamic/db.py | 61 -- dynamic/quiz.py | 31 - dynamic/requirements.txt | 3 - dynamic/routes/__init__.py | 0 dynamic/routes/auth.py | 34 - dynamic/routes/protected.py | 37 - dynamic/routes/public.py | 85 -- dynamic/static/quiz.js | 49 -- dynamic/static/style.css | 219 ----- dynamic/templates/base.html | 58 -- dynamic/templates/index.html | 83 -- dynamic/templates/profil/index.html | 83 -- dynamic/templates/quiz/intro.html | 55 -- dynamic/templates/quiz/list.html | 64 -- dynamic/templates/quiz/play.html | 52 -- dynamic/templates/quiz/result.html | 73 -- feedback/README.md | 26 - home/index.html | 1229 --------------------------- home/robots.txt | 4 - home/sitemap.xml | 9 - portail/README.md | 16 - static/README.md | 34 - wiki/README.md | 34 - 34 files changed, 30 insertions(+), 3709 deletions(-) delete mode 100644 admin/.env.example delete mode 100644 admin/app.py delete mode 100644 admin/builds.py delete mode 100644 admin/requirements.txt delete mode 100644 admin/static/admin.css delete mode 100644 admin/templates/index.html delete mode 100644 dynamic/.env.example delete mode 100644 dynamic/app.py delete mode 100644 dynamic/auth_utils.py delete mode 100644 dynamic/data/quizzes.json delete mode 100644 dynamic/db.py delete mode 100644 dynamic/quiz.py delete mode 100644 dynamic/requirements.txt delete mode 100644 dynamic/routes/__init__.py delete mode 100644 dynamic/routes/auth.py delete mode 100644 dynamic/routes/protected.py delete mode 100644 dynamic/routes/public.py delete mode 100644 dynamic/static/quiz.js delete mode 100644 dynamic/static/style.css delete mode 100644 dynamic/templates/base.html delete mode 100644 dynamic/templates/index.html delete mode 100644 dynamic/templates/profil/index.html delete mode 100644 dynamic/templates/quiz/intro.html delete mode 100644 dynamic/templates/quiz/list.html delete mode 100644 dynamic/templates/quiz/play.html delete mode 100644 dynamic/templates/quiz/result.html delete mode 100644 feedback/README.md delete mode 100644 home/index.html delete mode 100644 home/robots.txt delete mode 100644 home/sitemap.xml delete mode 100644 portail/README.md delete mode 100644 static/README.md delete mode 100644 wiki/README.md diff --git a/README.md b/README.md index 552c3de..a41ec9c 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,43 @@ -# org.alpinux.owni — Monorepo Alpinux +# alpinux.site.2026 — l'ancien monorepo -Dépôt unique de l'association **Alpinux** (LUG de Savoie). -Gitea : https://gitea.alpinux.org/alpinux.cedrica5l/alpinux.site.2026 +Ce dépôt a porté tous les sous-domaines d'**Alpinux** (LUG de Savoie) jusqu'à la +migration « un domaine = un dépôt », commencée le 2026-05-03 et terminée le 2026-09-20. -> **Le wiki n'est plus ici.** Depuis le 2026-09-19, `wiki.alpinux.org` est publié depuis -> le dépôt [alpinux-wiki](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki) — -> voir [`wiki/README.md`](wiki/README.md). Un push dans ce dépôt-ci ne publie plus rien. - -> **Le CDN non plus.** Depuis le 2026-09-20, `static.alpinux.org` vit dans le dépôt -> [alpinux-static](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-static) — -> voir [`static/README.md`](static/README.md), localement `~/Projects/alpinux.static/`. +**Il ne publie plus rien.** Un push ici ne met aucun site en ligne. --- -## Structure +## Où est passé quoi -Chaque sous-dossier correspond à un sous-domaine indépendant de `alpinux.org`. +| Domaine | Dépôt | Local | +|---|---|---| +| `alpinux.org` | [alpinux-www](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-www) | `~/Projects/alpinux.www` | +| `wiki.alpinux.org` | [alpinux-wiki](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki) | `~/Projects/alpinux.wiki` | +| `dynamic.alpinux.org` | [alpinux-dynamic](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-dynamic) | `~/Projects/alpinux.dynamic` | +| `admin.alpinux.org` | [alpinux-admin](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-admin) | `~/Projects/alpinux.admin` | +| `static.alpinux.org` | [alpinux-static](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-static) | — | +| `portail.alpinux.org` | [alpinux-portail](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-portail) | `~/Projects/alpinux-portail` | +| `feedback.alpinux.org` | [alpinux-feedback](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-feedback) | — | +| `installparty.alpinux.org` | [alpinux-installparty](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-installparty) | `~/Projects/alpinux.installparty` | -``` -org.alpinux.owni/ -├── wiki/ → déménagé dans le dépôt alpinux-wiki (voir wiki/README.md) -├── home/ → alpinux.org Page d'accueil statique -├── dynamic/ → dynamic.alpinux.org Flask — quiz et jeux (public + membres) -├── admin/ → admin.alpinux.org Flask — interface d'administration -├── static/ → déménagé dans le dépôt alpinux-static (voir static/README.md) -├── portail/ → portail.alpinux.org Portail membres (ISPConfig, Nextcloud…) -└── feedback/ → feedback.alpinux.org Formulaire de retours (à construire) -``` +Chaque dépôt porte son propre README : comment le lancer en local, comment il se déploie, +et ses pièges. Le déploiement, lui, est partout le même principe — un push sur `main`, +un webhook Gitea, un script sur le serveur. -Les configurations serveur (Apache, systemd) sont gérées, par ISPConfig à l'adresse https://owni.alpinux.org:8080, séparément et ne sont **pas versionnées**. +## Pourquoi séparer + +Un dépôt par domaine permet d'ouvrir l'écriture là où c'est utile sans donner le reste : +quelqu'un qui corrige une faute dans le wiki n'a pas besoin des scripts de déploiement du +site, ni du code de l'interface d'administration. --- -## Projets indépendants, liés par l'infrastructure +## Ce qui reste ici -Chaque projet : -- utilise **AlpID** (SSO Keycloak) pour l'authentification quand nécessaire -- a son propre environnement Python (`venv/`) et son fichier `.env` (non versionés) +`scripts/owni-certs-sync.sh` — la synchronisation des certificats Let's Encrypt vers les +copies qu'ISPConfig ne rafraîchit pas. Outil du serveur, rattaché à aucun domaine. -Les projets sont liés par le SSO partagé, le serveur commun, et l'identité visuelle définie dans https://wiki.alpinux.org/communication/. - ---- - -## Convention git et Claude - -> **Travailler dans le sous-dossier du projet concerné**, pas à la racine du dépôt. - -```bash -# le wiki se modifie dans ~/Projects/alpinux.wiki (dépôt alpinux-wiki) -# le CDN se modifie dans ~/Projects/alpinux.static (dépôt alpinux-static) -cd dynamic/ # pour modifier l'appli quiz -cd admin/ # pour modifier l'interface admin -cd portail/ # pour modifier le portail membres -cd feedback/ # pour modifier le formulaire de retour -``` - -De même, lorsqu'on utilise **Claude Code** (ou tout autre outil IA) : -lancer Claude depuis le sous-dossier du projet pour que le contexte soit limité à ce projet. - -```bash -cd ~/Projects/alpinux.wiki && claude -cd ~/Projects/org.alpinux.owni/admin && claude -cd ~/Projects/org.alpinux.owni/dynamic && claude -cd ~/Projects/alpinux.static && claude -cd ~/Projects/org.alpinux.owni/portail && claude -cd ~/Projects/org.alpinux.owni/feedback && claude -``` - ---- - -## Démarrage rapide par projet - -### wiki (wiki.alpinux.org) -Déménagé : voir le dépôt [alpinux-wiki](https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki) -et son README. - -### dynamic (dynamic.alpinux.org) -```bash -cd dynamic -python3 -m venv venv && source venv/bin/activate -pip install -r requirements.txt -cp .env.example .env && nano .env # renseigner les clés AlpID -flask run -``` - -### admin (admin.alpinux.org) -```bash -cd admin -python3 -m venv venv && source venv/bin/activate -pip install -r requirements.txt -cp .env.example .env && nano .env # renseigner les clés AlpID -flask run -``` - ---- - -## Déploiement serveur - -Voir https://wiki.alpinux.org/technique/ pour la documentation. -Les fichiers de configuration serveur sont conservés localement hors dépôt. +Les configurations serveur (Apache, systemd) restent gérées par +[ISPConfig](https://owni.alpinux.org:8080) et ne sont pas versionnées — à l'exception de +la chaîne de déploiement de chaque site, qui vit maintenant dans le dépôt du site +concerné. diff --git a/admin/.env.example b/admin/.env.example deleted file mode 100644 index 008db34..0000000 --- a/admin/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -SECRET_KEY=changez-moi-avec-une-valeur-aleatoire-longue - -ALPID_CLIENT_ID= -ALPID_CLIENT_SECRET= -ALPID_DISCOVERY_URL=https://alpid.alpinux.org/realms/alpinux/.well-known/openid-configuration - -# Noms des groupes Keycloak autorisés (séparés par virgule) -ADMIN_GROUPS=admins - -# Chemins de déploiement (valeurs par défaut dans /tmp — à définir en production) -# DEPLOY_SCRIPT= -# STATE_FILE= -# LOG_FILE= diff --git a/admin/app.py b/admin/app.py deleted file mode 100644 index 83d0d1d..0000000 --- a/admin/app.py +++ /dev/null @@ -1,120 +0,0 @@ -import os -from flask import Flask, redirect, url_for, session, request, jsonify, render_template, abort -from authlib.integrations.flask_client import OAuth -from werkzeug.middleware.proxy_fix import ProxyFix - -import builds - -app = Flask(__name__) -app.secret_key = os.environ["SECRET_KEY"] - -# Gère X-Forwarded-Proto envoyé par Apache -app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1) - -# ── OIDC AlpID ──────────────────────────────────────────────────── -oauth = OAuth(app) -oauth.register( - name="alpid", - server_metadata_url=os.environ["ALPID_DISCOVERY_URL"], - client_id=os.environ["ALPID_CLIENT_ID"], - client_secret=os.environ["ALPID_CLIENT_SECRET"], - client_kwargs={"scope": "openid profile email groups"}, -) - -ADMIN_GROUPS = set(os.environ.get("ADMIN_GROUPS", "admins").split(",")) - - -def _user(): - return session.get("user") - - -def _require_admin(): - user = _user() - if not user: - session["next_url"] = request.url - return redirect(url_for("login")) - if not user.get("is_admin"): - abort(403) - return None - - -# ── Auth ────────────────────────────────────────────────────────── -@app.route("/auth/login") -def login(): - return oauth.alpid.authorize_redirect(url_for("callback", _external=True)) - - -@app.route("/auth/callback") -def callback(): - token = oauth.alpid.authorize_access_token() - info = token.get("userinfo") or oauth.alpid.userinfo(token=token) - - # Keycloak expose les groupes dans le claim "groups" (à activer dans le mapper) - groups = set(info.get("groups", [])) - is_admin = bool(groups & ADMIN_GROUPS) - - session["user"] = { - "sub": info["sub"], - "name": info.get("name") or info.get("preferred_username", "Admin"), - "email": info.get("email", ""), - "is_admin": is_admin, - } - return redirect(session.pop("next_url", url_for("index"))) - - -@app.route("/auth/logout") -def logout(): - session.clear() - return redirect(url_for("index")) - - -# ── Pages ───────────────────────────────────────────────────────── -@app.route("/") -def index(): - redir = _require_admin() - if redir: - return redir - state = builds.get_state() - log = builds.get_log() - return render_template("index.html", user=_user(), state=state, log=log) - - -# ── API JSON ────────────────────────────────────────────────────── -@app.route("/api/build", methods=["POST"]) -def api_build(): - redir = _require_admin() - if redir: - return jsonify({"error": "non authentifié"}), 401 - user = _user() - ok = builds.start_build(triggered_by=user["name"]) - if ok: - return jsonify({"started": True}) - return jsonify({"started": False, "reason": "Un build est déjà en cours"}), 409 - - -@app.route("/api/status") -def api_status(): - redir = _require_admin() - if redir: - return jsonify({"error": "non authentifié"}), 401 - state = builds.get_state() - return jsonify({ - "running": state.get("running", False), - "last_success": state.get("last_success"), - "last_duration": state.get("last_duration_s"), - "started_at": state.get("started_at"), - "triggered_by": state.get("triggered_by"), - "history": state.get("history", [])[:5], - }) - - -@app.route("/api/log") -def api_log(): - redir = _require_admin() - if redir: - return jsonify({"error": "non authentifié"}), 401 - return jsonify({"log": builds.get_log()}) - - -if __name__ == "__main__": - app.run(debug=False, port=5002) diff --git a/admin/builds.py b/admin/builds.py deleted file mode 100644 index ed98aab..0000000 --- a/admin/builds.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Gestion des builds : exécution, état, historique. -Un seul build peut tourner à la fois. -""" -import json -import os -import subprocess -import threading -import time -from datetime import datetime -from pathlib import Path - -STATE_FILE = Path(os.environ.get("STATE_FILE", "/tmp/admin-state.json")) -LOG_FILE = Path(os.environ.get("LOG_FILE", "/tmp/admin-build.log")) -DEPLOY_SCRIPT = Path(os.environ.get("DEPLOY_SCRIPT", "deploy-wiki.sh")) - -_lock = threading.Lock() - - -def _load_state() -> dict: - if STATE_FILE.exists(): - try: - return json.loads(STATE_FILE.read_text()) - except Exception: - pass - return {"running": False, "history": []} - - -def _save_state(state: dict): - STATE_FILE.parent.mkdir(parents=True, exist_ok=True) - STATE_FILE.write_text(json.dumps(state, indent=2)) - - -def get_state() -> dict: - return _load_state() - - -def get_log(lines: int = 200) -> str: - if LOG_FILE.exists(): - all_lines = LOG_FILE.read_text(encoding="utf-8", errors="replace").splitlines() - return "\n".join(all_lines[-lines:]) - return "" - - -def is_running() -> bool: - return _load_state().get("running", False) - - -def start_build(triggered_by: str) -> bool: - """Lance le build dans un thread. Retourne False si déjà en cours.""" - with _lock: - state = _load_state() - if state.get("running"): - return False - state["running"] = True - state["started_at"] = datetime.now().isoformat(timespec="seconds") - state["triggered_by"] = triggered_by - _save_state(state) - - LOG_FILE.parent.mkdir(parents=True, exist_ok=True) - LOG_FILE.write_text("") - - threading.Thread(target=_run_build, args=(triggered_by,), daemon=True).start() - return True - - -def _run_build(triggered_by: str): - started = time.time() - success = False - try: - with open(LOG_FILE, "w", buffering=1) as log: - log.write(f"=== Build démarré le {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} par {triggered_by} ===\n\n") - proc = subprocess.Popen( - ["bash", str(DEPLOY_SCRIPT)], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - ) - for line in proc.stdout: - log.write(line) - log.flush() - proc.wait() - success = proc.returncode == 0 - log.write(f"\n=== Terminé (code {proc.returncode}) en {int(time.time()-started)}s ===\n") - except Exception as exc: - with open(LOG_FILE, "a") as log: - log.write(f"\n=== ERREUR : {exc} ===\n") - - with _lock: - state = _load_state() - state["running"] = False - state["last_success"] = success - state["last_duration_s"] = int(time.time() - started) - entry = { - "at": datetime.now().isoformat(timespec="seconds"), - "triggered_by": triggered_by, - "success": success, - "duration_s": int(time.time() - started), - } - state.setdefault("history", []).insert(0, entry) - state["history"] = state["history"][:20] - _save_state(state) diff --git a/admin/requirements.txt b/admin/requirements.txt deleted file mode 100644 index c436512..0000000 --- a/admin/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flask>=3.0 -authlib>=1.3 -requests>=2.31 -gunicorn>=21.0 diff --git a/admin/static/admin.css b/admin/static/admin.css deleted file mode 100644 index d70a7fa..0000000 --- a/admin/static/admin.css +++ /dev/null @@ -1,71 +0,0 @@ -:root { - --blue: #1a6bbf; - --blue-dark: #0f4e8f; - --blue-light:#e8f1fb; - --bg: #f3f6fb; - --text: #1a1a2e; - --muted: #666; - --radius: 10px; - --shadow: 0 2px 12px rgba(26,107,191,.1); -} - -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; } -code { background: #e8eef7; padding: .1rem .4rem; border-radius: 4px; font-size: .88em; } -a { color: var(--blue); } - -/* ── Header ── */ -header { background: var(--blue-dark); color: #fff; } -.header-inner { max-width: 900px; margin: 0 auto; padding: .8rem 1.5rem; display: flex; align-items: center; gap: 1rem; } -.brand { display: flex; align-items: center; gap: .6rem; color: #fff; font-size: 1.1rem; text-decoration: none; } -.brand strong { font-weight: 800; } -.brand-sub { opacity: .7; font-weight: 300; } -.brand img { border-radius: 5px; } -.header-user { margin-left: auto; display: flex; align-items: center; gap: 1rem; color: rgba(255,255,255,.85); font-size: .9rem; } -.btn-logout { color: rgba(255,255,255,.75); font-size: .85rem; border: 1px solid rgba(255,255,255,.4); border-radius: 4px; padding: .3rem .7rem; text-decoration: none; } -.btn-logout:hover { background: rgba(255,255,255,.1); color: #fff; } - -/* ── Main ── */ -main { max-width: 900px; margin: 2rem auto; padding: 0 1.5rem 3rem; display: flex; flex-direction: column; gap: 1.5rem; } - -/* ── Build card ── */ -.build-card { background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.8rem 2rem; } -.build-card-header { display: flex; align-items: flex-start; gap: 1rem; justify-content: space-between; flex-wrap: wrap; margin-bottom: .8rem; } -.build-card-header h2 { font-size: 1.1rem; margin-bottom: .2rem; } -.subtitle { font-size: .85rem; color: var(--muted); } -.build-meta { display: flex; gap: 1.5rem; font-size: .85rem; color: var(--muted); margin-bottom: 1.2rem; flex-wrap: wrap; } - -.btn { display: inline-flex; align-items: center; gap: .5rem; padding: .65rem 1.6rem; border-radius: 6px; font-size: .95rem; font-weight: 600; cursor: pointer; border: none; transition: all .15s; } -.btn-build { background: var(--blue); color: #fff; font-size: 1rem; } -.btn-build:hover:not(:disabled) { background: var(--blue-dark); } -.btn-build:disabled { opacity: .55; cursor: not-allowed; } - -/* ── Status badges ── */ -.status-badge { display: inline-block; padding: .25rem .7rem; border-radius: 20px; font-size: .82rem; font-weight: 700; white-space: nowrap; } -.status-badge.running { background: #fff3d4; color: #8a5c00; } -.status-badge.success { background: #d4f0dc; color: #1a6b35; } -.status-badge.failure { background: #fde8e8; color: #8a0000; } -.status-badge.idle { background: var(--blue-light); color: var(--blue-dark); } - -/* ── Log ── */ -.log-section { background: #111827; border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); } -.log-header { display: flex; align-items: center; justify-content: space-between; padding: .7rem 1.2rem; background: #1f2937; color: #d1d5db; } -.log-header h3 { font-size: .9rem; font-weight: 600; } -.scroll-toggle { display: flex; align-items: center; gap: .4rem; font-size: .82rem; color: #9ca3af; cursor: pointer; } -.scroll-toggle input { cursor: pointer; accent-color: var(--blue); } -.log-output { color: #a3e635; font-family: 'Courier New', monospace; font-size: .82rem; line-height: 1.55; padding: 1rem 1.2rem; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-break: break-word; } - -/* ── History ── */ -.history-section { background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.5rem 1.8rem; } -.history-section h3 { font-size: 1rem; margin-bottom: 1rem; color: var(--blue-dark); } -.history-table { width: 100%; border-collapse: collapse; font-size: .88rem; } -.history-table th { text-align: left; padding: .5rem .8rem; background: var(--blue-light); color: var(--blue-dark); font-weight: 600; } -.history-table td { padding: .5rem .8rem; border-bottom: 1px solid var(--blue-light); } -.history-table tr:last-child td { border-bottom: none; } -.empty { text-align: center; color: var(--muted); padding: 1.2rem; } - -@media (max-width: 600px) { - .build-card-header { flex-direction: column; } - .header-inner { flex-wrap: wrap; } - .history-table th:nth-child(4), .history-table td:nth-child(4) { display: none; } -} diff --git a/admin/templates/index.html b/admin/templates/index.html deleted file mode 100644 index bc02585..0000000 --- a/admin/templates/index.html +++ /dev/null @@ -1,215 +0,0 @@ - - - - - - Admin — Alpinux - - - - - -
-
- - Alpinux - Alpinux Admin - -
- {{ user.name }} - Déconnexion -
-
-
- -
- - -
-
-
-

📖 Wiki — wiki.alpinux.org

-

Lance git pull + mkdocs build --strict sur le serveur

-
-
- {% if state.running %} - ⏳ En cours… - {% elif state.last_success is none %} - En attente - {% elif state.last_success %} - ✓ Succès - {% else %} - ✗ Échec - {% endif %} -
-
- -
- {% if state.triggered_by %} - Dernier déclencheur : {{ state.triggered_by }} - {% endif %} - {% if state.last_duration_s %} - Durée : {{ state.last_duration_s }}s - {% endif %} -
- - -
- - -
-
-

Journal du dernier build

- -
-
{{ log }}
-
- - -
-

Historique des builds

- - - - - - {% for entry in state.history %} - - - - - - - {% else %} - - {% endfor %} - -
DateDéclenché parRésultatDurée
{{ entry.at }}{{ entry.triggered_by }} - {% if entry.success %} - ✓ Succès - {% else %} - ✗ Échec - {% endif %} - {{ entry.duration_s }}s
Aucun build enregistré
-
- -
- - - - diff --git a/dynamic/.env.example b/dynamic/.env.example deleted file mode 100644 index 8843c58..0000000 --- a/dynamic/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -# Copier ce fichier en .env et remplir les valeurs -# Ne jamais commiter le fichier .env - -SECRET_KEY=changez-moi-avec-une-valeur-aleatoire-longue - -# AlpID / OIDC (obtenir les valeurs dans la console Keycloak) -ALPID_CLIENT_ID= -ALPID_CLIENT_SECRET= -ALPID_DISCOVERY_URL=https://alpid.alpinux.org/realms/alpinux/.well-known/openid-configuration - -# Base de données SQLite -DATABASE= - -# Mode debug (mettre à 0 en production) -FLASK_DEBUG=0 diff --git a/dynamic/app.py b/dynamic/app.py deleted file mode 100644 index 887f6f3..0000000 --- a/dynamic/app.py +++ /dev/null @@ -1,46 +0,0 @@ -import os -from flask import Flask -from authlib.integrations.flask_client import OAuth - -from db import close_db, init_db -from routes.public import public_bp -from routes.auth import auth_bp -from routes.protected import protected_bp - - -def create_app(): - app = Flask(__name__) - - app.secret_key = os.environ['SECRET_KEY'] - - # OIDC AlpID - oauth = OAuth(app) - oauth.register( - name='alpid', - server_metadata_url=os.environ['ALPID_DISCOVERY_URL'], - client_id=os.environ['ALPID_CLIENT_ID'], - client_secret=os.environ['ALPID_CLIENT_SECRET'], - client_kwargs={'scope': 'openid profile email'}, - ) - app.extensions['oauth'] = oauth - - # Filtre Jinja2 utilitaire - app.jinja_env.filters['enumerate'] = enumerate - - # Blueprints - app.register_blueprint(public_bp) - app.register_blueprint(auth_bp, url_prefix='/auth') - app.register_blueprint(protected_bp) - - # Base de données - app.teardown_appcontext(close_db) - with app.app_context(): - init_db() - - return app - - -app = create_app() - -if __name__ == '__main__': - app.run(debug=os.environ.get('FLASK_DEBUG', '0') == '1') diff --git a/dynamic/auth_utils.py b/dynamic/auth_utils.py deleted file mode 100644 index 1844b69..0000000 --- a/dynamic/auth_utils.py +++ /dev/null @@ -1,16 +0,0 @@ -from functools import wraps -from flask import session, redirect, url_for, request - - -def current_user(): - return session.get('user') - - -def login_required(f): - @wraps(f) - def decorated(*args, **kwargs): - if not current_user(): - session['next_url'] = request.url - return redirect(url_for('auth.login')) - return f(*args, **kwargs) - return decorated diff --git a/dynamic/data/quizzes.json b/dynamic/data/quizzes.json deleted file mode 100644 index a1e022c..0000000 --- a/dynamic/data/quizzes.json +++ /dev/null @@ -1,679 +0,0 @@ -[ - { - "id": "bureau-decouverte", - "title": "Le bureau Linux", - "description": "Maîtrisez les bases de l'interface graphique de Linux Mint.", - "level": "Découverte", - "level_id": 1, - "members_only": false, - "duration_min": 3, - "icon": "🖥️", - "questions": [ - { - "id": 1, - "text": "Sur Linux Mint, comment ouvre-t-on une application depuis le menu ?", - "choices": [ - "On double-clique sur l'icône dans la barre des tâches", - "On clique sur le menu principal puis on cherche l'application", - "On appuie sur F5", - "On tape le nom dans la barre d'adresse" - ], - "answer": 1 - }, - { - "id": 2, - "text": "Que se passe-t-il quand on clique sur le bouton ✕ d'une fenêtre ?", - "choices": [ - "Le programme se met en veille", - "La fenêtre se réduit dans la barre des tâches", - "Le programme se ferme", - "L'ordinateur s'éteint" - ], - "answer": 2 - }, - { - "id": 3, - "text": "Qu'est-ce que la barre des tâches sous Linux Mint (bureau Cinnamon) ?", - "choices": [ - "Une barre de progression pour les téléchargements", - "La barre en bas de l'écran qui affiche les fenêtres ouvertes", - "Un outil de traduction de texte", - "La barre de menu d'une application" - ], - "answer": 1 - }, - { - "id": 4, - "text": "Comment déplacer une fenêtre sur le bureau ?", - "choices": [ - "En cliquant droit dessus et en choisissant « Déplacer »", - "En faisant glisser la barre de titre de la fenêtre", - "En maintenant Ctrl et en cliquant", - "Ce n'est pas possible sous Linux" - ], - "answer": 1 - }, - { - "id": 5, - "text": "Où vont les fichiers quand on les supprime dans le gestionnaire de fichiers ?", - "choices": [ - "Ils sont définitivement effacés immédiatement", - "Ils vont dans la Corbeille", - "Ils sont déplacés vers /tmp", - "Ils sont compressés automatiquement" - ], - "answer": 1 - }, - { - "id": 6, - "text": "Comment prendre une capture d'écran sous Linux Mint ?", - "choices": [ - "Ctrl+P", - "Alt+Impr écran (pour la fenêtre active)", - "La touche Impr écran (Print Screen) pour tout l'écran", - "Ctrl+C" - ], - "answer": 2 - } - ] - }, - { - "id": "internet-decouverte", - "title": "Internet et le navigateur web", - "description": "Naviguez sur le web en toute confiance avec Firefox.", - "level": "Découverte", - "level_id": 1, - "members_only": false, - "duration_min": 3, - "icon": "🌐", - "questions": [ - { - "id": 1, - "text": "Qu'est-ce qu'une URL ?", - "choices": [ - "Un type de virus informatique", - "L'adresse d'une page web", - "Un logiciel de navigation", - "Un protocole de messagerie" - ], - "answer": 1 - }, - { - "id": 2, - "text": "Que signifie HTTPS par rapport à HTTP ?", - "choices": [ - "HTTPS est plus rapide que HTTP", - "HTTPS chiffre la communication entre votre navigateur et le serveur", - "HTTPS est réservé aux sites professionnels", - "Il n'y a aucune différence pratique" - ], - "answer": 1 - }, - { - "id": 3, - "text": "Comment ouvrir un nouvel onglet dans Firefox ?", - "choices": [ - "Ctrl+N", - "Ctrl+T", - "Ctrl+O", - "F5" - ], - "answer": 1 - }, - { - "id": 4, - "text": "Qu'est-ce qu'un moteur de recherche ?", - "choices": [ - "Un logiciel qui accélère votre ordinateur", - "Un service qui référence des pages web et permet de les trouver", - "Un programme qui traduit les pages web automatiquement", - "Un type de navigateur spécialisé" - ], - "answer": 1 - }, - { - "id": 5, - "text": "Comment recharger une page web dans Firefox ?", - "choices": [ - "Ctrl+R ou F5", - "Ctrl+L", - "Alt+F4", - "Ctrl+Z" - ], - "answer": 0 - }, - { - "id": 6, - "text": "Qu'est-ce qu'un favori (bookmark) dans un navigateur ?", - "choices": [ - "Une page web enregistrée pour y accéder rapidement", - "Le site web le plus visité de la semaine", - "L'historique complet de navigation", - "Un mot de passe enregistré" - ], - "answer": 0 - } - ] - }, - { - "id": "fichiers-debutant", - "title": "Fichiers et dossiers", - "description": "Comprenez l'organisation des fichiers sous Linux.", - "level": "Débutant", - "level_id": 2, - "members_only": false, - "duration_min": 4, - "icon": "📁", - "questions": [ - { - "id": 1, - "text": "Quel est le dossier personnel de l'utilisateur « alice » sous Linux ?", - "choices": [ - "/home/alice", - "C:\\Users\\alice", - "/usr/alice", - "/root/alice" - ], - "answer": 0 - }, - { - "id": 2, - "text": "Qu'est-ce qu'une extension de fichier ?", - "choices": [ - "La taille d'un fichier en octets", - "Les lettres après le point dans le nom d'un fichier (.pdf, .mp3…)", - "Le dossier où se trouve un fichier", - "La date de création d'un fichier" - ], - "answer": 1 - }, - { - "id": 3, - "text": "Que représente le dossier / (barre oblique seule) sous Linux ?", - "choices": [ - "Le dossier personnel de l'utilisateur", - "Un dossier temporaire vide", - "La racine du système de fichiers (le point de départ de tout)", - "Le dossier des fichiers de configuration" - ], - "answer": 2 - }, - { - "id": 4, - "text": "Comment afficher les fichiers cachés dans Nemo (le gestionnaire de fichiers de Linux Mint) ?", - "choices": [ - "Clic droit → « Afficher les fichiers cachés »", - "Menu Affichage → « Afficher les fichiers cachés » ou raccourci Ctrl+H", - "Il n'est pas possible d'afficher les fichiers cachés", - "En tapant « hidden » dans la barre d'adresse" - ], - "answer": 1 - }, - { - "id": 5, - "text": "Sous Linux, par quel caractère commence le nom d'un fichier caché ?", - "choices": [ - "Un underscore _", - "Un point . (exemple : .bashrc)", - "Un tiret -", - "Un dièse #" - ], - "answer": 1 - }, - { - "id": 6, - "text": "Que fait le raccourci Ctrl+Z dans la plupart des applications ?", - "choices": [ - "Fermer l'application", - "Annuler la dernière action", - "Enregistrer le fichier", - "Couper la sélection" - ], - "answer": 1 - } - ] - }, - { - "id": "logiciels-debutant", - "title": "Installer des logiciels", - "description": "Gérez vos logiciels sous Linux Mint sans ligne de commande.", - "level": "Débutant", - "level_id": 2, - "members_only": false, - "duration_min": 4, - "icon": "📦", - "questions": [ - { - "id": 1, - "text": "Quel est l'outil graphique pour installer des logiciels sous Linux Mint ?", - "choices": [ - "Le Gestionnaire de logiciels", - "L'App Store", - "Le Centre de téléchargement Windows", - "Le Panneau de configuration" - ], - "answer": 0 - }, - { - "id": 2, - "text": "Qu'est-ce qu'un dépôt de logiciels (repository) sous Linux ?", - "choices": [ - "Un disque dur externe pour stocker des sauvegardes", - "Un serveur qui héberge des paquets logiciels prêts à installer", - "Un logiciel de gestion de fichiers", - "Une partition dédiée aux programmes" - ], - "answer": 1 - }, - { - "id": 3, - "text": "Pourquoi faut-il régulièrement mettre à jour son système Linux ?", - "choices": [ - "Pour améliorer les performances uniquement", - "Pour corriger les bugs et les failles de sécurité", - "Pour changer l'apparence du bureau", - "Les mises à jour ne sont pas nécessaires sous Linux" - ], - "answer": 1 - }, - { - "id": 4, - "text": "Qu'est-ce que LibreOffice ?", - "choices": [ - "Un système d'exploitation", - "Un navigateur web libre", - "Une suite bureautique libre, alternative à Microsoft Office", - "Un antivirus" - ], - "answer": 2 - }, - { - "id": 5, - "text": "Comment installe-t-on un logiciel en ligne de commande sous Linux Mint ?", - "choices": [ - "install ", - "sudo apt install ", - "get-app ", - "download " - ], - "answer": 1 - } - ] - }, - { - "id": "terminal-intermediaire", - "title": "Le terminal — premiers pas", - "description": "Découvrez la puissance de la ligne de commande Linux.", - "level": "Intermédiaire", - "level_id": 3, - "members_only": false, - "duration_min": 5, - "icon": "⌨️", - "questions": [ - { - "id": 1, - "text": "Quelle commande affiche le répertoire courant (là où vous êtes) ?", - "choices": ["ls", "cd", "pwd", "dir"], - "answer": 2 - }, - { - "id": 2, - "text": "Quelle commande liste les fichiers et dossiers du répertoire courant ?", - "choices": ["list", "ls", "dir", "files"], - "answer": 1 - }, - { - "id": 3, - "text": "Que fait la commande `cd ..` ?", - "choices": [ - "Revient au dossier personnel (~)", - "Monte d'un niveau dans l'arborescence (dossier parent)", - "Change le nom du dossier courant", - "Crée un nouveau dossier" - ], - "answer": 1 - }, - { - "id": 4, - "text": "Quelle commande crée un nouveau dossier nommé « projets » ?", - "choices": ["newdir projets", "create projets", "mkdir projets", "touch projets"], - "answer": 2 - }, - { - "id": 5, - "text": "Que fait la commande `man ls` ?", - "choices": [ - "Affiche la version de la commande ls", - "Exécute ls avec toutes les options disponibles", - "Affiche le manuel de la commande ls", - "Liste les fichiers en mode manuel" - ], - "answer": 2 - }, - { - "id": 6, - "text": "Que signifie le ~ (tilde) dans le prompt du terminal ?", - "choices": [ - "Le dossier racine /", - "Le dossier /tmp", - "Le dossier personnel de l'utilisateur (/home/utilisateur)", - "Le dernier dossier visité" - ], - "answer": 2 - } - ] - }, - { - "id": "permissions-intermediaire", - "title": "Les permissions Linux", - "description": "Comprenez qui peut lire, écrire et exécuter quoi.", - "level": "Intermédiaire", - "level_id": 3, - "members_only": false, - "duration_min": 5, - "icon": "🔐", - "questions": [ - { - "id": 1, - "text": "Que signifient les permissions `rwxr-xr--` sur un fichier ?", - "choices": [ - "Lecture seule pour tout le monde", - "Propriétaire : lire/écrire/exécuter | Groupe : lire/exécuter | Autres : lire seulement", - "Propriétaire : tout | Groupe : tout | Autres : rien", - "Ce format de permissions n'existe pas sous Linux" - ], - "answer": 1 - }, - { - "id": 2, - "text": "Quelle commande modifie les permissions d'un fichier ?", - "choices": ["perm", "chperm", "chmod", "access"], - "answer": 2 - }, - { - "id": 3, - "text": "Que fait `chmod 755 script.sh` ?", - "choices": [ - "Supprime tous les droits sur le fichier", - "Donne tous les droits au propriétaire, lecture+exécution au groupe et aux autres", - "Rend le fichier lisible uniquement par le propriétaire", - "Change le propriétaire du fichier" - ], - "answer": 1 - }, - { - "id": 4, - "text": "Quelle commande change le propriétaire d'un fichier ?", - "choices": ["owner", "chown", "chmod", "usermod"], - "answer": 1 - }, - { - "id": 5, - "text": "Que signifie la lettre `x` dans les permissions Linux ?", - "choices": [ - "Exclure (exclude)", - "eXtended (étendu)", - "Exécuter (execute)", - "eXport" - ], - "answer": 2 - } - ] - }, - { - "id": "git-avance", - "title": "Git — les bases", - "description": "Maîtrisez la gestion de versions avec Git.", - "level": "Avancé", - "level_id": 4, - "members_only": true, - "duration_min": 6, - "icon": "🌿", - "questions": [ - { - "id": 1, - "text": "Que fait `git init` dans un dossier ?", - "choices": [ - "Télécharge un dépôt distant", - "Initialise un nouveau dépôt Git local dans le dossier", - "Initialise la connexion avec GitHub/Gitea", - "Réinitialise toutes les modifications en cours" - ], - "answer": 1 - }, - { - "id": 2, - "text": "Quelle commande affiche l'état actuel du dépôt (fichiers modifiés, staged, etc.) ?", - "choices": ["git log", "git diff", "git status", "git show"], - "answer": 2 - }, - { - "id": 3, - "text": "Comment ajouter tous les fichiers modifiés à l'index (staging area) ?", - "choices": ["git commit -a", "git add .", "git stage all", "git push all"], - "answer": 1 - }, - { - "id": 4, - "text": "Que fait `git commit -m \"message\"` ?", - "choices": [ - "Envoie les modifications vers le dépôt distant", - "Crée un point de sauvegarde (commit) avec le message donné", - "Ajoute les fichiers à l'index", - "Crée une nouvelle branche nommée « message »" - ], - "answer": 1 - }, - { - "id": 5, - "text": "Quelle commande envoie les commits locaux vers le dépôt distant ?", - "choices": ["git send", "git upload", "git push", "git commit --remote"], - "answer": 2 - }, - { - "id": 6, - "text": "Que fait `git pull` ?", - "choices": [ - "Supprime les modifications locales non committées", - "Récupère et fusionne les modifications du dépôt distant", - "Crée une nouvelle branche à partir du distant", - "Affiche les derniers commits du dépôt distant" - ], - "answer": 1 - } - ] - }, - { - "id": "admin-avance", - "title": "Administration système", - "description": "Gérez votre système Linux comme un pro.", - "level": "Avancé", - "level_id": 4, - "members_only": true, - "duration_min": 6, - "icon": "⚙️", - "questions": [ - { - "id": 1, - "text": "Quelle commande met à jour la liste des paquets disponibles sous Debian/Ubuntu/Mint ?", - "choices": ["sudo apt upgrade", "sudo apt update", "sudo apt refresh", "sudo dpkg update"], - "answer": 1 - }, - { - "id": 2, - "text": "Quelle commande affiche les processus en cours d'exécution de façon interactive ?", - "choices": ["ps aux", "top ou htop", "process", "list-proc"], - "answer": 1 - }, - { - "id": 3, - "text": "Comment arrêter le service `nginx` avec systemd ?", - "choices": [ - "service nginx kill", - "nginx --stop", - "sudo systemctl stop nginx", - "sudo kill nginx" - ], - "answer": 2 - }, - { - "id": 4, - "text": "Que fait la commande `sudo` ?", - "choices": [ - "Exécute une commande avec les droits d'un autre utilisateur (par défaut root)", - "Sécurise une commande contre les erreurs", - "Vérifie si une commande existe sur le système", - "Exécute une commande en arrière-plan" - ], - "answer": 0 - }, - { - "id": 5, - "text": "Quelle commande affiche l'espace disque disponible sur les partitions montées ?", - "choices": ["diskspace", "du -h", "df -h", "free -h"], - "answer": 2 - }, - { - "id": 6, - "text": "Comment afficher les dernières lignes d'un fichier de log en temps réel ?", - "choices": [ - "cat /var/log/syslog", - "tail -f /var/log/syslog", - "watch /var/log/syslog", - "log -f /var/log/syslog" - ], - "answer": 1 - } - ] - }, - { - "id": "securite-expert", - "title": "Sécurité Linux", - "description": "Bonnes pratiques et outils de sécurité sous Linux.", - "level": "Expert", - "level_id": 5, - "members_only": true, - "duration_min": 7, - "icon": "🛡️", - "questions": [ - { - "id": 1, - "text": "Pourquoi ne faut-il pas utiliser le compte root au quotidien ?", - "choices": [ - "Root est plus lent que les autres utilisateurs", - "Une erreur ou un programme malveillant peut endommager tout le système sans restriction", - "Root ne peut pas accéder aux fichiers personnels", - "Il n'y a aucun risque particulier à utiliser root" - ], - "answer": 1 - }, - { - "id": 2, - "text": "Qu'est-ce qu'une clé SSH ?", - "choices": [ - "Un mot de passe chiffré pour les connexions web HTTPS", - "Une paire cryptographique (clé publique/privée) pour s'authentifier sans mot de passe", - "Un certificat SSL pour serveurs web", - "Un outil de chiffrement de disque comme LUKS" - ], - "answer": 1 - }, - { - "id": 3, - "text": "Quelle commande génère une paire de clés SSH de type Ed25519 ?", - "choices": [ - "ssh-create -t ed25519", - "openssl genkey ed25519", - "ssh-keygen -t ed25519", - "gpg --gen-key --type ed25519" - ], - "answer": 2 - }, - { - "id": 4, - "text": "Quel est le principe du moindre privilège (least privilege) ?", - "choices": [ - "Toujours utiliser le compte root pour les tâches système", - "Accorder à chaque processus et utilisateur uniquement les droits strictement nécessaires", - "Réduire les performances du système pour plus de sécurité", - "Interdire l'accès internet aux utilisateurs non-root" - ], - "answer": 1 - }, - { - "id": 5, - "text": "Quelle commande affiche les connexions réseau actives et les ports en écoute ?", - "choices": ["ifconfig -a", "ss -tulpn (ou netstat -tulpn)", "ping -l", "route -n"], - "answer": 1 - }, - { - "id": 6, - "text": "Qu'est-ce qu'un pare-feu (firewall) ?", - "choices": [ - "Un antivirus spécialisé pour les applications web", - "Un système qui filtre le trafic réseau entrant et sortant selon des règles", - "Un logiciel qui chiffre les applications installées", - "Un outil de sauvegarde automatique" - ], - "answer": 1 - } - ] - }, - { - "id": "bash-expert", - "title": "Bash scripting", - "description": "Automatisez vos tâches avec des scripts shell.", - "level": "Expert", - "level_id": 5, - "members_only": true, - "duration_min": 7, - "icon": "📜", - "questions": [ - { - "id": 1, - "text": "Quelle doit être la première ligne d'un script bash ?", - "choices": ["#!/usr/bin/python3", "#!/bin/bash", "#bash", "// bash script"], - "answer": 1 - }, - { - "id": 2, - "text": "Comment rendre un script bash exécutable ?", - "choices": ["bash +x script.sh", "chmod +x script.sh", "exec script.sh", "run script.sh"], - "answer": 1 - }, - { - "id": 3, - "text": "Que représente `$1` dans un script bash ?", - "choices": [ - "Le nom du script lui-même", - "La valeur de la variable nommée « 1 »", - "Le premier argument passé au script lors de son appel", - "Le code de retour de la dernière commande exécutée" - ], - "answer": 2 - }, - { - "id": 4, - "text": "Quelle est la différence entre `>` et `>>` en redirection ?", - "choices": [ - ">> est plus rapide que >", - ">> ajoute à la fin du fichier existant, > écrase le fichier", - ">> redirige les erreurs, > redirige la sortie standard", - "Aucune différence pratique entre les deux" - ], - "answer": 1 - }, - { - "id": 5, - "text": "Comment tester si le fichier `/etc/hosts` existe dans un script bash ?", - "choices": [ - "if exists \"/etc/hosts\"; then", - "if [ -f \"/etc/hosts\" ]; then", - "if file \"/etc/hosts\"; then", - "if check -f \"/etc/hosts\"; then" - ], - "answer": 1 - } - ] - } -] diff --git a/dynamic/db.py b/dynamic/db.py deleted file mode 100644 index 26216ea..0000000 --- a/dynamic/db.py +++ /dev/null @@ -1,61 +0,0 @@ -import sqlite3 -import os -from flask import g - - -def get_db(): - if 'db' not in g: - db_path = os.environ.get('DATABASE', 'scores.db') - os.makedirs(os.path.dirname(db_path), exist_ok=True) - g.db = sqlite3.connect(db_path) - g.db.row_factory = sqlite3.Row - return g.db - - -def close_db(e=None): - db = g.pop('db', None) - if db is not None: - db.close() - - -def init_db(): - db = get_db() - db.executescript(''' - CREATE TABLE IF NOT EXISTS quiz_results ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_sub TEXT NOT NULL, - user_name TEXT NOT NULL DEFAULT '', - quiz_id TEXT NOT NULL, - score INTEGER NOT NULL, - total INTEGER NOT NULL, - completed_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - CREATE INDEX IF NOT EXISTS idx_results_user ON quiz_results(user_sub); - ''') - db.commit() - - -def save_result(user_sub, user_name, quiz_id, score, total): - db = get_db() - db.execute( - 'INSERT INTO quiz_results (user_sub, user_name, quiz_id, score, total) VALUES (?,?,?,?,?)', - (user_sub, user_name, quiz_id, score, total), - ) - db.commit() - - -def get_user_results(user_sub): - db = get_db() - return db.execute( - 'SELECT * FROM quiz_results WHERE user_sub=? ORDER BY completed_at DESC', - (user_sub,), - ).fetchall() - - -def get_best_score(user_sub, quiz_id): - db = get_db() - row = db.execute( - 'SELECT MAX(score) as best, total FROM quiz_results WHERE user_sub=? AND quiz_id=?', - (user_sub, quiz_id), - ).fetchone() - return row if row and row['best'] is not None else None diff --git a/dynamic/quiz.py b/dynamic/quiz.py deleted file mode 100644 index 0374a25..0000000 --- a/dynamic/quiz.py +++ /dev/null @@ -1,31 +0,0 @@ -import json -from pathlib import Path - -_QUIZZES = None - -LEVEL_ORDER = {1: "Découverte", 2: "Débutant", 3: "Intermédiaire", 4: "Avancé", 5: "Expert"} -LEVEL_COLOR = {1: "green", 2: "teal", 3: "orange", 4: "red", 5: "purple"} - - -def load_quizzes(): - global _QUIZZES - if _QUIZZES is None: - path = Path(__file__).parent / "data" / "quizzes.json" - _QUIZZES = json.loads(path.read_text(encoding="utf-8")) - return _QUIZZES - - -def get_all(): - return load_quizzes() - - -def get_by_id(quiz_id): - return next((q for q in load_quizzes() if q["id"] == quiz_id), None) - - -def get_public(): - return [q for q in load_quizzes() if not q["members_only"]] - - -def get_members(): - return [q for q in load_quizzes() if q["members_only"]] diff --git a/dynamic/requirements.txt b/dynamic/requirements.txt deleted file mode 100644 index ad7a527..0000000 --- a/dynamic/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -flask>=3.0 -authlib>=1.3 -requests>=2.31 diff --git a/dynamic/routes/__init__.py b/dynamic/routes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dynamic/routes/auth.py b/dynamic/routes/auth.py deleted file mode 100644 index 5f523c1..0000000 --- a/dynamic/routes/auth.py +++ /dev/null @@ -1,34 +0,0 @@ -from flask import Blueprint, redirect, url_for, session, current_app, request - -auth_bp = Blueprint('auth', __name__) - - -@auth_bp.route('/login') -def login(): - redirect_uri = url_for('auth.callback', _external=True) - return current_app.extensions['oauth'].alpid.authorize_redirect(redirect_uri) - - -@auth_bp.route('/callback') -def callback(): - token = current_app.extensions['oauth'].alpid.authorize_access_token() - userinfo = token.get('userinfo') or \ - current_app.extensions['oauth'].alpid.userinfo(token=token) - session['user'] = { - 'sub': userinfo['sub'], - 'name': userinfo.get('name') or userinfo.get('preferred_username', 'Membre'), - 'username': userinfo.get('preferred_username', ''), - 'email': userinfo.get('email', ''), - } - return redirect(session.pop('next_url', url_for('public.index'))) - - -@auth_bp.route('/logout') -def logout(): - session.clear() - # Déconnexion côté AlpID si end_session_endpoint disponible - end_session = current_app.config.get('ALPID_END_SESSION_URL') - if end_session: - post_logout = url_for('public.index', _external=True) - return redirect(f"{end_session}?post_logout_redirect_uri={post_logout}") - return redirect(url_for('public.index')) diff --git a/dynamic/routes/protected.py b/dynamic/routes/protected.py deleted file mode 100644 index 84b561b..0000000 --- a/dynamic/routes/protected.py +++ /dev/null @@ -1,37 +0,0 @@ -from flask import Blueprint, render_template - -from auth_utils import current_user, login_required -from quiz import get_all -from db import get_user_results, get_best_score - -protected_bp = Blueprint('protected', __name__) - - -@protected_bp.route('/profil/') -@login_required -def profil(): - user = current_user() - results = get_user_results(user['sub']) - quizzes = get_all() - - # Calcule le meilleur score par quiz - best_scores = {} - for q in quizzes: - row = get_best_score(user['sub'], q['id']) - if row: - best_scores[q['id']] = {'score': row['best'], 'total': row['total']} - - # Statistiques globales - completed_ids = {r['quiz_id'] for r in results} - stats = { - 'quizzes_done': len(completed_ids), - 'quizzes_total': len(quizzes), - 'attempts': len(results), - } - - return render_template('profil/index.html', - user=user, - results=results, - quizzes={q['id']: q for q in quizzes}, - best_scores=best_scores, - stats=stats) diff --git a/dynamic/routes/public.py b/dynamic/routes/public.py deleted file mode 100644 index 03d7b54..0000000 --- a/dynamic/routes/public.py +++ /dev/null @@ -1,85 +0,0 @@ -import json -from flask import Blueprint, render_template, request, redirect, url_for, session, abort - -from auth_utils import current_user, login_required -from quiz import get_all, get_by_id -from db import save_result - -public_bp = Blueprint('public', __name__) - - -@public_bp.route('/') -def index(): - quizzes = get_all() - return render_template('index.html', quizzes=quizzes, user=current_user()) - - -@public_bp.route('/quiz/') -def quiz_list(): - quizzes = get_all() - return render_template('quiz/list.html', quizzes=quizzes, user=current_user()) - - -@public_bp.route('/quiz//') -def quiz_intro(quiz_id): - quiz = get_by_id(quiz_id) - if not quiz: - abort(404) - return render_template('quiz/intro.html', quiz=quiz, user=current_user()) - - -@public_bp.route('/quiz//jouer', methods=['GET', 'POST']) -def quiz_play(quiz_id): - quiz = get_by_id(quiz_id) - if not quiz: - abort(404) - - if quiz['members_only'] and not current_user(): - session['next_url'] = url_for('public.quiz_play', quiz_id=quiz_id) - return redirect(url_for('auth.login')) - - if request.method == 'POST': - answers = {} - for i in range(len(quiz['questions'])): - val = request.form.get(f'q{i}') - answers[i] = int(val) if val is not None else -1 - - score = sum( - 1 for i, q in enumerate(quiz['questions']) - if answers.get(i) == q['answer'] - ) - total = len(quiz['questions']) - - if current_user(): - save_result( - current_user()['sub'], - current_user()['name'], - quiz_id, score, total, - ) - - # Stocke résultat et détail en session pour la page résultat - session['last_result'] = { - 'quiz_id': quiz_id, - 'score': score, - 'total': total, - 'answers': answers, - } - return redirect(url_for('public.quiz_result', quiz_id=quiz_id)) - - return render_template('quiz/play.html', - quiz=quiz, - quiz_json=json.dumps(quiz), - user=current_user()) - - -@public_bp.route('/quiz//resultat') -def quiz_result(quiz_id): - quiz = get_by_id(quiz_id) - result = session.pop('last_result', None) - if not quiz or not result or result['quiz_id'] != quiz_id: - return redirect(url_for('public.quiz_intro', quiz_id=quiz_id)) - - return render_template('quiz/result.html', - quiz=quiz, - result=result, - user=current_user()) diff --git a/dynamic/static/quiz.js b/dynamic/static/quiz.js deleted file mode 100644 index ddc4980..0000000 --- a/dynamic/static/quiz.js +++ /dev/null @@ -1,49 +0,0 @@ -/* Quiz navigation — vanilla JS, aucune dépendance */ -(function () { - const quiz = JSON.parse(document.getElementById('quiz-data').textContent); - const total = quiz.questions.length; - let current = 0; - - function showQuestion(index) { - document.querySelectorAll('.question-block').forEach(function (el) { - el.hidden = parseInt(el.dataset.index) !== index; - }); - - document.getElementById('progress-text').textContent = - (index + 1) + ' / ' + total; - document.getElementById('progress-bar').style.width = - Math.round((index / total) * 100) + '%'; - - document.getElementById('btn-prev').hidden = index === 0; - document.getElementById('btn-next').hidden = index === total - 1; - document.getElementById('btn-submit').hidden = index !== total - 1; - } - - window.navigate = function (delta) { - var next = current + delta; - if (next < 0 || next >= total) return; - current = next; - showQuestion(current); - }; - - window.onChoice = function (questionIndex, choiceIndex) { - /* Mise à jour visuelle */ - document.querySelectorAll('.question-block[data-index="' + questionIndex + '"] .choice-label') - .forEach(function (lbl, j) { - lbl.classList.toggle('selected', j === choiceIndex); - }); - - /* Le radio button natif sert de valeur soumise avec le formulaire. - On synchronise aussi un hidden input de secours. */ - var hidden = document.getElementById('ans-' + questionIndex); - if (hidden) hidden.value = choiceIndex; - - /* Avancer automatiquement sauf sur la dernière question */ - if (questionIndex < total - 1) { - setTimeout(function () { navigate(1); }, 350); - } - }; - - /* Init */ - showQuestion(0); -})(); diff --git a/dynamic/static/style.css b/dynamic/static/style.css deleted file mode 100644 index 0ecc9f7..0000000 --- a/dynamic/static/style.css +++ /dev/null @@ -1,219 +0,0 @@ -/* ── Variables ────────────────────────────────────────────────── */ -:root { - --blue: #1a6bbf; - --blue-dark: #0f4e8f; - --blue-light: #e8f1fb; - --orange: #e8820c; - --bg: #f3f6fb; - --text: #1a1a2e; - --text-muted: #666; - --card-shadow: 0 2px 12px rgba(26,107,191,.1); - --radius: 10px; -} - -/* ── Reset / base ─────────────────────────────────────────────── */ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -body { font-family: 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; } -a { color: var(--blue); text-decoration: none; } -a:hover { text-decoration: underline; } -main { flex: 1; max-width: 1000px; margin: 0 auto; padding: 2rem 1.5rem; width: 100%; } -h1, h2, h3 { line-height: 1.2; } - -/* ── Header ───────────────────────────────────────────────────── */ -header { background: var(--blue-dark); color: #fff; } -.header-inner { max-width: 1000px; margin: 0 auto; padding: .8rem 1.5rem; display: flex; align-items: center; gap: 1.5rem; } -.brand { display: flex; align-items: center; gap: .6rem; color: #fff; font-size: 1.15rem; font-weight: 400; flex-shrink: 0; } -.brand:hover { text-decoration: none; opacity: .9; } -.brand strong { font-weight: 800; } -.brand-sub { font-weight: 300; font-size: .9em; opacity: .8; } -.brand-logo { border-radius: 6px; } -.main-nav { display: flex; align-items: center; gap: .3rem; margin-left: auto; flex-wrap: wrap; } -.main-nav a { color: rgba(255,255,255,.85); font-size: .88rem; padding: .4rem .8rem; border-radius: 5px; transition: background .15s; } -.main-nav a:hover { background: rgba(255,255,255,.15); color: #fff; text-decoration: none; } -.btn-nav { background: var(--blue); color: #fff !important; border-radius: 5px; padding: .4rem .9rem !important; } -.btn-nav-outline { border: 1px solid rgba(255,255,255,.5); border-radius: 5px; padding: .35rem .8rem !important; } -.nav-toggle { display: none; background: none; border: none; color: #fff; font-size: 1.4rem; cursor: pointer; margin-left: auto; } - -/* ── Footer ───────────────────────────────────────────────────── */ -footer { background: var(--blue-dark); color: rgba(255,255,255,.7); padding: 1rem 1.5rem; font-size: .85rem; } -.footer-inner { max-width: 1000px; margin: 0 auto; display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap; } -.footer-links { display: flex; gap: 1rem; } -.footer-links a { color: rgba(255,255,255,.75); } -.footer-links a:hover { color: #fff; } - -/* ── Buttons ──────────────────────────────────────────────────── */ -.btn { display: inline-block; padding: .6rem 1.4rem; border-radius: 6px; font-size: .95rem; font-weight: 600; cursor: pointer; border: none; transition: all .15s; text-decoration: none !important; } -.btn-primary { background: var(--blue); color: #fff; } -.btn-primary:hover { background: var(--blue-dark); } -.btn-outline { background: transparent; color: var(--blue); border: 2px solid var(--blue); } -.btn-outline:hover { background: var(--blue-light); } -.btn-success { background: #1e8a3e; color: #fff; } -.btn-success:hover { background: #166b30; } -.btn-lg { padding: .8rem 2rem; font-size: 1.05rem; } -.btn-sm { padding: .35rem .8rem; font-size: .84rem; } - -/* ── Badges ───────────────────────────────────────────────────── */ -.badge { display: inline-block; padding: .2rem .6rem; border-radius: 20px; font-size: .78rem; font-weight: 700; } -.badge.public { background: #d4f0dc; color: #1a6b35; } -.badge.members { background: #fde8cc; color: #a35800; } -.lock-badge { font-size: .8rem; color: #a35800; } - -.level-badge { display: inline-block; padding: .15rem .55rem; border-radius: 20px; font-size: .78rem; font-weight: 700; } -.level-badge-1 { background: #d4f0dc; color: #1a6b35; } -.level-badge-2 { background: #d0edf5; color: #0d6779; } -.level-badge-3 { background: #fff3d4; color: #8a5c00; } -.level-badge-4 { background: #ffe0d0; color: #8a2800; } -.level-badge-5 { background: #ead4f5; color: #5e0a8a; } - -/* ── Flash messages ───────────────────────────────────────────── */ -.flash { padding: .8rem 1.2rem; border-radius: 6px; margin-bottom: 1rem; } -.flash-error { background: #fde8e8; color: #8a0000; border-left: 4px solid #c0392b; } -.flash-success { background: #d4f0dc; color: #1a6b35; border-left: 4px solid #27ae60; } - -/* ── Section titles ───────────────────────────────────────────── */ -.section-title { font-size: 1.1rem; font-weight: 700; color: var(--blue-dark); margin-bottom: 1.2rem; display: flex; align-items: center; gap: .6rem; } -.section-title::after { content: ''; flex: 1; height: 2px; background: var(--blue-light); } - -/* ── Hero ─────────────────────────────────────────────────────── */ -.hero { background: linear-gradient(135deg, var(--blue-dark) 0%, var(--blue) 100%); color: #fff; border-radius: var(--radius); padding: 3rem 2rem; text-align: center; margin-bottom: 2.5rem; } -.hero h1 { font-size: 2.2rem; font-weight: 800; } -.hero-sub { font-weight: 300; opacity: .8; } -.hero-desc { margin: 1rem auto; max-width: 560px; opacity: .88; line-height: 1.6; font-size: 1.05rem; } -.hero-actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-top: 1.5rem; } - -/* ── Levels grid ──────────────────────────────────────────────── */ -.levels-section { margin-bottom: 2.5rem; } -.levels-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1rem; } -.level-card { background: #fff; border-radius: var(--radius); padding: 1.2rem 1rem; text-align: center; box-shadow: var(--card-shadow); } -.level-card p { font-size: .82rem; color: var(--text-muted); margin: .3rem 0 .5rem; } -.level-icon { font-size: 1.8rem; display: block; margin-bottom: .4rem; } - -/* ── Quiz grid / cards ────────────────────────────────────────── */ -.quiz-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 1rem; } -.quiz-card { display: flex; gap: 1rem; background: #fff; border-radius: var(--radius); padding: 1.2rem; box-shadow: var(--card-shadow); color: var(--text); transition: transform .15s, box-shadow .15s; text-decoration: none !important; border: 2px solid transparent; } -.quiz-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(26,107,191,.15); border-color: var(--blue-light); } -.quiz-card--members { border-left: 3px solid var(--orange); } -.quiz-icon { font-size: 2rem; flex-shrink: 0; align-self: flex-start; } -.quiz-card-body h3 { font-size: 1rem; margin-bottom: .3rem; } -.quiz-card-body p { font-size: .84rem; color: var(--text-muted); line-height: 1.4; margin-bottom: .5rem; } -.quiz-meta { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; } -.quiz-duration, .quiz-count { font-size: .78rem; color: var(--text-muted); } - -/* ── Members CTA ──────────────────────────────────────────────── */ -.members-cta { display: flex; align-items: center; gap: 1.5rem; background: #fff8f0; border: 2px solid #f5d0a0; border-radius: var(--radius); padding: 1.4rem 1.8rem; margin-top: 2rem; flex-wrap: wrap; } -.cta-icon { font-size: 2rem; flex-shrink: 0; } -.members-cta strong { display: block; margin-bottom: .2rem; } -.members-cta p { font-size: .9rem; color: var(--text-muted); margin: 0; } -.members-cta .btn { margin-left: auto; } - -/* ── Level sections in list ───────────────────────────────────── */ -.level-section { margin-bottom: 2.5rem; } -.level-heading { font-size: 1.2rem; font-weight: 700; padding-bottom: .6rem; border-bottom: 2px solid var(--blue-light); margin-bottom: 1.2rem; display: flex; align-items: center; gap: .7rem; } -.level-heading-4, .level-heading-5 { border-bottom-color: #f5d0a0; } - -/* ── Quiz intro ───────────────────────────────────────────────── */ -.quiz-intro-wrap { display: flex; justify-content: center; padding: 2rem 0; } -.quiz-intro-card { background: #fff; border-radius: var(--radius); box-shadow: var(--card-shadow); padding: 2.5rem 2rem; max-width: 560px; width: 100%; text-align: center; } -.quiz-intro-icon { font-size: 3.5rem; display: block; margin-bottom: .8rem; } -.quiz-intro-card h1 { font-size: 1.6rem; margin-bottom: .6rem; } -.quiz-intro-desc { color: var(--text-muted); line-height: 1.6; margin-bottom: 1.5rem; } -.quiz-intro-meta { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; background: var(--bg); border-radius: 8px; padding: 1.2rem; margin-bottom: 1.8rem; text-align: left; } -.meta-item { display: flex; flex-direction: column; gap: .2rem; } -.meta-label { font-size: .78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; } -.intro-lock-notice { background: #fff8f0; border: 1px solid #f5d0a0; border-radius: 8px; padding: 1.2rem; margin-bottom: 1.5rem; } -.intro-lock-notice p { color: var(--text-muted); margin-bottom: 1rem; } -.intro-note { font-size: .85rem; color: var(--text-muted); margin-top: .8rem; } - -/* ── Quiz play ────────────────────────────────────────────────── */ -.quiz-play-wrap { max-width: 680px; margin: 0 auto; } -.quiz-play-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.8rem; flex-wrap: wrap; } -.quiz-play-title { font-weight: 700; font-size: 1.05rem; flex: 1; } -.progress-bar-wrap { flex: 1; height: 8px; background: var(--blue-light); border-radius: 4px; overflow: hidden; min-width: 100px; } -.progress-bar { height: 100%; background: var(--blue); transition: width .3s; border-radius: 4px; } -.progress-text { font-size: .85rem; color: var(--text-muted); white-space: nowrap; } - -.question-block { background: #fff; border-radius: var(--radius); box-shadow: var(--card-shadow); padding: 2rem 1.8rem; } -.question-num { font-size: .82rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: .06em; margin-bottom: .6rem; } -.question-text { font-size: 1.2rem; line-height: 1.5; margin-bottom: 1.5rem; } -.choices { display: flex; flex-direction: column; gap: .7rem; } -.choice-label { display: flex; align-items: center; gap: .9rem; padding: .9rem 1.1rem; border: 2px solid var(--blue-light); border-radius: 8px; cursor: pointer; transition: all .15s; } -.choice-label:hover { border-color: var(--blue); background: var(--blue-light); } -.choice-label.selected { border-color: var(--blue); background: var(--blue-light); } -.choice-label input[type=radio] { position: absolute; opacity: 0; width: 0; } -.choice-letter { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: var(--blue-light); color: var(--blue); font-weight: 700; font-size: .85rem; flex-shrink: 0; transition: all .15s; } -.choice-label.selected .choice-letter { background: var(--blue); color: #fff; } -.choice-text { font-size: .95rem; } - -.quiz-nav { display: flex; gap: 1rem; justify-content: space-between; margin-top: 1.5rem; flex-wrap: wrap; } - -/* ── Quiz result ──────────────────────────────────────────────── */ -.result-wrap { display: flex; justify-content: center; } -.result-card { background: #fff; border-radius: var(--radius); box-shadow: var(--card-shadow); padding: 2.5rem 2rem; max-width: 680px; width: 100%; } -.result-icon { font-size: 3.5rem; display: block; text-align: center; margin-bottom: .8rem; } -.result-card h1 { text-align: center; font-size: 1.5rem; margin-bottom: 1.2rem; } -.score-display { display: flex; align-items: baseline; justify-content: center; gap: .5rem; margin-bottom: .8rem; } -.score-num { font-size: 4rem; font-weight: 800; color: var(--blue); line-height: 1; } -.score-sep { font-size: 2rem; color: var(--text-muted); } -.score-total { font-size: 2rem; color: var(--text-muted); } -.score-bar-wrap { height: 12px; background: var(--blue-light); border-radius: 6px; overflow: hidden; margin-bottom: .8rem; } -.score-bar { height: 100%; border-radius: 6px; transition: width .5s ease; } -.score-bar-great { background: #27ae60; } -.score-bar-good { background: var(--blue); } -.score-bar-ok { background: var(--orange); } -.score-bar-low { background: #e74c3c; } -.score-comment { text-align: center; font-size: 1.05rem; margin-bottom: .6rem; font-weight: 600; } -.result-saved { text-align: center; font-size: .9rem; color: var(--text-muted); margin-bottom: 1.8rem; } - -.detail-title { font-size: 1.05rem; margin-bottom: 1rem; padding-top: 1rem; border-top: 2px solid var(--blue-light); } -.answer-review { display: flex; flex-direction: column; gap: .7rem; margin-bottom: 1.8rem; } -.answer-item { padding: .9rem 1.1rem; border-radius: 8px; border-left: 4px solid; } -.answer-correct { background: #f0fdf4; border-color: #27ae60; } -.answer-wrong { background: #fff5f5; border-color: #e74c3c; } -.answer-q { font-weight: 600; margin-bottom: .3rem; font-size: .93rem; } -.answer-given { font-size: .9rem; } -.answer-correct-text { font-size: .88rem; color: #1a6b35; margin-top: .2rem; } -.tag-correct { background: #d4f0dc; color: #1a6b35; padding: .1rem .5rem; border-radius: 4px; font-size: .8rem; font-weight: 700; margin-left: .4rem; } -.tag-wrong { background: #fde8e8; color: #8a0000; padding: .1rem .5rem; border-radius: 4px; font-size: .8rem; font-weight: 700; margin-left: .4rem; } -.result-actions { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; } - -/* ── Profile ──────────────────────────────────────────────────── */ -.page-header { margin-bottom: 2rem; } -.page-header h1 { font-size: 1.8rem; } -.text-muted { color: var(--text-muted); } -.profil-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2.5rem; } -.stat-card { background: #fff; border-radius: var(--radius); padding: 1.4rem 1rem; text-align: center; box-shadow: var(--card-shadow); } -.stat-num { display: block; font-size: 2.2rem; font-weight: 800; color: var(--blue); } -.stat-label { font-size: .85rem; color: var(--text-muted); } -.scores-grid { display: flex; flex-direction: column; gap: .8rem; } -.score-item { display: flex; align-items: center; gap: 1rem; background: #fff; border-radius: var(--radius); padding: 1rem 1.2rem; box-shadow: var(--card-shadow); } -.score-item-icon { font-size: 1.8rem; flex-shrink: 0; } -.score-item-body { flex: 1; } -.score-item-body strong { display: block; margin-bottom: .2rem; } -.mini-score-bar-wrap { height: 6px; background: var(--blue-light); border-radius: 3px; overflow: hidden; margin: .3rem 0; } -.mini-score-bar { height: 100%; border-radius: 3px; } -.mini-score-bar.great { background: #27ae60; } -.mini-score-bar.good { background: var(--blue); } -.mini-score-bar.ok { background: var(--orange); } -.mini-score-text { font-size: .82rem; color: var(--text-muted); } -.history-table-wrap { overflow-x: auto; } -.history-table { width: 100%; border-collapse: collapse; background: #fff; border-radius: var(--radius); overflow: hidden; box-shadow: var(--card-shadow); } -.history-table th { background: var(--blue); color: #fff; padding: .7rem 1rem; text-align: left; font-size: .9rem; } -.history-table td { padding: .6rem 1rem; border-bottom: 1px solid var(--blue-light); font-size: .9rem; } -.history-table tr:last-child td { border-bottom: none; } -.score-great { color: #1a6b35; font-weight: 700; } -.score-good { color: var(--blue); font-weight: 700; } -.score-ok { color: #a35800; font-weight: 700; } - -/* ── Responsive ───────────────────────────────────────────────── */ -@media (max-width: 600px) { - .hero h1 { font-size: 1.6rem; } - .hero-actions { flex-direction: column; } - .levels-grid { grid-template-columns: repeat(2, 1fr); } - .quiz-grid { grid-template-columns: 1fr; } - .profil-stats { grid-template-columns: 1fr; } - .nav-toggle { display: block; } - .main-nav { display: none; flex-direction: column; width: 100%; background: var(--blue-dark); padding: .5rem 1.5rem 1rem; } - header.nav-open .main-nav { display: flex; } - .main-nav a { padding: .6rem .8rem; } - .members-cta .btn { margin-left: 0; width: 100%; text-align: center; } -} diff --git a/dynamic/templates/base.html b/dynamic/templates/base.html deleted file mode 100644 index 17bb1ac..0000000 --- a/dynamic/templates/base.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - {% block title %}Quiz Linux{% endblock %} — Alpinux - - - - - - {% block head %}{% endblock %} - - - -
-
- - - Alpinux Quiz - - - -
-
- -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% for category, message in messages %} -
{{ message }}
- {% endfor %} - {% endwith %} - {% block content %}{% endblock %} -
- - - -{% block scripts %}{% endblock %} - - diff --git a/dynamic/templates/index.html b/dynamic/templates/index.html deleted file mode 100644 index 0b7333e..0000000 --- a/dynamic/templates/index.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "base.html" %} -{% block title %}Accueil{% endblock %} - -{% block content %} -
-

Quiz Linux par Alpinux

-

- Testez vos connaissances sur Linux — du premier clic sur le bureau - jusqu'aux scripts Bash et la sécurité système. -

- -
- -
-

Cinq niveaux progressifs

-
-
- 🌱 - Découverte -

Bureau, internet, navigateur web

- Public -
-
- 📘 - Débutant -

Fichiers, logiciels, Linux Mint

- Public -
-
- ⚡ - Intermédiaire -

Terminal, permissions

- Public -
-
- 🔧 - Avancé -

Git, administration système

- Membres -
-
- 🏆 - Expert -

Sécurité, Bash scripting

- Membres -
-
-
- -
-

Quiz disponibles

- - {% if quizzes|length > 6 %} - - {% endif %} -
-{% endblock %} diff --git a/dynamic/templates/profil/index.html b/dynamic/templates/profil/index.html deleted file mode 100644 index 8379c90..0000000 --- a/dynamic/templates/profil/index.html +++ /dev/null @@ -1,83 +0,0 @@ -{% extends "base.html" %} -{% block title %}Mon profil{% endblock %} - -{% block content %} - - -
-
- {{ stats.quizzes_done }} - quiz terminés -
-
- {{ stats.quizzes_total }} - quiz disponibles -
-
- {{ stats.attempts }} - tentatives au total -
-
- -

Mes meilleurs scores

-{% if best_scores %} -
- {% for quiz_id, q in quizzes.items() %} - {% if quiz_id in best_scores %} - {% set bs = best_scores[quiz_id] %} - {% set pct = (bs.score / bs.total * 100) | int %} -
- {{ q.icon }} -
- {{ q.title }} - {{ q.level }} -
-
-
- {{ bs.score }} / {{ bs.total }} ({{ pct }}%) -
- Rejouer -
- {% endif %} - {% endfor %} -
-{% else %} -

Vous n'avez pas encore complété de quiz. Commencez maintenant !

-{% endif %} - -

Historique des tentatives

-{% if results %} -
- - - - - - {% for r in results %} - - - - - - {% endfor %} - -
QuizScoreDate
- {% if r['quiz_id'] in quizzes %} - {{ quizzes[r['quiz_id']].icon }} {{ quizzes[r['quiz_id']].title }} - {% else %} - {{ r['quiz_id'] }} - {% endif %} - - - {{ r['score'] }} / {{ r['total'] }} - - {{ r['completed_at'][:16] }}
-
-{% else %} -

Aucune tentative enregistrée.

-{% endif %} -{% endblock %} diff --git a/dynamic/templates/quiz/intro.html b/dynamic/templates/quiz/intro.html deleted file mode 100644 index e3fcd78..0000000 --- a/dynamic/templates/quiz/intro.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ quiz.title }}{% endblock %} -{% block description %}{{ quiz.description }} Niveau {{ quiz.level }} — {{ quiz.questions|length }} questions.{% endblock %} - -{% block content %} -
-
- {{ quiz.icon }} -

{{ quiz.title }}

-

{{ quiz.description }}

- -
-
- Niveau - {{ quiz.level }} -
-
- Questions - {{ quiz.questions | length }} -
-
- Durée estimée - {{ quiz.duration_min }} min -
-
- Accès - {% if quiz.members_only %} - 🔒 Membres - {% else %} - Public - {% endif %} -
-
- - {% if quiz.members_only and not user %} -
-

Ce quiz est réservé aux membres Alpinux.
- Connectez-vous avec votre compte AlpID pour y accéder.

- Se connecter avec AlpID -
- {% else %} - - Commencer le quiz → - - {% if user %} -

Votre score sera enregistré dans votre profil.

- {% else %} -

- Connectez-vous pour enregistrer votre score. -

- {% endif %} - {% endif %} -
-
-{% endblock %} diff --git a/dynamic/templates/quiz/list.html b/dynamic/templates/quiz/list.html deleted file mode 100644 index a0ddd39..0000000 --- a/dynamic/templates/quiz/list.html +++ /dev/null @@ -1,64 +0,0 @@ -{% extends "base.html" %} -{% block title %}Tous les quiz{% endblock %} -{% block description %}10 quiz Linux de tous niveaux — découverte, débutant, intermédiaire, avancé, expert.{% endblock %} - -{% block content %} - - -{% set levels = [ - (1, "🌱", "Découverte", "public"), - (2, "📘", "Débutant", "public"), - (3, "⚡", "Intermédiaire", "public"), - (4, "🔧", "Avancé", "members"), - (5, "🏆", "Expert", "members"), -] %} - -{% for level_id, icon, label, access in levels %} - {% set level_quizzes = quizzes | selectattr("level_id", "equalto", level_id) | list %} - {% if level_quizzes %} -
-

- {{ icon }} Niveau {{ label }} - {% if access == "members" %} - Membres AlpID - {% else %} - Public - {% endif %} -

- -
- {% endif %} -{% endfor %} - -{% if not user %} -
- 🔒 -
- Quiz Avancé et Expert réservés aux membres -

Créez un compte AlpID pour accéder aux 4 quiz avancés.

-
- Se connecter -
-{% endif %} -{% endblock %} diff --git a/dynamic/templates/quiz/play.html b/dynamic/templates/quiz/play.html deleted file mode 100644 index 5405a4c..0000000 --- a/dynamic/templates/quiz/play.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ quiz.title }}{% endblock %} - -{% block content %} -
-
- {{ quiz.icon }} {{ quiz.title }} -
-
-
- 1 / {{ quiz.questions|length }} -
- -
- {% for i, q in quiz.questions | enumerate %} -
0 %}hidden{% endif %}> -

Question {{ i + 1 }} / {{ quiz.questions|length }}

-

{{ q.text }}

-
- {% for j, choice in q.choices | enumerate %} - - {% endfor %} -
- -
- {% endfor %} - -
- - - -
-
-
- - -{% endblock %} - -{% block scripts %} - -{% endblock %} diff --git a/dynamic/templates/quiz/result.html b/dynamic/templates/quiz/result.html deleted file mode 100644 index 4385e3a..0000000 --- a/dynamic/templates/quiz/result.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "base.html" %} -{% block title %}Résultat — {{ quiz.title }}{% endblock %} - -{% block content %} -{% set pct = (result.score / result.total * 100) | int %} -
-
- - {% if pct >= 80 %}🏆 - {% elif pct >= 60 %}👍 - {% elif pct >= 40 %}📚 - {% else %}💪{% endif %} - - -

{{ quiz.title }}

- -
- {{ result.score }} - / - {{ result.total }} -
- -
-
-
- -

- {% if pct == 100 %}Score parfait ! 🎉 - {% elif pct >= 80 %}Excellent résultat ! - {% elif pct >= 60 %}Bon travail, continuez ! - {% elif pct >= 40 %}C'est un début, réessayez ! - {% else %}Pas de souci, la pratique fait le maître ! - {% endif %} -

- - {% if user %} -

✓ Résultat enregistré dans votre profil.

- {% else %} -

- Connectez-vous pour enregistrer vos scores. -

- {% endif %} - -

Détail des réponses

-
- {% for i, q in quiz.questions | enumerate %} - {% set user_ans = result.answers.get(i | string, result.answers.get(i, -1)) %} - {% set correct = (user_ans == q.answer) %} -
-

{{ i + 1 }}. {{ q.text }}

-

- Votre réponse : - {% if user_ans >= 0 %}{{ q.choices[user_ans] }}{% else %}(sans réponse){% endif %} - {% if correct %}✓ Correct - {% else %}✗{% endif %} -

- {% if not correct %} -

- Bonne réponse : {{ q.choices[q.answer] }} -

- {% endif %} -
- {% endfor %} -
- - -
-
-{% endblock %} diff --git a/feedback/README.md b/feedback/README.md deleted file mode 100644 index b738924..0000000 --- a/feedback/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# feedback.alpinux.org - -Formulaire de retours et suggestions pour l'association Alpinux. - -## Statut - -À construire. Ce sous-domaine n'est pas encore développé. - -## Idées - -- Formulaire simple (Flask ou statique) pour collecter les retours des participants aux événements -- Intégration AlpID optionnelle (retours anonymes possibles) -- Stockage SQLite ou envoi par e-mail vers une liste de l'association - -## Structure prévue - -``` -feedback/ -├── app.py # Flask app (à créer) -├── templates/ -├── static/ -├── requirements.txt -├── .env.example -├── feedback.alpinux.org.vhost.conf -└── feedback.alpinux.org.service -``` diff --git a/home/index.html b/home/index.html deleted file mode 100644 index 3f13ae6..0000000 --- a/home/index.html +++ /dev/null @@ -1,1229 +0,0 @@ - - - - - - - - Alpinux — Le LUG de Savoie - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- Logo Alpinux — LUG de Savoie -
-
- Alpinux -
-
le LUG de Savoie
-
-
-

- Association loi 1901 dédiée à Linux et aux logiciels libres en Savoie. - Réunions tous les 1er et 3e jeudis du mois à la Dynamo Chambéry. -

-
- - - -
- -

Agenda

- -
- -
-
- -
- -
-
-
- - -
- -
- -

Prochains rendez-vous

-
-
- -
- - - - - -

Nos services

- - -
- -
-

© Alpinux — Association loi 1901  ·  Chambéry, Savoie

-

- À propos  ·  - FAQ  ·  - Adhérer -

-
- - - - - - - diff --git a/home/robots.txt b/home/robots.txt deleted file mode 100644 index 299233e..0000000 --- a/home/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://alpinux.org/sitemap.xml diff --git a/home/sitemap.xml b/home/sitemap.xml deleted file mode 100644 index 667684f..0000000 --- a/home/sitemap.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - https://alpinux.org/ - 2026-05-03 - monthly - 1.0 - - diff --git a/portail/README.md b/portail/README.md deleted file mode 100644 index 2e049e9..0000000 --- a/portail/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# portail.alpinux.org - -Portail membres de l'association Alpinux. - -## Statut - -Géré directement via **ISPConfig** sur le serveur. Le contenu de ce sous-domaine (Nextcloud, Dolibarr ou autre outil collaboratif) est installé et configuré côté serveur et n'est pas versioné dans ce dépôt. - -## Accès - -- URL : https://portail.alpinux.org -- Authentification : AlpID (SSO Keycloak — https://alpid.alpinux.org) - -## Notes - -Ce dossier est un espace réservé. Si un développement spécifique au portail est nécessaire (thème, plugin, scripts de déploiement), il sera ajouté ici. diff --git a/static/README.md b/static/README.md deleted file mode 100644 index ab6a03c..0000000 --- a/static/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# static.alpinux.org a déménagé - -Le CDN et son tableau de bord Flask ne sont plus ici. Ils vivent dans leur propre -dépôt, avec l'arborescence remontée à la racine (`logo/`, `app/`, `scripts/` et non -plus `static/logo/`…) : - -**https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-static** - -```bash -git clone git@gitea.alpinux.org:alpinux.cedrica5l/alpinux-static.git -``` - -Localement : `~/Projects/alpinux.static/`. - -## Pourquoi - -Un domaine, un dépôt — la même bascule que pour le wiki, `admin` et `dynamic`. -Le contenu présent ici n'était de toute façon qu'un README : les assets et l'app -étaient déjà suivis ailleurs, et ce dossier décrivait un déploiement (`a2ensite`, -copie manuelle du vhost) qui ne correspond plus à ce qui tourne. - -## Ce qui change concrètement - -Le déploiement se fait depuis le dépôt `alpinux-static`, par rsync : - -```bash -cd ~/Projects/alpinux.static -scripts/deploy-app.sh # app Flask → /opt/static-cdn + restart du service static-cdn -scripts/push-assets.sh # logo/, wiki/, stats/, error/ → web root ISPConfig -``` - -**Aucun push dans ce dépôt-ci ne publie plus quoi que ce soit sur static.alpinux.org.** -Les URLs `https://static.alpinux.org/logo/…` référencées dans `home/` et `dynamic/` -restent valides, elles pointent vers ce CDN. diff --git a/wiki/README.md b/wiki/README.md deleted file mode 100644 index dd99693..0000000 --- a/wiki/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Le wiki a déménagé - -Le contenu de **wiki.alpinux.org** n'est plus ici. Il vit maintenant dans son propre -dépôt, avec le reste de l'arborescence remontée à la racine (`docs/` et non plus -`wiki/docs/`) : - -**https://gitea.alpinux.org/alpinux.cedrica5l/alpinux-wiki** - -```bash -git clone git@gitea.alpinux.org:alpinux.cedrica5l/alpinux-wiki.git -``` - -Localement : `~/Projects/alpinux.wiki/` — c'est aussi le coffre Obsidian à ouvrir -pour rédiger. - -## Pourquoi - -La migration du monorepo vers un dépôt par domaine, faite le 2026-05-03, n'avait pas été -suivie d'effet pour le wiki : le `repo_url` de `mkdocs.yml` — le bouton « Contribuer sur -Gitea » de chaque page — pointait encore ici, et y ramenait les contributeurs. Le wiki a -donc continué d'être modifié dans ce dépôt jusqu'au 2026-09-19, date à laquelle la bascule -a été terminée. - -Un dépôt séparé permet surtout d'ouvrir l'écriture sur la documentation sans donner accès -au reste : quelqu'un qui corrige une faute de frappe n'a pas besoin des scripts de -déploiement ni des configurations serveur. - -## Ce qui change concrètement - -Le déploiement est automatique : un `git push` sur `main` du dépôt `alpinux-wiki` suffit, -un webhook construit et met le site en ligne. **Aucun push dans ce dépôt-ci ne publie plus -quoi que ce soit.** - -La procédure complète : https://wiki.alpinux.org/technique/deploiement-wiki/