#!/usr/bin/env python3 """ Pages d'erreur du portfolio (style yb2026), générées depuis un seul modèle. - out/apache/.html : servies par Apache (ErrorDocument), polices liées au thème - out/npm/.html : servies par NPM quand Apache est éteint (502/503/504), polices intégrées en base64 - out/template.php : même page en PHP pour wp_die() et wp-content/php-error.php Relancer après modification : python3 ~/yb-erreurs/build.py && sudo ~/yb-erreurs/install.sh """ import base64, html, os ROOT = os.path.dirname(os.path.abspath(__file__)) FONTS = '/home/yass/wordpress/wp-content/themes/yb2026/assets/fonts' CODES = { 400: ("Requête invalide", "Le navigateur a envoyé une requête que le serveur n'a pas su lire. Recharger la page suffit souvent."), 401: ("Authentification requise", "Cette zone demande des identifiants. Si tu n'en as pas, le reste du site est ouvert à tous."), 403: ("Accès interdit", "Cette zone est privée : le serveur refuse de l'afficher. Le reste du site, lui, est grand ouvert."), 404: ("Page introuvable", "Cette adresse ne mène nulle part. Elle a peut-être changé, ou il y a une faute de frappe."), 405: ("Méthode non autorisée", "Cette adresse n'accepte pas ce type de requête."), 408: ("Délai dépassé", "La requête a mis trop de temps à arriver. Vérifie ta connexion et réessaie."), 410: ("Page supprimée", "Ce contenu a été retiré définitivement."), 413: ("Envoi trop lourd", "Ce que tu as envoyé dépasse la taille autorisée par le serveur."), 414: ("Adresse trop longue", "L'adresse demandée est trop longue pour être traitée."), 429: ("Trop de requêtes", "Doucement : trop de demandes en peu de temps. Patiente un instant puis réessaie."), 500: ("Erreur interne", "Quelque chose a planté côté serveur. Ce n'est pas de ton fait, et c'est sûrement déjà en cours de correction."), 501: ("Non pris en charge", "Le serveur ne sait pas traiter cette demande."), 502: ("Le site ne répond pas", "Le serveur du site ne répond pas correctement pour le moment. Réessaie dans une minute."), 503: ("Maintenance en cours", "Le site redémarre ou est en maintenance. Il revient très vite, réessaie dans une minute."), 504: ("Délai dépassé", "Le serveur du site a mis trop de temps à répondre. Réessaie dans un instant."), } NPM_CODES = (502, 503, 504) def font_css(inline): faces = [("Archivo", "Archivo-100-900-latin.woff2", "100 900", "62% 125%"), ("JetBrains Mono", "JetBrainsMono-400-latin.woff2", "400 700", None)] out = [] for fam, f, w, st in faces: src = ("data:font/woff2;base64," + base64.b64encode(open(os.path.join(FONTS, f), 'rb').read()).decode()) if inline \ else "/wp-content/themes/yb2026/assets/fonts/" + f out.append("@font-face{font-family:'%s';font-weight:%s;%sfont-display:swap;src:url(%s) format('woff2')}" % (fam, w, ("font-stretch:%s;" % st) if st else "", src)) return "\n".join(out) CSS = r""" :root{--bg:#0a0a0a;--ink:#f5f5f0;--lime:#d3fd50;--ink-rgb:245,245,240;--lime-rgb:211,253,80;--mut:rgba(245,245,240,.55);--b:rgba(245,245,240,.12);color-scheme:dark} @media (prefers-color-scheme:light){:root:not([data-theme="dark"]){--bg:#f3f2ed;--ink:#111;--lime:#4a7300;--ink-rgb:17,17,17;--lime-rgb:74,115,0;--mut:rgba(17,17,17,.55);--b:rgba(17,17,17,.12);color-scheme:light}} :root[data-theme="light"]{--bg:#f3f2ed;--ink:#111;--lime:#4a7300;--ink-rgb:17,17,17;--lime-rgb:74,115,0;--mut:rgba(17,17,17,.55);--b:rgba(17,17,17,.12);color-scheme:light} *{box-sizing:border-box;margin:0;padding:0} html,body{min-height:100%;background:var(--bg);overflow-x:clip} body{color:var(--ink);font:400 16px/1.6 'Archivo',system-ui,sans-serif;display:flex;flex-direction:column;min-height:100vh;padding:0 clamp(20px,4vw,44px);overflow-x:hidden} ::selection{background:var(--lime);color:var(--bg)} a{color:inherit;text-decoration:none} .grain{position:fixed;inset:0;pointer-events:none;opacity:.05;z-index:5;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E")} .top{display:flex;justify-content:space-between;align-items:center;padding:22px 0;border-bottom:1px solid var(--b);font:700 14px 'JetBrains Mono',monospace;letter-spacing:.05em} .top b{color:var(--lime);font-weight:700} .top span{font-weight:500;font-size:11.5px;letter-spacing:.18em;color:var(--mut)} main{flex:1;display:flex;flex-direction:column;justify-content:center;gap:22px;padding:48px 0;position:relative;min-width:0} .ring{position:absolute;right:-6vw;top:50%;width:min(56vw,620px);aspect-ratio:1;transform:translateY(-50%);border-radius:50%;pointer-events:none; background:radial-gradient(circle,rgba(var(--lime-rgb),.10),transparent 62%);border:1px dashed rgba(var(--lime-rgb),.22);animation:spin 60s linear infinite} .ring::after{content:'';position:absolute;inset:16%;border-radius:50%;border:1px solid rgba(var(--ink-rgb),.08)} @keyframes spin{to{transform:translateY(-50%) rotate(360deg)}} .kick{font:700 12px 'JetBrains Mono',monospace;letter-spacing:.22em;color:var(--lime)} .code{font:900 clamp(110px,26vw,300px)/.82 'Archivo',sans-serif;font-stretch:118%;letter-spacing:-.05em;display:flex;gap:.04em;user-select:none} .code span:nth-child(odd){color:var(--ink)} .code span:nth-child(even){color:transparent;-webkit-text-stroke:2px var(--lime)} h1{font:800 clamp(28px,4.4vw,52px)/1.05 'Archivo',sans-serif;letter-spacing:-.025em;text-wrap:balance;max-width:16ch} p.msg{max-width:52ch;color:rgba(var(--ink-rgb),.78);font-size:17px} .req{font:500 12.5px 'JetBrains Mono',monospace;color:var(--mut);display:flex;gap:10px;flex-wrap:wrap;align-items:center;max-width:100%} .req code{font:inherit;color:var(--ink);background:rgba(var(--ink-rgb),.06);border:1px solid var(--b);padding:3px 8px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .req em{font-style:normal;color:var(--lime)} .actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:6px} .btn{font:700 12.5px 'JetBrains Mono',monospace;letter-spacing:.14em;padding:14px 20px;border-radius:99px;border:1px solid var(--b);transition:transform .25s cubic-bezier(.3,1.5,.5,1),border-color .2s,color .2s,background .2s;cursor:pointer;background:none;color:var(--ink)} .btn:hover{border-color:var(--lime);color:var(--lime);transform:translateY(-2px)} .btn.main{background:var(--lime);border-color:var(--lime);color:var(--bg)} .btn.main:hover{color:var(--bg)} .btn:focus-visible{outline:2px solid var(--lime);outline-offset:3px} footer{padding:20px 0 26px;border-top:1px solid var(--b);font:500 11px 'JetBrains Mono',monospace;letter-spacing:.16em;color:var(--mut);display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap} @media (max-width:640px){.ring{right:-30vw;width:90vw}.top span{display:none}} """ def page(code, title, msg, inline_fonts, php=False): digits = "".join("%s" % d for d in str(code)) e = (lambda s: s) if php else html.escape head_title = "%s · %s" % (code, title) if not php else "" t = title if not php else "" m = e(msg) if not php else "" dg = digits if not php else "' . esc_html($d) . ''; ?>" cd = str(code) if not php else "" return """ %(head)s · Yasser Boulayoune
YB©2026YASSER BOULAYOUNE · PORTFOLIO

ERREUR %(cd)s

%(title)s

%(msg)s

REQUÊTE/→ %(cd)s

ACCUEIL → CONTACT
© 2026 YASSER BOULAYOUNERÉGION LYONNAISE
""" % {"head": head_title if php else html.escape(head_title), "fonts": font_css(inline_fonts), "css": CSS, "cd": cd, "digits": dg, "title": t if php else html.escape(t), "msg": m} HUB_CSS = r""" .hub{flex:1;padding:44px 0 60px;display:flex;flex-direction:column;gap:38px} .hub h1{max-width:none} .hub .lead{max-width:62ch;color:rgba(var(--ink-rgb),.75)} .grp h2{font:700 11.5px 'JetBrains Mono',monospace;letter-spacing:.22em;color:var(--mut);padding-bottom:10px;border-bottom:1px solid var(--b);display:flex;gap:10px} .grp h2 b{color:var(--lime)} .grp p.note{margin-top:10px;font-size:14px;color:var(--mut);max-width:70ch} .row{display:grid;grid-template-columns:96px 1fr auto;gap:18px;align-items:center;padding:16px 0;border-bottom:1px solid var(--b)} .row .n{font:900 40px/1 'Archivo',sans-serif;font-stretch:115%;color:transparent;-webkit-text-stroke:1.5px var(--lime)} .row .t{min-width:0} .row .t strong{display:block;font:700 18px 'Archivo',sans-serif} .row .t span{display:block;font:500 12px 'JetBrains Mono',monospace;color:var(--mut);margin-top:3px;overflow-wrap:anywhere} .row form{margin:0} .row .btn{white-space:nowrap} @media (max-width:620px){.row{grid-template-columns:64px 1fr;}.row .n{font-size:30px}.row .go{grid-column:1/-1}} """ def hub(inline_fonts): real = [ (403, "Dossier privé (Apache)", "GET /wp-content/uploads/ · listage de dossier interdit", 'DÉCLENCHER →'), (403, "Fichier sensible (Apache)", "GET /wp-config.php · fichier protégé", 'DÉCLENCHER →'), (403, "Dossier système (Apache)", "GET /wp-includes/", 'DÉCLENCHER →'), (404, "Page introuvable (thème WordPress)", "GET /cette-page-nexiste-pas/ · c'est le 404 du thème, pas ces pages-ci", 'DÉCLENCHER →'), (403, "Refus WordPress (wp_die)", "POST /wp-comments-post.php · commentaire sur un article aux commentaires fermés", '
'), ] prev = [(c, t, "Page Apache · /yb-erreur/%d.html" % c) for c, (t, m) in CODES.items()] rows = lambda items: "".join('
%s
%s%s
%s
' % i for i in items) prev_rows = rows([(str(c), html.escape(t), html.escape(d), 'APERÇU →' % c) for c, t, d in prev] + [("500", "Erreur critique (plantage PHP)", "Drop-in wp-content/php-error.php", 'APERÇU →')]) return """ Pages d'erreur · test
YB©2026BANC D'ESSAI · PAGES D'ERREUR

TERRAIN DE TEST

Toutes les pages d'erreur

Les liens du haut provoquent de vraies erreurs sur le site, avec le vrai code HTTP. Ceux du bas ouvrent chaque page en aperçu, y compris celles qu'on ne peut pas déclencher sans casser le site.

%02d ERREURS RÉELLES

%s

%02d APERÇUS

%s

Les 502, 503 et 504 s'affichent aussi quand le site est complètement éteint : c'est alors le proxy (NPM) qui les sert, avec le même design. Les aperçus renvoient un code 200, c'est normal.

© 2026 YASSER BOULAYOUNEPAGE NON INDEXÉE
""" % (font_css(inline_fonts), CSS, HUB_CSS, len(real), rows(real), len(prev) + 1, prev_rows) os.makedirs(os.path.join(ROOT, "out/apache"), exist_ok=True) os.makedirs(os.path.join(ROOT, "out/npm"), exist_ok=True) for c, (t, m) in CODES.items(): open(os.path.join(ROOT, "out/apache/%d.html" % c), "w").write(page(c, t, m, False)) if c in NPM_CODES: open(os.path.join(ROOT, "out/npm/%d.html" % c), "w").write(page(c, t, m, True)) open(os.path.join(ROOT, "out/apache/critique.html"), "w").write(page(500, "Erreur critique", "Le site a rencontré un problème technique. Ce n'est pas de ton fait et c'est sûrement déjà en cours de correction : réessaie dans quelques minutes.", False)) open(os.path.join(ROOT, "out/apache/index.html"), "w").write(hub(False)) open(os.path.join(ROOT, "out/template.php"), "w").write("" + page(0, "", "", False, php=True)) print("ok", len(CODES), "pages apache,", len(NPM_CODES), "pages npm, 1 modèle php")